├── .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 |
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 |
--------------------------------------------------------------------------------