You can manually run a one-off ECS Fargate Task based off a Dockerized image of your API server. But if you are expecting heavy traffic, you can setup Load Balancer, Service etc. In this guide we will setup everything using CloudFormation.
Some key points to note are:
- we will create a Load Balancer with its own Security Group
- we will create a Service with its own Security Group (Tasks start automatically on creation of Service)
- we will modify Security Group (inbound rule) of Service to receive traffic from Security Group of Load Balancer
When Task Definition is revised (essentially registering the task def again) with a new ECR image tag, the old Task running in the Service is stopped and its Public IP is stripped away. After a while the Task is removed. This Public IP was actually never directly consumed since the user accesses by the Load Balancer’s IP.
Existing tasks and services that reference a DELETE_IN_PROGRESS task definition revision continue to run without disruption.
If you stop a Task managed by a Service, a new instance of the Task will start again automatically if the Service has a target of running at least 1 Task at all times.
Prerequisites
In AWS Management Console, use the following settings to create a Security Group for the Load Balancer.
- Name: APILoadBalancerSG
- Inbound rule:
- Type: Custom TCP
- Protocol: TCP
- Port range: 80
- Source: 0.0.0.0/0
Another SG for the ECS Service.
- Name: APIServiceSG
- Inbound rule:
- Type: Custom TCP
- Protocol: TCP
- Port range: 80
- Source: APILoadBalancerSG
These two SGs needed to be created beforehand because the API server in this example expected an RDS to preexist since alembic migrations are done on API task startup and the RDS instance needed a SG that referenced the APIServiceSG.
CloudFormation
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
AWSTemplateFormatVersion: 2010-09-09
Parameters:
DeploymentEnv:
Description: The environment for deployment
Type: String
AllowedValues:
- prod
VPC:
Type: AWS::EC2::VPC::Id
SubnetA:
Type: AWS::EC2::Subnet::Id
SubnetB:
Type: AWS::EC2::Subnet::Id
ContainerSecurityGroup: # the APIServiceSG created earlier
Type: AWS::EC2::SecurityGroup::Id
LoadBalancerSecurityGroup: # the APILoadBalancerSG created earlier
Type: AWS::EC2::SecurityGroup::Id
Resources:
Cluster:
Type: 'AWS::ECS::Cluster'
Properties:
ClusterName: MyAPICluster
CapacityProviders:
- FARGATE
TaskExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: MyAPITaskExecutionRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: MyAPITaskExecutionRolePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- 'ecr:GetAuthorizationToken'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
- 'ssm:DescribeParameters'
- 'ssm:GetParameterHistory'
- 'ssm:GetParametersByPath'
- 'ssm:GetParameters'
- 'ssm:GetParameter'
Resource: '*'
TaskRole:
Type: AWS::IAM::Role
Properties:
RoleName: MyAPITaskRole
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: 'sts:AssumeRole'
Policies:
- PolicyName: MyAPITaskRolePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: AllowSSM
Effect: Allow
Action:
- 'ssm:DescribeParameters'
- 'ssm:GetParameterHistory'
- 'ssm:GetParametersByPath'
- 'ssm:GetParameters'
- 'ssm:GetParameter'
- 'ssm:PutParameter'
Resource: '*'
- Sid: AllowLogs
Effect: Allow
Action:
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: '*'
- Sid: AllowECR
Effect: Allow
Action:
- 'ecr:GetAuthorizationToken'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
- 'ecr:DescribeImages'
- 'ecr:GetRepositoryPolicy'
- 'ecr:SetRepositoryPolicy'
Resource:
- !Sub 'arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/*'
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/MyAPITaskDef
RetentionInDays: 30
TaskDefinition:
Type: 'AWS::ECS::TaskDefinition'
DependsOn:
- LogGroup
- TaskExecutionRole
- TaskRole
Properties:
Family: MyAPITaskDef
ContainerDefinitions:
- EntryPoint:
- sh
- '-c'
Command:
- >-
/bin/bash -c "
alembic upgrade head;
gunicorn src.main:app --workers 1 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 --log-level=debug --timeout=90"
Essential: true
Image: !Join
- ''
- - !Ref AWS::AccountId
- .dkr.ecr.
- !Ref AWS::Region
- .amazonaws.com/
- my-api-image:latest
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: ecs
Name: MyAPIContainer
PortMappings:
- ContainerPort: 80
Protocol: tcp
Environment:
- Name: ENV
Value: !Ref DeploymentEnv
Secrets:
- Name: POSTGRES_HOST
ValueFrom: !Sub '/myapp/${DeploymentEnv}/db_host'
- Name: POSTGRES_DB
ValueFrom: !Sub '/myapp/${DeploymentEnv}/db_name'
- Name: POSTGRES_PASSWORD
ValueFrom: !Sub '/myapp/${DeploymentEnv}/db_password'
- Name: POSTGRES_PORT
ValueFrom: !Sub '/myapp/${DeploymentEnv}/db_port'
- Name: POSTGRES_USER
ValueFrom: !Sub '/myapp/${DeploymentEnv}/db_user'
- Name: AUTH0_DOMAIN
ValueFrom: !Sub '/myapp/${DeploymentEnv}/auth0/domain'
Cpu: 512
ExecutionRoleArn: !Ref TaskExecutionRole
TaskRoleArn: !Ref TaskRole
Memory: 1024
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: 60
Name: MyAPILoadBalancer
Scheme: internet-facing
SecurityGroups:
- !Ref LoadBalancerSecurityGroup
Subnets:
- !Ref SubnetA
- !Ref SubnetB
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 30
HealthCheckPath: /v1/ping
HealthCheckTimeoutSeconds: 6
UnhealthyThresholdCount: 2
HealthyThresholdCount: 2
Name: MyAPITargetGroup
Port: 80
Protocol: HTTP
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60
TargetType: ip
VpcId: !Ref VPC
ListenerHTTP:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
Service:
Type: 'AWS::ECS::Service'
DependsOn:
- ListenerHTTP
Properties:
ServiceName: MyAPIService
Cluster: !Ref Cluster
DesiredCount: 1
HealthCheckGracePeriodSeconds: 60
LaunchType: FARGATE
LoadBalancers:
- ContainerName: MyAPIContainer
ContainerPort: 80
TargetGroupArn: !Ref TargetGroup
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets:
- !Ref SubnetA
- !Ref SubnetB
SecurityGroups:
- !Ref ContainerSecurityGroup
TaskDefinition: !Ref TaskDefinition
Outputs:
Endpoint:
Description: Endpoint
Value: !Join
- ''
- - http://
- !GetAtt LoadBalancer.DNSName