How to deploy a simple public facing application on AWS Fargate using AWS SAM CLI
Dependencies
This pattern uses the AWS SAM CLI for deploying CloudFormation stacks on your AWS account.
You should follow the appropriate steps for installing SAM CLI .
About
AWS SAM CLI streamlines the deployment of your containerized applications and necessary infrastructure resources efficiently. An extension of AWS CloudFormation, SAM CLI simplifies serverless application deployment and management by allowing you to define infrastructure as code. This facilitates version control and reproducibility, simplifying the packaging and deployment of your application code, dependencies, and configurations.
This pattern will show how to deploy a simple nodeJS application to AWS Fargate using AWS SAM CLI. The following resources will be created as part of the provided templates:
Fargate Cluster
Amazon Elastic Container Registry
Fargate Service
Fargate Task Definition
Amazon VPC
Internet Gateway
2 Public Subnets
2 Private Subnets
Application Load Balancer
Architecture
The following diagram shows the architecture that will be deployed:
Define your Infrastructure
The following AWS SAM CLI template.yml creates a simple Amazon ECS cluster using AWS Fargate.
As part of the template.yml, the following resources will be created:
Amazon ECS Cluster
ECR Repo
Log Group
All IAM related roles/policies
In addition to the template.yml, you will also need a vpc.yml, where the necessary network resources are defined.
Finally, AWS SAM CLI will also look for a samconfig file, which contains default parameters for your Infrastructure as Code.
File: template.yml
Language: yaml
Download
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
AWSTemplateFormatVersion : '2010-09-09'
Transform : AWS::Serverless-2016-10-31
Description : AWS Fargate with VPC, ALB, and ECR
Parameters :
ImageTag :
Description : tag name for image
Type : String
Default : latest
Globals :
Function :
Timeout : 3
MemorySize : 128
Resources :
VPC :
Type : AWS::Serverless::Application
Properties :
Location : ./vpc.yml
ECRRepo :
Type : AWS::ECR::Repository
Properties :
EmptyOnDelete : true
Cluster :
Type : AWS::ECS::Cluster
Properties :
CapacityProviders :
- FARGATE
Service :
Type : AWS::ECS::Service
Properties :
ServiceName : "hello-world"
Cluster : !Ref Cluster
LaunchType : FARGATE
EnableExecuteCommand : true
HealthCheckGracePeriodSeconds : 5
NetworkConfiguration :
AwsvpcConfiguration :
AssignPublicIp : ENABLED
Subnets : [ !GetAtt "VPC.Outputs.PublicSubnet1", !GetAtt VPC.Outputs.PublicSubnet2]
SecurityGroups : [ !GetAtt VPC.Outputs.SG]
DeploymentConfiguration :
MaximumPercent : 200
MinimumHealthyPercent : 50
DesiredCount : 1
TaskDefinition : !Ref "TaskDefinition"
LoadBalancers :
- ContainerName : "hello-world"
ContainerPort : 3000
TargetGroupArn : !GetAtt VPC.Outputs.LB
TaskDefinition :
Type : AWS::ECS::TaskDefinition
Properties :
Family : HelloWorld
Cpu : 1024
Memory : 8192
NetworkMode : awsvpc
RequiresCompatibilities :
- FARGATE
ExecutionRoleArn : !GetAtt ECSTaskExecutionRole.Arn
TaskRoleArn : !Ref ECSTaskRole
RuntimePlatform :
CpuArchitecture : X86_64
ContainerDefinitions :
- Name : hello-world
Cpu : 1024
Memory : 8192
Image : !Sub
- ${RepoUrl}:${ImageTag}
- RepoUrl : !GetAtt ECRRepo.RepositoryUri
PortMappings :
- ContainerPort : 3000
LogConfiguration :
LogDriver : awslogs
Options :
mode : non-blocking
max-buffer-size : 25m
awslogs-group : !Ref LogGroup
awslogs-region : !Ref AWS::Region
awslogs-stream-prefix : containerlog
LogGroup :
Type : AWS::Logs::LogGroup
Properties :
LogGroupName : /fargatelogs
ECSTaskExecutionRole :
Type : AWS::IAM::Role
Properties :
AssumeRolePolicyDocument :
Statement :
- Effect : Allow
Principal :
Service : [ ecs-tasks.amazonaws.com]
Action : [ 'sts:AssumeRole' ]
Condition :
ArnLike :
aws:SourceArn : !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*
StringEquals :
aws:SourceAccount : !Ref AWS::AccountId
Path : /
ManagedPolicyArns :
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
ECSTaskRole :
Type : AWS::IAM::Role
Properties :
AssumeRolePolicyDocument :
Statement :
- Effect : Allow
Principal :
Service : [ ecs-tasks.amazonaws.com]
Action : [ 'sts:AssumeRole' ]
Condition :
ArnLike :
aws:SourceArn : !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*
StringEquals :
aws:SourceAccount : !Ref AWS::AccountId
Path : /
Outputs :
ClusterName :
Description : Amazon ECS Cluster Name
Value : !Ref Cluster
ServiceName :
Description : Amazon ECS Service Name
Value : !GetAtt Service.Name
FQDN :
Description : URL for your application
Value : !GetAtt VPC.Outputs.PublicLBFQDN
RepositoryUrl :
Description : URL of the repo
Value : !GetAtt ECRRepo.RepositoryUri
File: vpc.yml
Language: yaml
Download
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
AWSTemplateFormatVersion : '2010-09-09'
Description : VPC
Mappings :
SubnetConfig :
VPC :
CIDR : "10.0.0.0/16"
Public1 :
CIDR : "10.0.0.0/19"
Public2 :
CIDR : "10.0.32.0/19"
Private1 :
CIDR : "10.0.64.0/19"
Private2 :
CIDR : "10.0.96.0/19"
Resources :
# VPC
VPC :
Type : AWS::EC2::VPC
Properties :
EnableDnsSupport : true
EnableDnsHostnames : true
CidrBlock : !FindInMap ["SubnetConfig", "VPC", "CIDR"]
Tags :
- Key : Name
Value : VPC
# VPC Internet Gateway and Attachment
VPCInternetGateway :
Type : AWS::EC2::InternetGateway
Properties :
Tags :
- Key : Name
Value : VPC Internet Gateway
VPCInternetGatewayAttachment :
Type : AWS::EC2::VPCGatewayAttachment
Properties :
VpcId : !Ref "VPC"
InternetGatewayId : !Ref VPCInternetGateway
# Public Subnet
PublicSubnet1 :
Type : AWS::EC2::Subnet
Properties :
AvailabilityZone :
Fn::Select :
- 0
- Fn::GetAZs : { Ref : "AWS::Region" }
VpcId : !Ref "VPC"
CidrBlock : !FindInMap ["SubnetConfig", "Public1", "CIDR"]
MapPublicIpOnLaunch : true
Tags :
- Key : Name
Value : Public Subnet 1
PublicSubnet1RouteTable :
Type : AWS::EC2::RouteTable
Properties :
VpcId : !Ref VPC
Tags :
- Key : Name
Value : Public Subnet 1 Route Table
PublicSubnet1RouteTableAssociation :
Type : AWS::EC2::SubnetRouteTableAssociation
Properties :
RouteTableId : !Ref PublicSubnet1RouteTable
SubnetId : !Ref PublicSubnet1
PublicSubnet1DefaultRoute :
Type : AWS::EC2::Route
Properties :
RouteTableId :
Ref : PublicSubnet1RouteTable
DestinationCidrBlock : 0.0.0.0 /0
GatewayId : !Ref VPCInternetGateway
DependsOn :
- VPC
PublicSubnet1EIP :
Type : AWS::EC2::EIP
Properties :
Domain : vpc
PublicSubnet1NatGateway :
Type : AWS::EC2::NatGateway
Properties :
AllocationId : !GetAtt PublicSubnet1EIP.AllocationId
SubnetId :
Ref : PublicSubnet1
Tags :
- Key : Name
Value : Public Subnet 1 Nat Gateway
PublicSubnet2 :
Type : AWS::EC2::Subnet
Properties :
AvailabilityZone :
Fn::Select :
- 1
- Fn::GetAZs : { Ref : "AWS::Region" }
VpcId : !Ref "VPC"
CidrBlock : !FindInMap ["SubnetConfig", "Public2", "CIDR"]
MapPublicIpOnLaunch : true
Tags :
- Key : Name
Value : Public Subnet 2
PublicSubnet2RouteTable :
Type : AWS::EC2::RouteTable
Properties :
VpcId :
Ref : "VPC"
Tags :
- Key : Name
Value : Public Subnet 2 Route Table
PublicSubnet2RouteTableAssociation :
Type : AWS::EC2::SubnetRouteTableAssociation
Properties :
RouteTableId : !Ref PublicSubnet2RouteTable
SubnetId : !Ref PublicSubnet2
PublicSubnet2DefaultRoute :
Type : AWS::EC2::Route
Properties :
RouteTableId :
Ref : PublicSubnet2RouteTable
DestinationCidrBlock : 0.0.0.0 /0
GatewayId : !Ref VPCInternetGateway
DependsOn :
- VPC
PublicSubnet2EIP :
Type : AWS::EC2::EIP
Properties :
Domain : vpc
PublicSubnet2NatGateway :
Type : AWS::EC2::NatGateway
Properties :
AllocationId : !GetAtt PublicSubnet2EIP.AllocationId
SubnetId : !Ref PublicSubnet2
Tags :
- Key : Name
Value : Public Subnet 2 Nat Gateway
# Private Subnets
PrivateSubnet1 :
Type : AWS::EC2::Subnet
Properties :
AvailabilityZone :
Fn::Select :
- 0
- Fn::GetAZs : {Ref : 'AWS::Region' }
VpcId : !Ref 'VPC'
CidrBlock : !FindInMap ["SubnetConfig", "Private1", "CIDR"]
Tags :
- Key : Name
Value : Subnet-One
Private1RouteTable :
Type : AWS::EC2::RouteTable
Properties :
VpcId : !Ref VPC
Tags :
- Key : Name
Value : rt-1
Private1RouteTableAssociation :
Type : AWS::EC2::SubnetRouteTableAssociation
Properties :
RouteTableId : !Ref Private1RouteTable
SubnetId : !Ref PrivateSubnet1
PrivateSubnet1DefaultRoute :
Type : AWS::EC2::Route
Properties :
RouteTableId : !Ref Private1RouteTable
DestinationCidrBlock : 0.0.0.0 /0
NatGatewayId : !Ref PublicSubnet1NatGateway
PrivateSubnet2 :
Type : AWS::EC2::Subnet
Properties :
AvailabilityZone :
Fn::Select :
- 1
- Fn::GetAZs : {Ref : 'AWS::Region' }
VpcId : !Ref 'VPC'
CidrBlock : !FindInMap ["SubnetConfig", "Private2", "CIDR"]
Tags :
- Key : Name
Value : Subnet-Two
Private2RouteTable :
Type : AWS::EC2::RouteTable
Properties :
VpcId : !Ref VPC
Tags :
- Key : Name
Value : rt-2
Private2RouteTableAssociation :
Type : AWS::EC2::SubnetRouteTableAssociation
Properties :
RouteTableId : !Ref Private2RouteTable
SubnetId : !Ref PrivateSubnet2
PrivateSubnet2DefaultRoute :
Type : AWS::EC2::Route
Properties :
RouteTableId : !Ref Private2RouteTable
DestinationCidrBlock : 0.0.0.0 /0
NatGatewayId : !Ref PublicSubnet2NatGateway
# Load Balancer Configuration
PublicLoadBalancer :
DependsOn : VPCInternetGateway
Type : AWS::ElasticLoadBalancingV2::LoadBalancer
Properties :
Name : public-http-lb
Scheme : internet-facing
SecurityGroups :
- !Ref PublicHTTPSecurityGroup
Subnets :
- !Ref PublicSubnet1
- !Ref PublicSubnet2
Type : application
PublicLBTargetGroup :
Type : AWS::ElasticLoadBalancingV2::TargetGroup
Properties :
# Check if your application is healthy within 20 = 10*2 seconds, compared to 2.5 mins = 30*5 seconds.
HealthCheckIntervalSeconds : 10 # Default is 30.
HealthyThresholdCount : 2 # Default is 5.
HealthCheckTimeoutSeconds : 5
Name : public-tg
VpcId : !Ref VPC
Protocol : HTTP
TargetGroupAttributes :
- Key : deregistration_delay.timeout_seconds
Value : 0
Port : 3000
TargetType : ip
IpAddressType : ipv4
PublicLBListener :
Type : "AWS::ElasticLoadBalancingV2::Listener"
Properties :
DefaultActions :
- Type : forward
TargetGroupArn : !Ref PublicLBTargetGroup
LoadBalancerArn : !Ref PublicLoadBalancer
Port : 80
Protocol : HTTP
PublicHTTPSecurityGroup :
Type : AWS::EC2::SecurityGroup
Properties :
GroupDescription : HTTP access to the public facing load balancer
SecurityGroupEgress :
- CidrIp : "0.0.0.0/0"
Description : Allow all outbound traffic by default
IpProtocol : "-1"
SecurityGroupIngress :
# Allow traffic from services on port 3000
- CidrIp : 0.0.0.0 /0
Description : Allow traffic from services on port 3000
FromPort : 3000
IpProtocol : tcp
ToPort : 3000
VpcId : !Ref VPC
ApplicationLBSecurityGroupIngress :
Type : AWS::EC2::SecurityGroupIngress
Properties :
Description : Allow from anyone on port 80
IpProtocol : tcp
CidrIp : 0.0.0.0 /0
FromPort : 80
ToPort : 80
GroupId : !Ref PublicHTTPSecurityGroup
Outputs :
PublicLBFQDN :
Value : !Join
- ""
- - "http://"
- !GetAtt PublicLoadBalancer.DNSName
- "/"
VPCID :
Value : !Ref VPC
PublicSubnet1 :
Value : !Ref PublicSubnet1
PublicSubnet2 :
Value : !Ref PublicSubnet2
PrivateSubnet1 :
Value : !Ref PrivateSubnet1
PrivateSubnet2 :
Value : !Ref PrivateSubnet2
LB :
Value : !Ref PublicLBTargetGroup
SG :
Value : !Ref PublicHTTPSecurityGroup
File: samconfig.toml
Language: toml
Download
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# More information about the configuration file can be found here:
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
version = 0.1
[ default ]
[ default . global . parameters ]
stack_name = "nodejs-sam"
[ default . build . parameters ]
cached = true
parallel = true
[ default . validate . parameters ]
lint = true
[ default . deploy . parameters ]
capabilities = "CAPABILITY_IAM"
confirm_changeset = false
resolve_s3 = true
s3_prefix = "nodejs-sam"
region = "us-east-1"
parameter_overrides = "Tag=\"latest\""
image_repositories = []
[ default . package . parameters ]
resolve_s3 = true
[ default . sync . parameters ]
watch = true
[ default . local_start_api . parameters ]
warm_containers = "EAGER"
[ default . local_start_lambda . parameters ]
warm_containers = "EAGER"
You will also need an application you can deploy to your infrastructure. For the purpose of this demo, a simple Hello World nodeJS application is provided for you.
File: index.js
Language: Javascript
Download
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require ( 'express' )
const app = express ()
const port = 3000
app . get ( '/' , ( req , res ) => {
res . send ( 'Hello World!' )
res . status ( 200 ). json
})
app . listen ( port , () => {
console . log ( `Access your application at` , `http://localhost: ${ port } ` )
})
module . exports = app
File: package.json
Language: JSON
Download
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"name" : "hello-world" ,
"version" : "1.0.0" ,
"description" : "A simple hello-world application" ,
"main" : "index.js" ,
"scripts" : {
"start" : "node index.js" ,
"test" : "jest --forceExit --ci --reporters=default --reporters=jest-junit" ,
"lint" : "eslint . --ext .js"
},
"jest-junit" : {
"outputDirectory" : "reports" ,
"outputName" : "jest-junit.xml" ,
"ancestorSeparator" : " › " ,
"uniqueOutputName" : "false" ,
"suiteNameTemplate" : "{filepath}" ,
"classNameTemplate" : "{classname}" ,
"titleTemplate" : "{title}"
},
"author" : "Jessica Deen" ,
"license" : "MIT" ,
"dependencies" : {
"express" : "^4.18.2"
},
"devDependencies" : {
"eslint" : "^8.51.0" ,
"jest" : "^29.7.0" ,
"jest-junit" : "^16.0.0" ,
"supertest" : "^6.3.3"
}
}
File: Dockerfile
Language: Dockerfile
Download
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM node:lts-alpine
ENV NODE_ENV = production
WORKDIR /usr/src/app
COPY [ "package.json" , "package-lock.json*" , "./" ]
RUN npm install --production --silent && mv node_modules ../
COPY . .
EXPOSE 3000
RUN chown -R node /usr/src/app
USER node
CMD [ "npm" , "start" ]
Once you have the files downloaded, you can deploy your infrastructure using the following commands:
Test it Out
After the provided templates are deployed, you can find the the following AWS CloudFormation outputs using the following commands:
RepositoryUrl = $( aws cloudformation --region us-east-1 describe-stacks --stack-name nodejs-sam --query "Stacks[0].Outputs[?OutputKey=='RepositoryUrl'].OutputValue" --output text) && echo $RepositoryUrl
FQDN = $( aws cloudformation --region us-east-1 describe-stacks --stack-name nodejs-sam --query "Stacks[0].Outputs[?OutputKey=='FQDN'].OutputValue" --output text) && echo $FQDN
⚠️ Warning: You will want to build and push your container image prior to deploying this application to Fargate.
The default template will look for the image in your provisioned Amazon Elastic Container Registry with the tag “latest”. Once the image exists, you will be able to test your application using the output retrieved from the FQDN command.
💡 Tip: In Production, it’s not best practice to use the latest tag for your containerized application images. Instead, you’ll want to tag your images per release as part of your CI/CD workflow or pipeline.