Creating An ECS cluster with CloudFormation
Oluwafemi Lawal
Posted on July 6, 2022
What is ECS?
Amazon Elastic Container Service (Amazon ECS) is the AWS container orchestration service that runs and manages Docker containers. With ECS, you can run clusters of virtual machines with either the EC2-backed option or the serverless option with Fargate.
You can read more about ECS in the AWS documentation
Infrastructure
Disclaimer: This is not created for a production use case. It is just for illustration purposes.
Things that will be created included in this post:
- VPC
- Internet Gateway
- Public Subnets
- Route tables
- ECS cluster
- ECS Service
- Task definition
- IAM roles
- Application Load balancer
- Target group
- Security groups
- CloudWatch Log groups
That's a long list, but we will accomplish this with CloudFormation. To use HTTPS with the load balancer, you would need to create an SSL certificate in AWS Certificate Manager, but this post will make a load balancer with HTTP, and the cluster will be in public subnets instead of private.
The application used is a simple rest api.
You can create an ECR repository by following this post and push the docker image to the repo following this post.
I could divide the CloudFormation template into multiple templates, then export and import values when needed, and give detailed explanations for what each template does, but that would make this post very long...
I'm going to post everything in a single template, you can consult the CloudFormation documentation for explanations
AWSTemplateFormatVersion: "2010-09-09"
Description: >
This template creates ECS Fargate Services.
############################################################
# PARAMETERS RECORDS BLOCK
############################################################
Parameters:
ImageTag:
Type: String
Default: latest
ServiceName:
Type: String
Default: rest-api
ContainerPort:
Type: Number
Default: 80
ContainerCpu:
Type: String
Default: 256
ContainerMemory:
Type: String
Default: 1GB
HealthCheckPath:
Type: String
Default: /
MinContainers:
Type: Number
Default: 1
VpcCIDR:
Description: "IP range for this VPC"
Type: "String"
Default: "10.0.0.0/16"
PublicSubnetOneCIDR:
Description: "IP range for the public subnet in the first Availability Zone"
Type: "String"
Default: "10.0.1.0/24"
PublicSubnetTwoCIDR:
Description: "IP range for the public subnet in the second Availability Zone"
Type: "String"
Default: "10.0.2.0/24"
Resources:
############################################################
# VPC & PUBLIC SUBNETS
############################################################
RestVPC:
Type: "AWS::EC2::VPC"
Properties:
CidrBlock: !Ref "VpcCIDR"
Tags:
- Key: "Name"
Value: "Rest-API-VPC"
####### Create Public Subnet #######
PublicSubnetOne:
Type: "AWS::EC2::Subnet"
Properties:
VpcId: !Ref RestVPC
CidrBlock: !Ref "PublicSubnetOneCIDR"
AvailabilityZone: !Select ["0", !GetAZs ]
MapPublicIpOnLaunch: "True"
Tags:
- Key: "Name"
Value: !Sub "${PublicSubnetOneCIDR}-PublicSubnetOne"
PublicSubnetTwo:
Type: "AWS::EC2::Subnet"
Properties:
VpcId: !Ref RestVPC
CidrBlock: !Ref "PublicSubnetTwoCIDR"
AvailabilityZone: !Select ["1", !GetAZs ]
MapPublicIpOnLaunch: "True"
Tags:
- Key: "Name"
Value: !Sub "${PublicSubnetTwoCIDR}-PublicSubnetTwo"
######## Create Public Route Table #######
PublicRouteTable1:
Type: "AWS::EC2::RouteTable"
Properties:
VpcId: !Ref RestVPC
Tags:
- Key: "Name"
Value: "PublicRoute1"
PublicRouteTable2:
Type: "AWS::EC2::RouteTable"
Properties:
VpcId: !Ref RestVPC
Tags:
- Key: "Name"
Value: "PublicRoute2"
######## Create Internet Gateway #######
InternetGateway:
Type: "AWS::EC2::InternetGateway"
Properties:
Tags:
- Key: "Name"
Value: "InternetGateway"
######## Attach Internet Gateway to VPC #######
GatewayToInternet:
Type: "AWS::EC2::VPCGatewayAttachment"
Properties:
VpcId: !Ref RestVPC
InternetGatewayId: !Ref "InternetGateway"
######## Route-out Public Route Table to Internet Gateway (Internet connection) #######
PublicRouteIGW1:
Type: "AWS::EC2::Route"
DependsOn: "GatewayToInternet"
Properties:
RouteTableId: !Ref "PublicRouteTable1"
DestinationCidrBlock: "0.0.0.0/0"
GatewayId: !Ref "InternetGateway"
PublicRouteIGW2:
Type: "AWS::EC2::Route"
DependsOn: "GatewayToInternet"
Properties:
RouteTableId: !Ref "PublicRouteTable2"
DestinationCidrBlock: "0.0.0.0/0"
GatewayId: !Ref "InternetGateway"
######## Associate Public Route Table with Public Subnet1 & Subnet2 #######
PublicSubnetOneRouteTableAssociation:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
SubnetId: !Ref "PublicSubnetOne"
RouteTableId: !Ref "PublicRouteTable1"
PublicSubnetTwoRouteTableAssociation:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
SubnetId: !Ref "PublicSubnetTwo"
RouteTableId: !Ref "PublicRouteTable2"
######## Create Custom Network ACL #######
PublicNetworkACL:
Type: "AWS::EC2::NetworkAcl"
Properties:
VpcId: !Ref RestVPC
Tags:
- Key: "Name"
Value: "PublicNetworkACL"
PublicInboundPublicACL:
Type: "AWS::EC2::NetworkAclEntry"
Properties:
NetworkAclId: !Ref PublicNetworkACL
RuleNumber: "100"
Protocol: "-1"
RuleAction: "allow"
Egress: "false"
CidrBlock: "0.0.0.0/0"
PublicOutboundPublicACL:
Type: "AWS::EC2::NetworkAclEntry"
Properties:
NetworkAclId: !Ref PublicNetworkACL
RuleNumber: "100"
Protocol: "-1"
RuleAction: "allow"
Egress: "true"
CidrBlock: "0.0.0.0/0"
######## Associate Public Subnet to Network ACL #######
PublicSubnetOneNetworkAclAssociation:
Type: "AWS::EC2::SubnetNetworkAclAssociation"
Properties:
SubnetId: !Ref "PublicSubnetOne"
NetworkAclId: !Ref "PublicNetworkACL"
PublicSubnetTwoNetworkAclAssociation:
Type: "AWS::EC2::SubnetNetworkAclAssociation"
Properties:
SubnetId: !Ref "PublicSubnetTwo"
NetworkAclId: !Ref "PublicNetworkACL"
############################################################
# ECS CLUSTER BLOCK
############################################################
Cluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Join ["-", [!Ref ServiceName, cluster]]
############################################################
# ECS TASK DEFINITION
############################################################
TaskDefinition:
Type: AWS::ECS::TaskDefinition
DependsOn: LogGroup
Properties:
# Name of the task definition. Subsequent versions of the task definition are grouped together under this name.
Family: !Join ["-", [!Ref ServiceName, TaskDefinition]]
# awsvpc is required for Fargate
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
Cpu: !Ref ContainerCpu
Memory: !Ref ContainerMemory
ExecutionRoleArn: !Ref ExecutionRole
TaskRoleArn: !Ref TaskRole
ContainerDefinitions:
- Name: !Ref ServiceName
Image:
!Join [
".",
[
!Ref AWS::AccountId,
"dkr.ecr",
!Ref AWS::Region,
!Sub "amazonaws.com/ecr-repository:${ImageTag}",
],
]
PortMappings:
- ContainerPort: !Ref ContainerPort
# Send logs to CloudWatch Logs
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-region: !Ref AWS::Region
awslogs-group: !Ref LogGroup
awslogs-stream-prefix: ecs
############################################################
# IAM ROLE
############################################################
ExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join ["-", [!Ref ServiceName, ExecutionRole]]
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
TaskRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join ["-", [!Ref ServiceName, TaskRole]]
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
############################################################
# SECURITY GROUP
############################################################
ContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Join ["-", [!Ref ServiceName, ContainerSecurityGroup]]
VpcId: !Ref RestVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref ContainerPort
ToPort: !Ref ContainerPort
SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
LoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription:
!Join ["-", [!Ref ServiceName, LoadBalancerSecurityGroup]]
VpcId: !Ref RestVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
############################################################
# ECS SERVICE
############################################################
Service:
Type: AWS::ECS::Service
# This dependency is needed so that the load balancer is setup correctly in time
DependsOn:
- ApplicationLoadBalancer
- ALBHTTPListener
- ListenerRule
- TargetGroup
Properties:
ServiceName: !Ref ServiceName
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
DeploymentConfiguration:
MinimumHealthyPercent: 100
MaximumPercent: 200
DesiredCount: !Ref MinContainers
HealthCheckGracePeriodSeconds: 300
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets:
- !Ref PublicSubnetOne
- !Ref PublicSubnetTwo
SecurityGroups:
- !Ref ContainerSecurityGroup
LoadBalancers:
- ContainerName: !Ref ServiceName
ContainerPort: !Ref ContainerPort
TargetGroupArn: !Ref TargetGroup
############################################################
# ECS TARGET GROUP
############################################################
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 10
HealthCheckPath: !Ref HealthCheckPath
HealthCheckTimeoutSeconds: 5
UnhealthyThresholdCount: 2
HealthyThresholdCount: 2
Name: !Join ["-", [!Ref ServiceName, TargetGroup]]
Port: !Ref ContainerPort
Protocol: HTTP
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60
TargetType: ip
VpcId: !Ref RestVPC
Matcher:
HttpCode: 200
############################################################
# APPLICATION LOAD BALANCER
############################################################
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: 60
Name: !Join ["-", [!Ref ServiceName, ApplicationLoadBalancer]]
Scheme: internet-facing
SecurityGroups:
- !Ref LoadBalancerSecurityGroup
Subnets:
- !Ref PublicSubnetOne
- !Ref PublicSubnetTwo
ALBHTTPListener:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
DefaultActions:
- FixedResponseConfig:
StatusCode: 200
Type: fixed-response
Port: "80"
Protocol: HTTP
# ---- Applcation Load Balancer Listener Rule ---- #
ListenerRule:
Type: "AWS::ElasticLoadBalancingV2::ListenerRule"
Properties:
Actions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
Conditions:
- Field: path-pattern
Values:
- "*"
ListenerArn: !Ref ALBHTTPListener
Priority: 1
############################################################
# CLOUDWATCH LOG GROUPS
############################################################
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Join ["", [/ecs/, !Ref ServiceName, TaskDefinition]]
I named the template ecs.yaml
. You can deploy it by running:
aws cloudformation deploy --template-file ecs.yml --stack-name ecs-infrastructure --capabilities CAPABILITY_NAMED_IAM --profile default
You can navigate to the ECS console and confirm your cluster was created successfully.
You can also open the load balancer address to see if the container works as expected.
Cleanup
Always remember to delete resources you do not need, run:
aws cloudformation delete-stack --stack-name ecs-infrastructure
You can check the status of the stack to confirm if it was deleted successfully:
aws cloudformation delete-stack --stack-name ecs-infrastructure
Confirm the stack was deleted successfully; remember to clean up the ECR repository if you implemented that.
Posted on July 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.