AWSTemplateFormatVersion: 2010-09-09 Description: CloudFormation template for a full E2E ECS service with CI/CD on Fargate. Parameters: VPC: Type: AWS::EC2::VPC::Id Default: vpc-6f596904 Description: The id of the VPC SubnetA: Type: AWS::EC2::Subnet::Id Default: subnet-6e596905 Description: The id of the first subnet SubnetB: Type: AWS::EC2::Subnet::Id Default: subnet-6d596906 Description: The id of the second subnet DatabaseSecurityGroup: Type: String Description: Security group id to access the database Image: Type: String # Docker image for this service. It must have a latest tag # Example 260851616142.dkr.ecr.eu-west-1.amazonaws.com/nginx EnvironmentName: Type: String Description: Name of env such as staging or production BernardSQSPolicy: Type: String # CPU # 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB # 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB # 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB # 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments # 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments CpuSize: Type: Number # Memory # 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) # 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) # 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) # Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) # Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) MemorySize: Type: String ContainerPort: Type: Number Default: 80 LoadBalancerPort: Type: Number Default: 443 HealthCheckPath: Type: String Default: / Certificate: Type: String # Update with the certificate ARN from Certificate Manager, which must exist in the same region. HostedZoneName: Type: String Default: photowall.com Subdomain: Type: String # for autoscaling MinContainers: Type: Number Default: 1 # for autoscaling MaxContainers: Type: Number Default: 4 # target CPU utilization (%) AutoScalingTargetValue: Type: Number Default: 50 # Github info for code pipeline GitHubRepo: Type: String GitHubBranch: Type: String GitHubToken: Type: String NoEcho: true GitHubUser: Type: String Resources: Cluster: Type: AWS::ECS::Cluster Properties: ClusterName: !Join ['', [!Ref 'AWS::StackName', Cluster]] TaskDefinition: Type: AWS::ECS::TaskDefinition # Makes sure the log group is created before it is used. DependsOn: LogGroup Properties: # Name of the task definition. Subsequent versions of the task definition are grouped together under this name. Family: !Join ['', [!Ref 'AWS::StackName', TaskDefinition]] # awsvpc is required for Fargate NetworkMode: awsvpc RequiresCompatibilities: - FARGATE Cpu: !Ref CpuSize Memory: !Ref MemorySize # A role needed by ECS. # "The ARN of the task execution role that containers in this task can assume. All containers in this task are granted the permissions that are specified in this role." # "There is an optional task execution IAM role that you can specify with Fargate to allow your Fargate tasks to make API calls to Amazon ECR." ExecutionRoleArn: !Ref ExecutionRole # "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that grants containers in the task permission to call AWS APIs on your behalf." TaskRoleArn: !Ref TaskRole ContainerDefinitions: - Name: !Ref AWS::StackName Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${AWS::StackName}:latest Environment: - Name: DEBUG Value: True PortMappings: - ContainerPort: !Ref ContainerPort # Define which parameters should be fetched Secrets: - { Name: "SQLALCHEMY_DATABASE_URI", ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/SQLALCHEMY_DATABASE_URI" } - { Name: "API_KEYS", ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/API2_API_KEYS" } - { Name: "BERNARD_QUEUE_URL", ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/BERNARD_QUEUE_URL" } # Send logs to CloudWatch Logs LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Ref AWS::Region awslogs-group: !Ref LogGroup awslogs-stream-prefix: ecs # A role needed by ECS ExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['', [!Ref 'AWS::StackName', ExecutionRole]] AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: 'sts:AssumeRole' ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess # A role for the containers TaskRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['', [!Ref 'AWS::StackName', TaskRole]] AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: 'sts:AssumeRole' ManagedPolicyArns: - !Ref BernardSQSPolicy # A role needed for auto scaling AutoScalingRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['', [!Ref 'AWS::StackName', AutoScalingRole]] AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: 'sts:AssumeRole' ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole' ContainerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Join ['', [!Ref 'AWS::StackName', ContainerSecurityGroup]] VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref ContainerPort ToPort: !Ref ContainerPort SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup LoadBalancerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Join ['', [!Ref 'AWS::StackName', LoadBalancerSecurityGroup]] VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref LoadBalancerPort ToPort: !Ref LoadBalancerPort CidrIp: 0.0.0.0/0 Service: Type: AWS::ECS::Service # This dependency is needed so that the load balancer is setup correctly in time DependsOn: - ListenerHTTPS Properties: ServiceName: !Ref AWS::StackName Cluster: !Ref Cluster TaskDefinition: !Ref TaskDefinition DeploymentConfiguration: MinimumHealthyPercent: 100 MaximumPercent: 200 DesiredCount: 1 # This may need to be adjusted if the container takes a while to start up HealthCheckGracePeriodSeconds: 30 LaunchType: FARGATE NetworkConfiguration: AwsvpcConfiguration: # change to DISABLED if you're using private subnets that have access to a NAT gateway AssignPublicIp: ENABLED Subnets: - !Ref SubnetA - !Ref SubnetB SecurityGroups: - !Ref ContainerSecurityGroup - !Ref DatabaseSecurityGroup LoadBalancers: - ContainerName: !Ref AWS::StackName ContainerPort: !Ref ContainerPort TargetGroupArn: !Ref TargetGroup TargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckIntervalSeconds: 10 # will look for a 200 status code by default unless specified otherwise HealthCheckPath: !Ref HealthCheckPath HealthCheckTimeoutSeconds: 5 UnhealthyThresholdCount: 2 HealthyThresholdCount: 2 Name: !Join ['', [!Ref 'AWS::StackName', TargetGroup]] Port: !Ref ContainerPort Protocol: HTTP TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 # default is 300 TargetType: ip VpcId: !Ref VPC ListenerHTTPS: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - TargetGroupArn: !Ref TargetGroup Type: forward LoadBalancerArn: !Ref LoadBalancer Port: !Ref LoadBalancerPort Protocol: HTTPS Certificates: - CertificateArn: !Ref Certificate LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: LoadBalancerAttributes: # this is the default, but is specified here in case it needs to be changed - Key: idle_timeout.timeout_seconds Value: 60 Name: !Join ['', [!Ref 'AWS::StackName', LoadBalancer]] # "internal" is also an option Scheme: internet-facing SecurityGroups: - !Ref LoadBalancerSecurityGroup Subnets: - !Ref SubnetA - !Ref SubnetB DNSRecord: Type: AWS::Route53::RecordSet Properties: HostedZoneName: !Join ['', [!Ref HostedZoneName, .]] Name: !Join ['', [!Ref Subdomain, ., !Ref HostedZoneName, .]] Type: A AliasTarget: DNSName: !GetAtt LoadBalancer.DNSName HostedZoneId: !GetAtt LoadBalancer.CanonicalHostedZoneID LogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join ['', [/ecs/, !Ref 'AWS::StackName', TaskDefinition]] AutoScalingTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: MinCapacity: !Ref MinContainers MaxCapacity: !Ref MaxContainers ResourceId: !Join ['/', [service, !Ref Cluster, !GetAtt Service.Name]] ScalableDimension: ecs:service:DesiredCount ServiceNamespace: ecs # "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that allows Application Auto Scaling to modify your scalable target." RoleARN: !GetAtt AutoScalingRole.Arn AutoScalingPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !Join ['', [!Ref 'AWS::StackName', AutoScalingPolicy]] PolicyType: TargetTrackingScaling ScalingTargetId: !Ref AutoScalingTarget TargetTrackingScalingPolicyConfiguration: PredefinedMetricSpecification: PredefinedMetricType: ECSServiceAverageCPUUtilization ScaleInCooldown: 10 ScaleOutCooldown: 10 # Keep things at or lower than 50% CPU utilization, for example TargetValue: !Ref AutoScalingTargetValue ## Deployment pipeline below CodeBuildServiceRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: root PolicyDocument: Version: 2012-10-17 Statement: - Resource: "*" Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - ecr:GetAuthorizationToken - Resource: !Sub arn:aws:s3:::${ArtifactBucket}/* Effect: Allow Action: - s3:GetObject - s3:PutObject - s3:GetObjectVersion - Resource: !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${AWS::StackName} Effect: Allow Action: - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage - ecr:BatchCheckLayerAvailability - ecr:PutImage - ecr:InitiateLayerUpload - ecr:UploadLayerPart - ecr:CompleteLayerUpload CodePipelineServiceRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: root PolicyDocument: Version: 2012-10-17 Statement: - Resource: - !Sub arn:aws:s3:::${ArtifactBucket}/* Effect: Allow Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketVersioning - Resource: "*" Effect: Allow Action: - ecs:List* - ecs:Describe* - ecs:RegisterTaskDefinition - ecs:UpdateService - codebuild:StartBuild - codebuild:BatchGetBuilds - iam:PassRole ArtifactBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain CodeBuildProject: Type: AWS::CodeBuild::Project Properties: Artifacts: Type: CODEPIPELINE Source: Type: CODEPIPELINE BuildSpec: | version: 0.2 phases: pre_build: commands: - $(aws ecr get-login --no-include-email) - TAG="$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | head -c 8)" - IMAGE_URI="${REPOSITORY_URI}:${TAG}" - IMAGE_LATEST_URI="${REPOSITORY_URI}:latest" build: commands: - docker build --tag "$IMAGE_URI" . - docker tag "$IMAGE_URI" "$IMAGE_LATEST_URI" post_build: commands: - docker push "$IMAGE_URI" - docker push "$IMAGE_LATEST_URI" - printf '[{"name":"%s","imageUri":"%s"}]' "$IMAGE_NAME" "$IMAGE_LATEST_URI" > images.json artifacts: files: images.json Environment: ComputeType: BUILD_GENERAL1_SMALL Image: aws/codebuild/docker:17.09.0 Type: LINUX_CONTAINER EnvironmentVariables: - Name: AWS_DEFAULT_REGION Value: !Ref AWS::Region - Name: REPOSITORY_URI Value: !Ref Image - Name: IMAGE_NAME Value: !Ref AWS::StackName Name: !Ref AWS::StackName ServiceRole: !Ref CodeBuildServiceRole Pipeline: Type: AWS::CodePipeline::Pipeline Properties: RoleArn: !GetAtt CodePipelineServiceRole.Arn Name: !Join ['', [!Ref 'AWS::StackName', Pipeline]] ArtifactStore: Type: S3 Location: !Ref ArtifactBucket Stages: - Name: Source Actions: - Name: App ActionTypeId: Category: Source Owner: ThirdParty Version: 1 Provider: GitHub Configuration: Owner: !Ref GitHubUser Repo: !Ref GitHubRepo Branch: !Ref GitHubBranch OAuthToken: !Ref GitHubToken OutputArtifacts: - Name: App RunOrder: 1 - Name: Build Actions: - Name: Build ActionTypeId: Category: Build Owner: AWS Version: 1 Provider: CodeBuild Configuration: ProjectName: !Ref CodeBuildProject InputArtifacts: - Name: App OutputArtifacts: - Name: BuildOutput RunOrder: 1 - Name: Deploy Actions: - Name: Deploy ActionTypeId: Category: Deploy Owner: AWS Version: 1 Provider: ECS Configuration: ClusterName: !Ref Cluster ServiceName: !Ref Service FileName: images.json InputArtifacts: - Name: BuildOutput RunOrder: 1 # What outputs to expose Outputs: Endpoint: Description: Endpoint Value: !Join ['', ['https://', !Ref DNSRecord]] Service: Value: Service PipelineUrl: Value: !Sub https://console.aws.amazon.com/codepipeline/home?region=${AWS::Region}#/view/${Pipeline} ContainerSecurityGroup: Value: !Ref ContainerSecurityGroup TaskDefinition: Value: !Ref TaskDefinition