├── .gitignore ├── application ├── frontend │ ├── myweb │ │ ├── templates │ │ │ ├── health.html │ │ │ └── index.html │ │ ├── static │ │ │ ├── blue.png │ │ │ ├── green.png │ │ │ └── style.css │ │ └── app.py │ ├── requirements.txt │ └── Dockerfile └── docker-compose.yml ├── README.md ├── LICENSE ├── infrastructure └── cloudformation.yaml └── pipeline └── cloudformation.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | -------------------------------------------------------------------------------- /application/frontend/myweb/templates/health.html: -------------------------------------------------------------------------------- 1 | healthy -------------------------------------------------------------------------------- /application/frontend/myweb/static/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-containers/demo-app-for-docker-compose/HEAD/application/frontend/myweb/static/blue.png -------------------------------------------------------------------------------- /application/frontend/myweb/static/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-containers/demo-app-for-docker-compose/HEAD/application/frontend/myweb/static/green.png -------------------------------------------------------------------------------- /application/frontend/requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2 2 | Flask==1.1.2 3 | itsdangerous==1.1.0 4 | Jinja2==2.11.3 5 | MarkupSafe==1.1.1 6 | numpy==1.19.5 7 | pandas==1.1.5 8 | python-dateutil==2.8.1 9 | pytz==2021.1 10 | redis==3.5.3 11 | six==1.15.0 12 | Werkzeug==1.0.1 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo App for Docker Compose 2 | 3 | This repository includes a sample application to be used in 4 | Docker Compose demonstrations, specifically demos involving the [Docker Compose 5 | for Amazon ECS Plugin](https://docs.docker.com/cloud/ecs-integration/) 6 | 7 | This repository also includes CloudFormation templates to: 8 | - Build the foundational AWS resources (VPCs, ECS Clusters). 9 | - Build a CodePipeline to deploy the Docker Compose file to Amazon ECS in an 10 | automated fashion. 11 | -------------------------------------------------------------------------------- /application/docker-compose.yml: -------------------------------------------------------------------------------- 1 | x-aws-vpc: ${AWS_VPC} 2 | x-aws-cluster: ${AWS_ECS_CLUSTER} 3 | x-aws-loadbalancer: ${AWS_ELB} 4 | 5 | services: 6 | frontend: 7 | image: ${IMAGE_URI:-frontend}:${IMAGE_TAG:-latest} 8 | build: ./frontend 9 | environment: 10 | REDIS_URL: "backend" 11 | networks: 12 | - demoapp 13 | ports: 14 | - 80:80 15 | 16 | backend: 17 | image: public.ecr.aws/docker/library/redis:6.2 18 | volumes: 19 | - redisdata:/data 20 | networks: 21 | - demoapp 22 | 23 | volumes: 24 | redisdata: 25 | 26 | networks: 27 | demoapp: 28 | -------------------------------------------------------------------------------- /application/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/python:3.8-slim 2 | 3 | LABEL maintainer=opomer@amazon.co.uk 4 | 5 | WORKDIR /app 6 | ADD requirements.txt /app/requirements.txt 7 | 8 | RUN apt-get update && \ 9 | apt-get install --no-install-recommends curl -y && \ 10 | rm -rf /var/lib/apt/lists/* && \ 11 | pip install --no-cache-dir --upgrade pip && \ 12 | pip install --no-cache-dir -r requirements.txt 13 | 14 | COPY ./myweb /app/ 15 | 16 | EXPOSE 80 17 | 18 | HEALTHCHECK --interval=30s --timeout=5s \ 19 | CMD curl -sf http://localhost/health || exit 1 20 | 21 | ENTRYPOINT ["python"] 22 | CMD ["app.py"] 23 | -------------------------------------------------------------------------------- /application/frontend/myweb/static/style.css: -------------------------------------------------------------------------------- 1 | button { 2 | background: none; 3 | border:0px; 4 | } 5 | 6 | h1 { 7 | text-align:center; 8 | } 9 | 10 | h2 { 11 | text-align:center; 12 | } 13 | 14 | h3 { 15 | text-align:center; 16 | font-size: 10px; 17 | } 18 | 19 | table { 20 | border:1px solid black; 21 | margin-left:auto; 22 | margin-right:auto; 23 | width: 300px; 24 | text-align:left; 25 | } 26 | 27 | th { 28 | text-align:left; 29 | } 30 | 31 | body { 32 | width: 35em; 33 | margin: 0 auto; 34 | font-family: Tahoma, Verdana, Arial, sans-serif; 35 | vertical-align: middle; 36 | } 37 | 38 | button { 39 | cursor: pointer; 40 | } 41 | -------------------------------------------------------------------------------- /application/frontend/myweb/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo App 5 | 6 | 7 | 8 |

Welcome to the Demo App!



9 |
10 |
12 |
13 |

14 |

No of Clicks Today: {{no_clicks}}

15 |

Served by Container {{hostname}}

16 |


17 |

Previous Days Results

18 | {% for table in tables %} 19 | {{ table|safe }} 20 | {% endfor %} 21 |

22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /application/frontend/myweb/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import socket 4 | from datetime import date 5 | 6 | import pandas as pd 7 | from flask import Flask, g, render_template, request 8 | from redis import Redis 9 | 10 | app = Flask(__name__) 11 | 12 | redisurl = os.getenv('REDIS_URL') 13 | hostname = socket.gethostname() 14 | 15 | # Button Colour 16 | buttoncolour = "blue" 17 | button = './static/{}.png'.format(buttoncolour) 18 | 19 | 20 | def get_redis(): 21 | if not hasattr(g, 'redis'): 22 | g.redis = Redis(host=redisurl, db=0, socket_timeout=5, 23 | decode_responses=True) 24 | return g.redis 25 | 26 | 27 | def generate_table(): 28 | redis = get_redis() 29 | result = pd.DataFrame(columns=['Date', 'Clicks']) 30 | 31 | keys = redis.keys('*') 32 | for key in keys: 33 | val = redis.get(key) 34 | raw = json.dumps([{'Date': key, 'Clicks': val}]) 35 | df = pd.read_json(raw) 36 | result = result.append(df, ignore_index=True) 37 | 38 | result = result.sort_values(by=['Date'], ascending=False) 39 | 40 | return result 41 | 42 | 43 | @app.route('/', methods=['POST', 'GET']) 44 | def index(): 45 | 46 | redis = get_redis() 47 | today = str(date.today()) 48 | 49 | if request.method == 'POST': 50 | redis.incr(today) 51 | 52 | global no_clicks 53 | no_clicks = redis.get(today) 54 | 55 | df = generate_table() 56 | 57 | return render_template( 58 | 'index.html', 59 | no_clicks=no_clicks, 60 | hostname=hostname, 61 | logo=button, 62 | tables=[df.to_html(classes='data', index=False)], 63 | titles=df.columns.values) 64 | 65 | 66 | @app.route('/health', methods=['GET']) 67 | def health(): 68 | return render_template('health.html') 69 | 70 | 71 | if __name__ == '__main__': 72 | app.run(debug=True, host='0.0.0.0', port=80) 73 | -------------------------------------------------------------------------------- /infrastructure/cloudformation.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: ECS Cluster in a new VPC 3 | 4 | Parameters: 5 | VpcCidr: 6 | Description: CIDR Range for the VPC 7 | Type: String 8 | Default: 10.0.0.0/16 9 | AllowedPattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/(16|24)) 10 | PublicSubnetOneCidr: 11 | Description: CIDR Range for public subnet one 12 | Type: String 13 | Default: 10.0.1.0/24 14 | AllowedPattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/24) 15 | PublicSubnetTwoCidr: 16 | Description: CIDR Range for public subnet two 17 | Type: String 18 | Default: 10.0.2.0/24 19 | AllowedPattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/24) 20 | PrivateSubnetOneCidr: 21 | Description: CIDR Range for private subnet one 22 | Type: String 23 | Default: 10.0.3.0/24 24 | AllowedPattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/24) 25 | PrivateSubnetTwoCidr: 26 | Description: CIDR Range for private subnet two 27 | Type: String 28 | Default: 10.0.4.0/24 29 | AllowedPattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/24) 30 | 31 | Resources: 32 | VPC: 33 | Type: AWS::EC2::VPC 34 | Properties: 35 | CidrBlock: !Ref VpcCidr 36 | EnableDnsHostnames: true 37 | EnableDnsSupport: true 38 | InstanceTenancy: default 39 | Tags: 40 | - Key: Name 41 | Value: !Sub '${AWS::StackName}' 42 | 43 | PublicSubnetOne: 44 | Type: AWS::EC2::Subnet 45 | Properties: 46 | AvailabilityZone: 47 | Fn::Select: 48 | - 0 49 | - Fn::GetAZs: {Ref: 'AWS::Region'} 50 | VpcId: !Ref 'VPC' 51 | CidrBlock: !Ref PublicSubnetOneCidr 52 | MapPublicIpOnLaunch: true 53 | Tags: 54 | - Key: Name 55 | Value: !Join 56 | - "-" 57 | - - !Sub '${AWS::StackName}' 58 | - 'public-1' 59 | - Key: subnet-type 60 | Value: 'Public' 61 | 62 | PublicSubnetTwo: 63 | Type: AWS::EC2::Subnet 64 | Properties: 65 | AvailabilityZone: 66 | Fn::Select: 67 | - 1 68 | - Fn::GetAZs: {Ref: 'AWS::Region'} 69 | VpcId: !Ref 'VPC' 70 | CidrBlock: !Ref PublicSubnetTwoCidr 71 | MapPublicIpOnLaunch: true 72 | Tags: 73 | - Key: Name 74 | Value: !Join 75 | - "-" 76 | - - !Sub '${AWS::StackName}' 77 | - 'public-2' 78 | - Key: subnet-type 79 | Value: 'Public' 80 | 81 | PublicRouteTable: 82 | Type: AWS::EC2::RouteTable 83 | Properties: 84 | VpcId: 85 | Ref: VPC 86 | Tags: 87 | - Key: Name 88 | Value: !Join 89 | - "-" 90 | - - !Sub '${AWS::StackName}' 91 | - 'public-routetable' 92 | 93 | PublicSubnetOneRouteTableAssociation: 94 | Type: AWS::EC2::SubnetRouteTableAssociation 95 | Properties: 96 | RouteTableId: 97 | Ref: PublicRouteTable 98 | SubnetId: 99 | Ref: PublicSubnetOne 100 | 101 | PublicSubnetTwoRouteTableAssociation: 102 | Type: AWS::EC2::SubnetRouteTableAssociation 103 | Properties: 104 | RouteTableId: 105 | Ref: PublicRouteTable 106 | SubnetId: 107 | Ref: PublicSubnetTwo 108 | 109 | PublicInternetGateway: 110 | Type: AWS::EC2::InternetGateway 111 | Properties: 112 | Tags: 113 | - Key: Name 114 | Value: !Join 115 | - "-" 116 | - - !Sub '${AWS::StackName}' 117 | - 'internet-gateway' 118 | 119 | PublicInternetGatewayAssociation: 120 | Type: AWS::EC2::VPCGatewayAttachment 121 | Properties: 122 | VpcId: !Ref 'VPC' 123 | InternetGatewayId: !Ref 'PublicInternetGateway' 124 | 125 | PublicSubnetDefaultRoute: 126 | Type: AWS::EC2::Route 127 | Properties: 128 | RouteTableId: 129 | Ref: PublicRouteTable 130 | DestinationCidrBlock: 0.0.0.0/0 131 | GatewayId: 132 | Ref: PublicInternetGateway 133 | 134 | PublicEIP: 135 | Type: AWS::EC2::EIP 136 | Properties: 137 | Domain: vpc 138 | Tags: 139 | - Key: Name 140 | Value: !Join 141 | - "-" 142 | - - !Sub '${AWS::StackName}' 143 | - 'nat-gw-eip' 144 | 145 | PublicNatGW: 146 | Type: AWS::EC2::NatGateway 147 | Properties: 148 | AllocationId: 149 | Fn::GetAtt: 150 | - PublicEIP 151 | - AllocationId 152 | SubnetId: 153 | Ref: PublicSubnetOne 154 | Tags: 155 | - Key: Name 156 | Value: !Join 157 | - "-" 158 | - - !Sub '${AWS::StackName}' 159 | - 'nat-gw' 160 | 161 | PrivateSubnetOne: 162 | Type: AWS::EC2::Subnet 163 | Properties: 164 | AvailabilityZone: 165 | Fn::Select: 166 | - 0 167 | - Fn::GetAZs: {Ref: 'AWS::Region'} 168 | VpcId: !Ref 'VPC' 169 | CidrBlock: !Ref PrivateSubnetOneCidr 170 | Tags: 171 | - Key: Name 172 | Value: !Join 173 | - "-" 174 | - - !Sub '${AWS::StackName}' 175 | - 'private-1' 176 | - Key: subnet-type 177 | Value: 'Private' 178 | 179 | PrivateSubnetTwo: 180 | Type: AWS::EC2::Subnet 181 | Properties: 182 | AvailabilityZone: 183 | Fn::Select: 184 | - 1 185 | - Fn::GetAZs: {Ref: 'AWS::Region'} 186 | VpcId: !Ref 'VPC' 187 | CidrBlock: !Ref PrivateSubnetTwoCidr 188 | Tags: 189 | - Key: Name 190 | Value: !Join 191 | - "-" 192 | - - !Sub '${AWS::StackName}' 193 | - 'private-2' 194 | - Key: subnet-type 195 | Value: 'Private' 196 | 197 | PrivateRouteTable: 198 | Type: AWS::EC2::RouteTable 199 | Properties: 200 | VpcId: 201 | Ref: VPC 202 | Tags: 203 | - Key: Name 204 | Value: !Join 205 | - "-" 206 | - - !Sub '${AWS::StackName}' 207 | - 'private-routetable' 208 | 209 | PrivateSubnetDefaultRoute: 210 | Type: AWS::EC2::Route 211 | Properties: 212 | RouteTableId: 213 | Ref: PrivateRouteTable 214 | DestinationCidrBlock: 0.0.0.0/0 215 | NatGatewayId: 216 | Ref: PublicNatGW 217 | 218 | PrivateSubnetOneRouteTableAssociation: 219 | Type: AWS::EC2::SubnetRouteTableAssociation 220 | Properties: 221 | RouteTableId: 222 | Ref: PrivateRouteTable 223 | SubnetId: 224 | Ref: PrivateSubnetOne 225 | 226 | PrivateSubnetTwoRouteTableAssociation: 227 | Type: AWS::EC2::SubnetRouteTableAssociation 228 | Properties: 229 | RouteTableId: 230 | Ref: PrivateRouteTable 231 | SubnetId: 232 | Ref: PrivateSubnetTwo 233 | 234 | # Security Group for Demo 235 | DemoSecurityGroup: 236 | Type: AWS::EC2::SecurityGroup 237 | Properties: 238 | GroupDescription: Allow Access to Web Port from anywhere 239 | SecurityGroupIngress: 240 | - IpProtocol: tcp 241 | FromPort: 80 242 | ToPort: 80 243 | CidrIp: "0.0.0.0/0" 244 | VpcId: !Ref VPC 245 | 246 | # Application Load Balancer 247 | DemoLoadbalancer: 248 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 249 | Properties: 250 | Name: !Sub "${AWS::StackName}-alb" 251 | Type: "application" 252 | Scheme: "internet-facing" 253 | SecurityGroups: 254 | - !Ref DemoSecurityGroup 255 | Subnets: 256 | - !Ref PublicSubnetOne 257 | - !Ref PublicSubnetTwo 258 | 259 | # ECS Resources 260 | ECSCluster: 261 | Type: AWS::ECS::Cluster 262 | Properties: 263 | ClusterName: !Join ["-",[!Sub '${AWS::StackName}', 'cluster']] 264 | ClusterSettings: 265 | - Name: containerInsights 266 | Value: enabled 267 | CapacityProviders: 268 | - FARGATE 269 | - FARGATE_SPOT 270 | DefaultCapacityProviderStrategy: 271 | - CapacityProvider: FARGATE 272 | Weight: 1 273 | - CapacityProvider: FARGATE_SPOT 274 | Weight: 2 275 | 276 | # ECS Task Execution Role 277 | ECSTaskExecutionRole: 278 | Type: AWS::IAM::Role 279 | Properties: 280 | AssumeRolePolicyDocument: 281 | Statement: 282 | - Effect: Allow 283 | Principal: 284 | Service: [ecs-tasks.amazonaws.com] 285 | Action: ['sts:AssumeRole'] 286 | Path: / 287 | ManagedPolicyArns: 288 | - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 289 | 290 | # These output values will be available to service templates to use. 291 | Outputs: 292 | VpcId: 293 | Description: The ID of the VPC that this stack is deployed in 294 | Value: !Ref 'VPC' 295 | ClusterName: 296 | Description: The name of the ECS cluster 297 | Value: !Ref 'ECSCluster' 298 | LoadbalancerId: 299 | Description: The Demo App Loadbalancer Arn 300 | Value: !Ref 'DemoLoadbalancer' 301 | LoadbalancerEndpoint: 302 | Description: The Demo App Loadbalancer Endpoint 303 | Value: !Join 304 | - "" 305 | - - "http://" 306 | - !GetAtt 'DemoLoadbalancer.DNSName' 307 | -------------------------------------------------------------------------------- /pipeline/cloudformation.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Change Set CodeDeploy Pipeline 3 | 4 | Parameters: 5 | ApplicationStackName: 6 | Description: Name of the not yet created Application CFN Stack 7 | Type: String 8 | Default: compose-application 9 | AllowedPattern: (^[a-z][a-z0-9-]+$) 10 | ChangeSetName: 11 | Description: Name of Cfn Change Set 12 | Type: String 13 | Default: compose-demo 14 | AllowedPattern: (^[a-z][a-z0-9-]+$) 15 | ExistingAwsVpc: 16 | Description: Name of existing AWS Vpc 17 | Type: AWS::EC2::VPC::Id 18 | ExistingLoadbalancer: 19 | Description: ARN of existing Application Loadbalancer 20 | Type: String 21 | ExistingEcsCluster: 22 | Description: Name of existing Ecs Cluster 23 | Type: String 24 | 25 | Resources: 26 | 27 | DemoAppEcr: 28 | Type: AWS::ECR::Repository 29 | Properties: 30 | RepositoryName: !Join 31 | - "-" 32 | - - !Sub "${AWS::StackName}" 33 | - "demo-app" 34 | 35 | SourceBucket: 36 | Type: "AWS::S3::Bucket" 37 | Properties: 38 | VersioningConfiguration: 39 | Status: Enabled 40 | 41 | PipelineRole: 42 | Type: AWS::IAM::Role 43 | Properties: 44 | AssumeRolePolicyDocument: 45 | Statement: 46 | - Action: sts:AssumeRole 47 | Effect: Allow 48 | Principal: 49 | Service: codepipeline.amazonaws.com 50 | Version: "2012-10-17" 51 | 52 | PipelineRoleDefaultPolicy: 53 | Type: AWS::IAM::Policy 54 | Properties: 55 | PolicyName: PipelineRoleDefaultPolicy 56 | Roles: 57 | - Ref: PipelineRole 58 | PolicyDocument: 59 | Version: "2012-10-17" 60 | Statement: 61 | - Action: 62 | - s3:GetObject* 63 | - s3:GetBucket* 64 | - s3:List* 65 | - s3:DeleteObject* 66 | - s3:PutObject* 67 | - s3:Abort* 68 | Effect: Allow 69 | Resource: 70 | - Fn::GetAtt: 71 | - SourceBucket 72 | - Arn 73 | - Fn::Join: 74 | - "" 75 | - - Fn::GetAtt: 76 | - SourceBucket 77 | - Arn 78 | - /* 79 | - Action: 80 | - codebuild:StartBuild 81 | - codebuild:BatchGetBuilds 82 | Effect: Allow 83 | Resource: 84 | - Fn::GetAtt: 85 | - ExtractBuild 86 | - Arn 87 | - Fn::GetAtt: 88 | - ImageBuild 89 | - Arn 90 | - Action: 91 | - cloudformation:CreateChangeSet 92 | - cloudformation:DescribeStacks 93 | - cloudformation:DescribeChangeSet 94 | - cloudformation:ExecuteChangeSet 95 | Effect: Allow 96 | Resource: 97 | - !Join 98 | - "" 99 | - - "arn:aws:cloudformation:" 100 | - Ref: AWS::Region 101 | - ":" 102 | - Ref: AWS::AccountId 103 | - ":stack/" 104 | - Ref: ApplicationStackName 105 | - "/*" 106 | - Action: 107 | - iam:PassRole 108 | Effect: Allow 109 | Resource: 110 | - !GetAtt [ExtractBuildRole, Arn] 111 | 112 | ImageBuildRole: 113 | Type: AWS::IAM::Role 114 | Properties: 115 | AssumeRolePolicyDocument: 116 | Statement: 117 | - Action: sts:AssumeRole 118 | Effect: Allow 119 | Principal: 120 | Service: codebuild.amazonaws.com 121 | Version: "2012-10-17" 122 | 123 | ImageBuildRolePolicy: 124 | Type: AWS::IAM::Policy 125 | Properties: 126 | PolicyName: ImageBuildRoleDefaultPolicy 127 | Roles: 128 | - Ref: ImageBuildRole 129 | PolicyDocument: 130 | Version: "2012-10-17" 131 | Statement: 132 | - Action: 133 | - logs:CreateLogGroup 134 | - logs:CreateLogStream 135 | - logs:PutLogEvents 136 | Effect: Allow 137 | Resource: 138 | - Fn::Join: 139 | - "" 140 | - - "arn:" 141 | - Ref: AWS::Partition 142 | - ":logs:" 143 | - Ref: AWS::Region 144 | - ":" 145 | - Ref: AWS::AccountId 146 | - :log-group:/aws/codebuild/ 147 | - Ref: ImageBuild 148 | - :* 149 | - Action: 150 | - s3:GetObject* 151 | - s3:GetBucket* 152 | - s3:List* 153 | - s3:PutObject* 154 | Effect: Allow 155 | Resource: 156 | - Fn::GetAtt: 157 | - SourceBucket 158 | - Arn 159 | - Fn::Join: 160 | - "" 161 | - - Fn::GetAtt: 162 | - SourceBucket 163 | - Arn 164 | - /* 165 | - Action: 166 | - ecr:GetAuthorizationToken 167 | Effect: Allow 168 | Resource: 169 | - "*" 170 | - Action: 171 | - ecr:BatchCheckLayerAvailability 172 | - ecr:CompleteLayerUpload 173 | - ecr:InitiateLayerUpload 174 | - ecr:PutImage 175 | - ecr:UploadLayerPart 176 | Effect: Allow 177 | Resource: 178 | - !GetAtt DemoAppEcr.Arn 179 | 180 | ExtractBuildRole: 181 | Type: AWS::IAM::Role 182 | Properties: 183 | ManagedPolicyArns: 184 | - "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" 185 | AssumeRolePolicyDocument: 186 | Statement: 187 | - Action: sts:AssumeRole 188 | Effect: Allow 189 | Principal: 190 | Service: codebuild.amazonaws.com 191 | - Action: sts:AssumeRole 192 | Effect: Allow 193 | Principal: 194 | Service: cloudformation.amazonaws.com 195 | Version: "2012-10-17" 196 | 197 | ExtractBuildRoleDefaultPolicy: 198 | Type: AWS::IAM::Policy 199 | Properties: 200 | PolicyName: ExtractBuildRoleDefaultPolicy 201 | Roles: 202 | - Ref: ExtractBuildRole 203 | PolicyDocument: 204 | Version: "2012-10-17" 205 | Statement: 206 | - Action: 207 | - logs:CreateLogGroup 208 | - logs:CreateLogStream 209 | - logs:PutLogEvents 210 | Effect: Allow 211 | Resource: 212 | - Fn::Join: 213 | - "" 214 | - - "arn:" 215 | - Ref: AWS::Partition 216 | - ":logs:" 217 | - Ref: AWS::Region 218 | - ":" 219 | - Ref: AWS::AccountId 220 | - :log-group:/aws/codebuild/ 221 | - Ref: ExtractBuild 222 | - :* 223 | - Action: 224 | - s3:GetObject* 225 | - s3:GetBucket* 226 | - s3:List* 227 | - s3:PutObject* 228 | Effect: Allow 229 | Resource: 230 | - Fn::GetAtt: 231 | - SourceBucket 232 | - Arn 233 | - Fn::Join: 234 | - "" 235 | - - Fn::GetAtt: 236 | - SourceBucket 237 | - Arn 238 | - /* 239 | 240 | ComposeRolePolicy: 241 | Type: AWS::IAM::Policy 242 | Properties: 243 | PolicyName: ComposeRolePolicy 244 | Roles: 245 | - Ref: ExtractBuildRole 246 | PolicyDocument: 247 | Version: "2012-10-17" 248 | Statement: 249 | - Action: 250 | - cloudformation:* 251 | - ecs:ListAccountSettings 252 | - ecs:CreateCluster 253 | - ecs:CreateService 254 | - ecs:DeleteCluster 255 | - ecs:DeleteService 256 | - ecs:DeregisterTaskDefinition 257 | - ecs:DescribeClusters 258 | - ecs:DescribeServices 259 | - ecs:DescribeTasks 260 | - ecs:ListTasks 261 | - ecs:RegisterTaskDefinition 262 | - ecs:UpdateService 263 | - ec2:AuthorizeSecurityGroupIngress 264 | - ec2:DescribeVpcs 265 | - ec2:DescribeVpcAttribute 266 | - ec2:DescribeSubnets 267 | - ec2:DescribeRouteTables 268 | - ec2:CreateSecurityGroup 269 | - ec2:CreateTags 270 | - ec2:DescribeSecurityGroups 271 | - ec2:DeleteSecurityGroup 272 | - ec2:RevokeSecurityGroupIngress 273 | - elasticfilesystem:CreateAccessPoint 274 | - elasticfilesystem:CreateFileSystem 275 | - elasticfilesystem:CreateMountTarget 276 | - elasticfilesystem:DeleteAccessPoint 277 | - elasticfilesystem:DeleteFileSystem 278 | - elasticfilesystem:DeleteMountTarget 279 | - elasticfilesystem:DescribeAccessPoints 280 | - elasticfilesystem:DescribeBackupPolicy 281 | - elasticfilesystem:DescribeFileSystemPolicy 282 | - elasticfilesystem:DescribeFileSystems 283 | - elasticfilesystem:DescribeLifecycleConfiguration 284 | - elasticfilesystem:DescribeMountTargets 285 | - elasticfilesystem:ModifyMountTarget 286 | - elasticfilesystem:ModifyMountTargetSecurityGroups 287 | - iam:AttachRolePolicy 288 | - iam:CreateRole 289 | - iam:DeleteRole 290 | - iam:DeleteRolePolicy 291 | - iam:DetachRolePolicy 292 | - iam:PassRole 293 | - iam:PutRolePolicy 294 | - elasticloadbalancing:* 295 | - application-autoscaling:* 296 | - servicediscovery:* 297 | - logs:CreateLogGroup 298 | - logs:DescribeLogGroups 299 | - logs:FilterLogEvents 300 | - logs:DeleteLogGroup 301 | - route53:CreateHostedZone 302 | - route53:DeleteHostedZone 303 | - route53:GetHealthCheck 304 | - route53:GetHostedZone 305 | - route53:ListHostedZonesByName 306 | Effect: Allow 307 | Resource: 308 | - "*" 309 | 310 | # CodeBuild to Build the Container Image 311 | ImageBuild: 312 | Type: AWS::CodeBuild::Project 313 | Properties: 314 | Name: !Join 315 | - "-" 316 | - - !Sub ${AWS::StackName} 317 | - "ImageBuild" 318 | Artifacts: 319 | Type: CODEPIPELINE 320 | EncryptionDisabled: false 321 | Environment: 322 | ComputeType: BUILD_GENERAL1_SMALL 323 | Image: aws/codebuild/standard:5.0 324 | PrivilegedMode: true 325 | Type: LINUX_CONTAINER 326 | EnvironmentVariables: 327 | - Name: AWS_ACCOUNT_ID 328 | Type: PLAINTEXT 329 | Value: !Ref AWS::AccountId 330 | ServiceRole: !Ref ImageBuildRole 331 | Source: 332 | Type: CODEPIPELINE 333 | BuildSpec: | 334 | version: 0.2 335 | phases: 336 | pre_build: 337 | commands: 338 | - echo Logging in to Amazon ECR... 339 | - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com 340 | build: 341 | commands: 342 | - echo Building the Docker image... 343 | - cd frontend/ 344 | - docker build -t $IMAGE_URI:$IMAGE_TAG . 345 | post_build: 346 | commands: 347 | - echo Pushing the Docker image... 348 | - docker push $IMAGE_URI:$IMAGE_TAG 349 | 350 | # Code Build to Extract Cfn 351 | ExtractBuild: 352 | Type: AWS::CodeBuild::Project 353 | Properties: 354 | Name: !Join 355 | - "-" 356 | - - !Sub ${AWS::StackName} 357 | - "ExtractBuild" 358 | Artifacts: 359 | Type: CODEPIPELINE 360 | EncryptionDisabled: false 361 | Environment: 362 | ComputeType: BUILD_GENERAL1_SMALL 363 | Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0 364 | PrivilegedMode: true 365 | Type: LINUX_CONTAINER 366 | ServiceRole: !Ref ExtractBuildRole 367 | Source: 368 | Type: CODEPIPELINE 369 | BuildSpec: | 370 | version: 0.2 371 | phases: 372 | install: 373 | commands: 374 | - mv /usr/local/bin/docker /usr/bin/docker 375 | - curl -L https://raw.githubusercontent.com/docker/compose-cli/main/scripts/install/install_linux.sh | sh 376 | pre_build: 377 | commands: 378 | - echo Logging in to Amazon ECR... 379 | - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com 380 | - echo Creating Docker Compose Context 381 | - curl "http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}" > creds.json 382 | - export AWS_ACCESS_KEY_ID=$(cat creds.json | jq -r .AccessKeyId) 383 | - export AWS_SECRET_ACCESS_KEY=$(cat creds.json | jq -r .SecretAccessKey) 384 | - export AWS_SESSION_TOKEN=$(cat creds.json | jq -r .Token) 385 | - docker context create ecs demoecs --from-env 386 | - docker context use demoecs 387 | build: 388 | commands: 389 | - echo Convert Compose File 390 | - docker --debug compose convert > cloudformation.yml 391 | artifacts: 392 | files: 393 | - cloudformation.yml 394 | 395 | # Code Pipeline 396 | Pipeline: 397 | Type: AWS::CodePipeline::Pipeline 398 | Properties: 399 | ArtifactStore: 400 | Type: S3 401 | Location: !Ref "SourceBucket" 402 | RoleArn: 403 | Fn::GetAtt: 404 | - PipelineRole 405 | - Arn 406 | Stages: 407 | - Name: S3Source 408 | Actions: 409 | - Name: TemplateSource 410 | ActionTypeId: 411 | Category: Source 412 | Owner: AWS 413 | Provider: S3 414 | Version: "1" 415 | Configuration: 416 | S3Bucket: !Ref "SourceBucket" 417 | S3ObjectKey: "compose-bundle.zip" 418 | OutputArtifacts: 419 | - Name: Source 420 | - Name: Build 421 | Actions: 422 | - Name: BuildContainerImage 423 | ActionTypeId: 424 | Category: Build 425 | Owner: AWS 426 | Provider: CodeBuild 427 | Version: "1" 428 | Configuration: 429 | ProjectName: !Ref ImageBuild 430 | EnvironmentVariables: !Sub | 431 | [ 432 | { 433 | "name": "AWS_ACCOUNT_ID", 434 | "value": "${AWS::AccountId}", 435 | "type": "PLAINTEXT" 436 | }, 437 | { 438 | "name": "IMAGE_URI", 439 | "value": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${DemoAppEcr}", 440 | "type": "PLAINTEXT" 441 | }, 442 | { 443 | "name": "IMAGE_TAG", 444 | "value": "#{codepipeline.PipelineExecutionId}", 445 | "type": "PLAINTEXT" 446 | } 447 | ] 448 | InputArtifacts: 449 | - Name: Source 450 | OutputArtifacts: 451 | - Name: ImageBuild 452 | - Name: Compose2Cloudformation 453 | Actions: 454 | - Name: ExtractCFN 455 | ActionTypeId: 456 | Category: Build 457 | Owner: AWS 458 | Provider: CodeBuild 459 | Version: "1" 460 | Configuration: 461 | ProjectName: !Ref ExtractBuild 462 | EnvironmentVariables: !Sub | 463 | [ 464 | { 465 | "name": "AWS_ACCOUNT_ID", 466 | "value": "${AWS::AccountId}", 467 | "type": "PLAINTEXT" 468 | }, 469 | { 470 | "name": "IMAGE_URI", 471 | "value": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${DemoAppEcr}", 472 | "type": "PLAINTEXT" 473 | }, 474 | { 475 | "name": "IMAGE_TAG", 476 | "value": "#{codepipeline.PipelineExecutionId}", 477 | "type": "PLAINTEXT" 478 | }, 479 | { 480 | "name": "AWS_ECS_CLUSTER", 481 | "value": "${ExistingEcsCluster}", 482 | "type": "PLAINTEXT" 483 | }, 484 | { 485 | "name": "AWS_VPC", 486 | "value": "${ExistingAwsVpc}", 487 | "type": "PLAINTEXT" 488 | }, 489 | { 490 | "name": "AWS_ELB", 491 | "value": "${ExistingLoadbalancer}", 492 | "type": "PLAINTEXT" 493 | } 494 | ] 495 | InputArtifacts: 496 | - Name: Source 497 | OutputArtifacts: 498 | - Name: ExtractedCfn 499 | - Name: DeployStage 500 | Actions: 501 | - Name: CreateChangeSet 502 | ActionTypeId: 503 | Category: Deploy 504 | Owner: AWS 505 | Provider: CloudFormation 506 | Version: "1" 507 | InputArtifacts: 508 | - Name: ExtractedCfn 509 | Configuration: 510 | ActionMode: CHANGE_SET_REPLACE 511 | RoleArn: !GetAtt [ExtractBuildRole, Arn] 512 | StackName: !Ref ApplicationStackName 513 | ChangeSetName: !Ref ChangeSetName 514 | TemplatePath: "ExtractedCfn::cloudformation.yml" 515 | Capabilities: CAPABILITY_IAM 516 | RunOrder: 1 517 | - Name: ApproveChangeSet 518 | ActionTypeId: 519 | Category: Approval 520 | Owner: AWS 521 | Provider: Manual 522 | Version: "1" 523 | RunOrder: 2 524 | - Name: ExecuteChangeSet 525 | ActionTypeId: 526 | Category: Deploy 527 | Owner: AWS 528 | Provider: CloudFormation 529 | Version: "1" 530 | Configuration: 531 | ActionMode: CHANGE_SET_EXECUTE 532 | StackName: !Ref ApplicationStackName 533 | ChangeSetName: !Ref ChangeSetName 534 | RoleArn: !GetAtt [ExtractBuildRole, Arn] 535 | RunOrder: 3 536 | 537 | Outputs: 538 | DemoAppEcr: 539 | Description: ECR Repository to store the Demo App Image 540 | Value: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${DemoAppEcr}" 541 | DemoAppEcrName: 542 | Description: ECR Repository to store the Demo App Image 543 | Value: !Ref DemoAppEcr 544 | S3BucketName: 545 | Description: S3 Bucket to store Application Source Code 546 | Value: !Ref SourceBucket 547 | --------------------------------------------------------------------------------