--- AWSTemplateFormatVersion: 2010-09-09 Description: AWS SaaS Factory Bootcamp Baseline Parameters: BootcampBucket: Description: S3 bucket with workshop resources Type: String ParticipantAssumedRoleArn: Description: Workshop Studio Participant IAM Role. Leave blank when not using Workshop Studio. Type: String Default: '' ArtifactBucket: Description: S3 bucket for CodeBuild artifacts Type: String WebsiteBucket: Description: S3 bucket to host the website Type: String Conditions: WorkshopStudio: !Not [!Equals ['', !Ref ParticipantAssumedRoleArn]] Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: saas-bootcamp-vpc InternetGateway: Type: AWS::EC2::InternetGateway DependsOn: VPC Properties: Tags: - Key: Name Value: saas-bootcamp-igw AttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway RouteTablePublic: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: saas-bootcamp-route-pub RoutePublic: Type: AWS::EC2::Route DependsOn: AttachGateway Properties: RouteTableId: !Ref RouteTablePublic DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway SubnetPublicA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [0, !GetAZs ''] CidrBlock: 10.0.32.0/19 Tags: - Key: Name Value: saas-bootcamp-subA-pub SubnetPublicARouteTable: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPublicA RouteTableId: !Ref RouteTablePublic SubnetPublicB: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [1, !GetAZs ''] CidrBlock: 10.0.96.0/19 Tags: - Key: Name Value: saas-bootcamp-subB-pub SubnetPublicBRouteTable: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPublicB RouteTableId: !Ref RouteTablePublic NatGatewayAddrA: Type: AWS::EC2::EIP DependsOn: AttachGateway Properties: Domain: vpc NatGatewayA: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayAddrA.AllocationId SubnetId: !Ref SubnetPublicA Tags: - Key: Name Value: saas-bootcamp-nat-subA-pub RouteTableNatA: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: saas-bootcamp-route-natA RouteNatA: Type: AWS::EC2::Route Properties: RouteTableId: !Ref RouteTableNatA DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGatewayA SubnetPrivateA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [0, !GetAZs ''] CidrBlock: 10.0.0.0/19 Tags: - Key: Name Value: saas-bootcamp-subA-priv SubnetPrivateARouteTable: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPrivateA RouteTableId: !Ref RouteTableNatA NatGatewayAddrB: Type: AWS::EC2::EIP DependsOn: AttachGateway Properties: Domain: vpc NatGatewayB: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayAddrB.AllocationId SubnetId: !Ref SubnetPublicB Tags: - Key: Name Value: saas-bootcamp-nat-subB-pub RouteTableNatB: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: saas-bootcamp-route-natB RouteNatB: Type: AWS::EC2::Route Properties: RouteTableId: !Ref RouteTableNatB DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGatewayB SubnetPrivateB: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [1, !GetAZs ''] CidrBlock: 10.0.64.0/19 Tags: - Key: Name Value: saas-bootcamp-subB-priv SubnetPrivateBRouteTable: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPrivateB RouteTableId: !Ref RouteTableNatB Cloud9: Type: AWS::Cloud9::EnvironmentEC2 Properties: AutomaticStopTimeMinutes: 60 SubnetId: !Ref SubnetPublicA InstanceType: t3.medium Name: SaaS Bootcamp IDE Repositories: - RepositoryUrl: https://github.com/aws-samples/aws-saas-factory-bootcamp PathComponent: aws-saas-factory-bootcamp OwnerArn: !If [WorkshopStudio, !Ref ParticipantAssumedRoleArn, !Ref "AWS::NoValue"] ECSCluster: Type: AWS::ECS::Cluster Properties: ClusterName: saas-bootcamp ECSSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: saas-bootcamp-ecs-sg GroupDescription: Access to Fargate Containers VpcId: !Ref VPC ECSSecurityGroupIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref ECSSecurityGroup SourceSecurityGroupId: !Ref ALBSecurityGroup IpProtocol: -1 ALBSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: saas-bootcamp-alb-sg GroupDescription: Access to the load balancer VpcId: !Ref VPC SecurityGroupIngress: - CidrIp: 0.0.0.0/0 IpProtocol: tcp FromPort: 80 ToPort: 80 ECSLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer DependsOn: AttachGateway Properties: Scheme: internet-facing LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: 30 Subnets: - !Ref SubnetPublicA - !Ref SubnetPublicB SecurityGroups: - !Ref ALBSecurityGroup ALBTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Name: saas-bootcamp-default-tg Port: 80 Protocol: HTTP TargetType: ip VpcId: !Ref VPC ALBListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: LoadBalancerArn: !Ref ECSLoadBalancer DefaultActions: - Type: forward TargetGroupArn: !Ref ALBTargetGroup Port: 80 Protocol: HTTP LambdaCodeBuildStartBuildExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub saas-bootcamp-cfn-codebuild-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-bootcamp-cfn-codebuild-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - Effect: Allow Action: - logs:CreateLogStream - logs:DescribeLogStreams Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - codebuild:StartBuild Resource: !Sub arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/* - Effect: Allow Action: - s3:ListBucket - s3:ListBucketVersions - s3:GetBucketVersioning Resource: - !Sub arn:aws:s3:::${BootcampBucket} - Effect: Allow Action: - s3:GetObject - s3:PutObject - s3:DeleteObject - s3:DeleteObjectVersion Resource: - !Sub arn:aws:s3:::${BootcampBucket}/* LambdaCodeBuildStartBuildLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/saas-bootcamp-codebuild-start RetentionInDays: 7 LambdaCodeBuildStartBuild: Type: AWS::Lambda::Function DependsOn: LambdaCodeBuildStartBuildLogs Properties: FunctionName: saas-bootcamp-codebuild-start Role: !GetAtt LambdaCodeBuildStartBuildExecutionRole.Arn Runtime: python3.9 Timeout: 60 MemorySize: 512 Handler: index.lambda_handler Code: ZipFile: | import json import boto3 import cfnresponse from botocore.exceptions import ClientError def lambda_handler(event, context): print(json.dumps(event, default=str)) if event['RequestType'] == 'Create': try: codebuild = boto3.client('codebuild') response = codebuild.start_build(projectName = event['ResourceProperties']['Project']) cfnresponse.send(event, context, cfnresponse.SUCCESS, {"BuildStatus": response['build']['buildStatus']}) except ClientError as codebuild_error: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": str(codebuild_error)}) raise elif event['RequestType'] in ['Update', 'Delete']: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) else: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": "Unknown RequestType %s" % event['RequestType']}) CodeBuildRole: Type: AWS::IAM::Role Properties: RoleName: !Sub saas-bootcamp-codebuild-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - codebuild.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-bootcamp-codebuild-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - Effect: Allow Action: - logs:CreateLogStream - logs:DescribeLogStreams Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - s3:listBucket - s3:GetBucketVersioning Resource: - !Sub arn:aws:s3:::${BootcampBucket} - !Sub arn:aws:s3:::${ArtifactBucket} - !Sub arn:aws:s3:::${WebsiteBucket} - Effect: Allow Action: - s3:DeleteObject - s3:PutObject - s3:PutObjectAcl - s3:GetObject - s3:GetObjectVersion Resource: - !Sub arn:aws:s3:::${BootcampBucket}/* - !Sub arn:aws:s3:::${ArtifactBucket}/* - !Sub arn:aws:s3:::${WebsiteBucket}/* - Effect: Allow Action: - ecr:GetAuthorizationToken Resource: '*' - Effect: Allow Action: - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage - ecr:BatchCheckLayerAvailability - ecr:PutImage - ecr:InitiateLayerUpload - ecr:UploadLayerPart - ecr:CompleteLayerUpload Resource: - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/user-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/auth-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/tenant-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/order-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/product-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/tenant-registration CodePipelineRole: Type: AWS::IAM::Role Properties: RoleName: !Sub saas-bootcamp-codepipeline-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - codepipeline.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonECS_FullAccess Policies: - PolicyName: saas-bootcamp-codepipeline-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - iam:PassRole Resource: '*' Condition: StringEqualsIfExists: iamPassedToService: - ecs-tasks.amazonaws.com - Effect: Allow Action: - s3:GetBucketVersioning Resource: - !Sub arn:aws:s3:::${ArtifactBucket} - Effect: Allow Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion Resource: - !Sub arn:aws:s3:::${ArtifactBucket}/* - Effect: Allow Action: - lambda:ListFunctions Resource: '*' - Effect: Allow Action: - lambda:InvokeFunction Resource: - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:saas-bootcamp-update-ecs CodePipelineUpdateEcsServiceExecRole: Type: AWS::IAM::Role Properties: RoleName: !Sub saas-bootcamp-update-ecs-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-bootcamp-update-ecs-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - Effect: Allow Action: - logs:CreateLogStream - logs:DescribeLogStreams Resource: - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - codepipeline:PutJobSuccessResult - codepipeline:PutJobFailureResult Resource: '*' - Effect: Allow Action: - ecs:DescribeServices - ecs:UpdateService Resource: - !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:service/saas-bootcamp* Condition: StringLike: ecs:cluster: - !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:cluster/saas-bootcamp CodePipelineUpdateEcsServiceLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/saas-bootcamp-update-ecs RetentionInDays: 7 CodePipelineUpdateEcsServiceLambda: Type: AWS::Lambda::Function DependsOn: CodePipelineUpdateEcsServiceLogs Properties: FunctionName: saas-bootcamp-update-ecs Role: !GetAtt CodePipelineUpdateEcsServiceExecRole.Arn Runtime: python3.9 Timeout: 300 MemorySize: 512 Handler: index.lambda_handler Code: ZipFile: | import json import boto3 from botocore.exceptions import ClientError ecs = boto3.client('ecs') codepipeline = boto3.client('codepipeline') def lambda_handler(event, context): job = event['CodePipeline.job'] job_id = job['id'] job_params = json.loads(job['data']['actionConfiguration']['configuration']['UserParameters']) cluster = job_params['cluster'] service = job_params['service'] count = job_params['desiredCount'] # If the desired count for the ECS service is less than the requested count # update the service before continuing the CodePipeline try: current_settings = ecs.describe_services(cluster=cluster, services=[service]) for current_setting in current_settings['services']: if current_setting['desiredCount'] < count: try: ecs.update_service(cluster=cluster, service=service, desiredCount=count) except ClientError as ecs_error: print("ecs:UpdateService %s" % str(ecs_error)) fail_job(job_id, "Error calling ecs:UpdateService for %s in cluster %s" % (service, cluster), context) raise except ClientError as ecs_error: print("ecs:DescribeServices %s" % str(ecs_error)) fail_job(job_id, "Error calling ecs:DescribeServices for %s in cluster %s" % (service, cluster), context) raise # Tell CodePipeline to continue codepipeline.put_job_success_result(jobId=job_id) def fail_job(job_id, message, context): try: details = { "type": "JobFailed", "message": message, "externalExecutionId": context.aws_request_id } codepipeline.put_job_failure_result(jobId=job_id, failureDetails=details) except ClientError as codepipeline_error: print("codepipeline:PutJobFailureResult %s" % str(codepipeline_error)) raise CodePipelineUpdateEcsServicePermission: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref CodePipelineUpdateEcsServiceLambda Principal: codepipeline.amazonaws.com Action: lambda:InvokeFunction CodePipelineEventRole: Type: AWS::IAM::Role Properties: RoleName: !Sub saas-bootcamp-event-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-bootcamp-event-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - codepipeline:StartPipelineExecution Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:* ClearEcrImagesExecRole: Type: AWS::IAM::Role Properties: RoleName: !Sub saas-bootcamp-clear-ecr-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-bootcamp-clear-ecr-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - Effect: Allow Action: - logs:CreateLogStream - logs:DescribeLogStreams Resource: - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - ecr:DescribeImages - ecr:BatchDeleteImage Resource: - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/user-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/auth-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/tenant-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/order-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/product-manager - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-bootcamp/tenant-registration ClearEcrImagesLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/saas-bootcamp-clear-ecr RetentionInDays: 7 ClearEcrImagesLambda: Type: AWS::Lambda::Function DependsOn: ClearEcrImagesLogs Properties: FunctionName: saas-bootcamp-clear-ecr Role: !GetAtt ClearEcrImagesExecRole.Arn Runtime: python3.9 Timeout: 300 MemorySize: 512 Handler: index.lambda_handler Code: ZipFile: | import json import boto3 import cfnresponse from botocore.exceptions import ClientError def lambda_handler(event, context): print(json.dumps(event, default=str)) if event['RequestType'] in ['Create', 'Update']: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) elif event['RequestType'] == 'Delete': ecr = boto3.client('ecr') repo = event['ResourceProperties']['Repository'] try: images = [] token = None while True: if not token: images_response = ecr.describe_images(repositoryName=repo, maxResults=1000) else: images_response = ecr.describe_images(repositoryName=repo, nextToken=token, maxResults=1000) token = images_response['nextToken'] if 'nextToken' in images_response else '' if 'imageDetails' in images_response: for image_detail in images_response['imageDetails']: images.append({"imageDigest": image_detail['imageDigest']}) if not token: break print("Deleting %d images from repo %s" % (len(images), repo)) if len(images) > 0: delete_response = ecr.batch_delete_image(repositoryName=repo, imageIds=images) if 'failures' in delete_response and len(delete_response['failures']) > 0: for fail in delete_response['failures']: print("Failed to delete image %s %s in repo %s" % (fail['imageId']['imageDigest'], fail['failureReason'], repo)) cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": "ecr:BatchDeleteImage failed"}) else: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) else: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except ClientError as ecr_error: print("ecr error %s" % str(ecr_error)) raise else: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": "Unknown RequestType %s" % event['RequestType']}) ApiGateway: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootcampBucket}/resources/api-gateway.yml Parameters: ECSLoadBalancer: !GetAtt ECSLoadBalancer.DNSName UserService: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootcampBucket}/resources/user-service.yml Parameters: BootcampBucket: !Ref BootcampBucket ArtifactBucket: !Ref ArtifactBucket CodeBuildRoleArn: !GetAtt CodeBuildRole.Arn CodePipelineRoleArn: !GetAtt CodePipelineRole.Arn CodePipelineEventRoleArn: !GetAtt CodePipelineEventRole.Arn LambdaCodeBuildStartBuildArn: !GetAtt LambdaCodeBuildStartBuild.Arn LambdaClearEcrImagesArn: !GetAtt ClearEcrImagesLambda.Arn VPC: !Ref VPC ALBListener: !Ref ALBListener ECSCluster: !Ref ECSCluster ServiceUrl: !GetAtt ApiGateway.Outputs.ApiGatewayEndpoint ECSSecurityGroup: !Ref ECSSecurityGroup SubnetPrivateA: !Ref SubnetPrivateA SubnetPrivateB: !Ref SubnetPrivateB AuthService: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootcampBucket}/resources/auth-service.yml Parameters: BootcampBucket: !Ref BootcampBucket ArtifactBucket: !Ref ArtifactBucket CodeBuildRoleArn: !GetAtt CodeBuildRole.Arn CodePipelineRoleArn: !GetAtt CodePipelineRole.Arn CodePipelineEventRoleArn: !GetAtt CodePipelineEventRole.Arn LambdaCodeBuildStartBuildArn: !GetAtt LambdaCodeBuildStartBuild.Arn LambdaClearEcrImagesArn: !GetAtt ClearEcrImagesLambda.Arn VPC: !Ref VPC ALBListener: !Ref ALBListener ECSCluster: !Ref ECSCluster ServiceUrl: !GetAtt ApiGateway.Outputs.ApiGatewayEndpoint ECSSecurityGroup: !Ref ECSSecurityGroup SubnetPrivateA: !Ref SubnetPrivateA SubnetPrivateB: !Ref SubnetPrivateB TenantService: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootcampBucket}/resources/tenant-service.yml Parameters: BootcampBucket: !Ref BootcampBucket ArtifactBucket: !Ref ArtifactBucket CodeBuildRoleArn: !GetAtt CodeBuildRole.Arn CodePipelineRoleArn: !GetAtt CodePipelineRole.Arn CodePipelineEventRoleArn: !GetAtt CodePipelineEventRole.Arn LambdaCodeBuildStartBuildArn: !GetAtt LambdaCodeBuildStartBuild.Arn LambdaClearEcrImagesArn: !GetAtt ClearEcrImagesLambda.Arn VPC: !Ref VPC ALBListener: !Ref ALBListener ECSCluster: !Ref ECSCluster ServiceUrl: !GetAtt ApiGateway.Outputs.ApiGatewayEndpoint ECSSecurityGroup: !Ref ECSSecurityGroup SubnetPrivateA: !Ref SubnetPrivateA SubnetPrivateB: !Ref SubnetPrivateB RegistrationService: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootcampBucket}/resources/registration-service.yml Parameters: BootcampBucket: !Ref BootcampBucket ArtifactBucket: !Ref ArtifactBucket CodeBuildRoleArn: !GetAtt CodeBuildRole.Arn CodePipelineRoleArn: !GetAtt CodePipelineRole.Arn CodePipelineEventRoleArn: !GetAtt CodePipelineEventRole.Arn LambdaCodeBuildStartBuildArn: !GetAtt LambdaCodeBuildStartBuild.Arn LambdaClearEcrImagesArn: !GetAtt ClearEcrImagesLambda.Arn VPC: !Ref VPC ALBListener: !Ref ALBListener ECSCluster: !Ref ECSCluster ServiceUrl: !GetAtt ApiGateway.Outputs.ApiGatewayEndpoint ECSSecurityGroup: !Ref ECSSecurityGroup SubnetPrivateA: !Ref SubnetPrivateA SubnetPrivateB: !Ref SubnetPrivateB OrderService: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootcampBucket}/resources/order-service.yml Parameters: BootcampBucket: !Ref BootcampBucket ArtifactBucket: !Ref ArtifactBucket CodeBuildRoleArn: !GetAtt CodeBuildRole.Arn CodePipelineRoleArn: !GetAtt CodePipelineRole.Arn CodePipelineEventRoleArn: !GetAtt CodePipelineEventRole.Arn LambdaCodeBuildStartBuildArn: !GetAtt LambdaCodeBuildStartBuild.Arn LambdaClearEcrImagesArn: !GetAtt ClearEcrImagesLambda.Arn VPC: !Ref VPC ALBListener: !Ref ALBListener ECSCluster: !Ref ECSCluster ServiceUrl: !GetAtt ApiGateway.Outputs.ApiGatewayEndpoint ECSSecurityGroup: !Ref ECSSecurityGroup SubnetPrivateA: !Ref SubnetPrivateA SubnetPrivateB: !Ref SubnetPrivateB ProductService: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootcampBucket}/resources/product-service.yml Parameters: BootcampBucket: !Ref BootcampBucket ArtifactBucket: !Ref ArtifactBucket CodeBuildRoleArn: !GetAtt CodeBuildRole.Arn CodePipelineRoleArn: !GetAtt CodePipelineRole.Arn CodePipelineEventRoleArn: !GetAtt CodePipelineEventRole.Arn LambdaCodeBuildStartBuildArn: !GetAtt LambdaCodeBuildStartBuild.Arn LambdaClearEcrImagesArn: !GetAtt ClearEcrImagesLambda.Arn VPC: !Ref VPC ALBListener: !Ref ALBListener ECSCluster: !Ref ECSCluster ServiceUrl: !GetAtt ApiGateway.Outputs.ApiGatewayEndpoint ECSSecurityGroup: !Ref ECSSecurityGroup SubnetPrivateA: !Ref SubnetPrivateA SubnetPrivateB: !Ref SubnetPrivateB WebClient: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootcampBucket}/resources/web-client.yml Parameters: BootcampBucket: !Ref BootcampBucket WebsiteBucket: !Ref WebsiteBucket CodeBuildRoleArn: !GetAtt CodeBuildRole.Arn LambdaCodeBuildStartBuildArn: !GetAtt LambdaCodeBuildStartBuild.Arn ServiceUrl: !GetAtt ApiGateway.Outputs.ApiGatewayEndpoint Outputs: ApiGatewayEndpoint: Description: API Gateway Invoke URL Value: !GetAtt ApiGateway.Outputs.ApiGatewayEndpoint WebAppUrl: Description: Web application URL Value: !GetAtt WebClient.Outputs.WebAppUrl ...