In this article, we will learn about how to automate the deployment of a simple game 2048 application in a containerized environment using AWS ECS combined with AWS CloudFormation for Infrastructure-as-code template.
Why This Architecture?
AWS ECS: Managed container orchestration for scalable deployments in AWS environment.
CloudFormation: Infrastructure-as-Code (IaC) for repeatable, version-controlled setups.
Automation: By combining the use of AWS CloudFormation and ECS, we can automate the entire process of launching resources and deploying services.
Cost Efficiency: Pay only for resources used (ALB, ECS tasks, ECR storage).
Prerequisites
An active AWS account with CLI or GUI access.
A S3 bucket to store your template files.
Docker installed locally (if you want to test in local first)
Basic knowledge of YAML and containers.
Architecture Diagram
Below is high-level overview diagram of architecture which we will deploy on this blog post:
Step-by-step Implementation
In this stage, we will perform a step-by-step implementation guide and walk you through the process of creating YAML files for CloudFormation and actually deploying an open-source docker image for 2048 game application.
Step 0: Create a S3 bucket to store your child templates
Before doing anything else, we will first create a s3 bucket to store our CloudFormation templates that will later be referenced from the parent stack in a nested stack fashion.
I will just go into AWS console and create s3 bucket like this:
After creating S3 bucket for storage, we will move onto creating CF template files that we will use in this tutorial.
Step 1: Create a CloudFormation YAML template for based resources like VPC, ECS cluster and ECR repo
First thing first, we will create a stack named base.yaml
with following content for creating necessary base resources like:
a VPC with two public subnets, two private subnets and a NAT Gateway for internet connection from private subnets.
an ECS cluster with
FARGATE
andFARGATE_SPOT
capacity provider for serverless computing.a private ECR repository for storing application container image with proper lifecycle policy defined.
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
CloudFormation template for VPC with public/private subnets, ECS Cluster, and ECR Repository
Parameters:
VpcCidr:
Description: CIDR block for the VPC
Type: String
PublicSubnet1Cidr:
Description: CIDR block for Public Subnet 1
Type: String
PublicSubnet2Cidr:
Description: CIDR block for Public Subnet 2
Type: String
PrivateSubnet1Cidr:
Description: CIDR block for Private Subnet 1
Type: String
PrivateSubnet2Cidr:
Description: CIDR block for Private Subnet 2
Type: String
ClusterName:
Description: Name for the ECS Cluster
Type: String
ECRRepoName:
Description: Name for the ECR Repository
Type: String
Resources:
# VPC Configuration
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-vpc
# Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-igw
GatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref Vpc
InternetGatewayId: !Ref InternetGateway
# Public Subnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: !Ref PublicSubnet1Cidr
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-subnet-1
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: !Ref PublicSubnet2Cidr
AvailabilityZone: !Select [1, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-subnet-2
# Private Subnets
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: !Ref PrivateSubnet1Cidr
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-subnet-1
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: !Ref PrivateSubnet2Cidr
AvailabilityZone: !Select [1, !GetAZs ""]
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-subnet-2
# NAT Gateway
NatGatewayEIP:
Type: AWS::EC2::EIP
DependsOn: GatewayAttachment
Properties:
Domain: vpc
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGatewayEIP.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-nat-gateway
# Route Tables
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-rt
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-rt
PublicRoute:
Type: AWS::EC2::Route
DependsOn: GatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
# Route Table Associations
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
# ECS Cluster
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Ref ClusterName
CapacityProviders:
- FARGATE
- FARGATE_SPOT
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE
Weight: 1
ClusterSettings:
- Name: containerInsights
Value: enabled
# ECR Repository
ECRRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Ref ECRRepoName
LifecyclePolicy:
LifecyclePolicyText: |
{
"rules": [
{
"rulePriority": 1,
"description": "Keep only 5 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 5
},
"action": {
"type": "expire"
}
}
]
}
ImageScanningConfiguration:
ScanOnPush: true
Outputs:
VpcId:
Description: VPC ID
Value: !Ref Vpc
Export:
Name: !Sub ${AWS::StackName}-VpcId
PublicSubnet1Id:
Description: Public Subnet 1 ID
Value: !Ref PublicSubnet1
Export:
Name: !Sub ${AWS::StackName}-PublicSubnet1Id
PublicSubnet2Id:
Description: Public Subnet 2 ID
Value: !Ref PublicSubnet2
Export:
Name: !Sub ${AWS::StackName}-PublicSubnet2Id
PrivateSubnet1Id:
Description: Private Subnet 1 ID
Value: !Ref PrivateSubnet1
Export:
Name: !Sub ${AWS::StackName}-PrivateSubnet1Id
PrivateSubnet2Id:
Description: Private Subnet 2 ID
Value: !Ref PrivateSubnet2
Export:
Name: !Sub ${AWS::StackName}-PrivateSubnet2Id
ECSClusterName:
Description: ECS Cluster Name
Value: !Ref ECSCluster
Export:
Name: !Sub ${AWS::StackName}-ECSClusterName
ECRRepositoryName:
Description: ECR Repository Name
Value: !Ref ECRRepository
Export:
Name: !Sub ${AWS::StackName}-ECRRepositoryName
Copy above template file to your created s3 bucket in Step0 using command like:
aws s3 cp --region us-east-1 base.yaml s3://2048-game-templates-bucket
Next, we need to pass parameter values defined in the stack like VpcCidr, ClusterName, ECRRepoName and Subnet CIDRs. For that, we will create a vpc-parameters.json
file with values:
[
{
"ParameterKey": "VpcCidr",
"ParameterValue": "10.0.0.0/16"
},
{
"ParameterKey": "PublicSubnet1Cidr",
"ParameterValue": "10.0.1.0/24"
},
{
"ParameterKey": "PublicSubnet2Cidr",
"ParameterValue": "10.0.2.0/24"
},
{
"ParameterKey": "PrivateSubnet1Cidr",
"ParameterValue": "10.0.3.0/24"
},
{
"ParameterKey": "PrivateSubnet2Cidr",
"ParameterValue": "10.0.4.0/24"
},
{
"ParameterKey": "ClusterName",
"ParameterValue": "test-ecs-cluster"
},
{
"ParameterKey": "ECRRepoName",
"ParameterValue": "app-repository"
}
]
After defining all the above steps, we can create our base CloudFormation stack from the command line using:
aws cloudformation create-stack --region us-east-1 --stack-name game-2048 --template-url https://2048-game-templates-bucket.s3.us-east-1.amazonaws.com/base.yaml --parameters file://vpc-parameters.json
Step 2: Preparing container image to be used with the service stack
After the base stack with VPC configuration, ECS cluster and ECR repository is created, we will have a private ECR repository with given name and lifecycle policy of keeping 5 versions of our application image. Let’s prepare and push the image we will use in this example to our private repository.
- First, download and pull the image from public ECR repository that hosts the 2048 game using command:
docker pull public.ecr.aws/kishorj/docker-2048:latest
- Login to the private repository we created using command: (You can also get the login command from AWS ECR repository)
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 757641753030.dkr.ecr.us-east-1.amazonaws.com
- Tag the image with our private repository name and push to the repository with
latest
tag:
docker tag public.ecr.aws/kishorj/docker-2048:latest 757641753030.dkr.ecr.us-east-1.amazonaws.com/app-repository:latest
docker push 757641753030.dkr.ecr.us-east-1.amazonaws.com/app-repository:latest
Step 3: Create a CloudFormation template for ECS service and ALB
The next step is to create another CF template for creating ECS service and its dependencies such as TaskRole, Execution Role, Auto Scaling Policies and Application Load Balancer (ALB) for exposing traffic to users.
Create a service.yaml
file with following content:
AWSTemplateFormatVersion: 2010-09-09
Description: >
This template shows how to create Amazon Elastic Container Service
(Amazon ECS) using clusters powered by AWS Fargate with CloudFormation.
Stack Name will be used as S3 artifacts bucket name and fargate service name.
Metadata:
"AWS::CloudFormation::Interface":
ParameterGroups:
- Label:
default: "Common Parameters"
Parameters:
- Environment
- Label:
default: "VPC Parameters"
Parameters:
- VPC
- AppSubnetA
- AppSubnetB
- Label:
default: "Container Parameters"
Parameters:
- ClusterName
- ContainerPort
- FargateCPU
- FargateMemory
- ContainerRegistery
- LoadBalancerPort
- Label:
default: "LoadBalancer Parameters"
Parameters:
- LoadBalancerType
- LBSubnetA
- LBSubnetB
- HealthCheckPath
- MinContainers
- MaxContainers
- SSLCertificateARN
- HttpPort
- IsEnableHTTPS
- Label:
default: "AutoScaling Parameters"
Parameters:
- TargetCPUUtilization
- TargetMemoryUtilization
ParameterLabels:
Environment:
default: "A tag is a label as well as Environment that you or AWS assigns to an AWS resource. You can use tags to organize your resources, and cost allocation tags to track your AWS costs on a detailed level."
VPC:
default: "Which VPC should this be deployed to?"
Parameters:
VPC:
Type: AWS::EC2::VPC::Id
AppSubnetA:
Type: AWS::EC2::Subnet::Id
AppSubnetB:
Type: AWS::EC2::Subnet::Id
LBSubnetA:
Type: AWS::EC2::Subnet::Id
LBSubnetB:
Type: AWS::EC2::Subnet::Id
ClusterName:
Type: String
FargateCPU:
Type: String
Default: 512
FargateMemory:
Type: String
Default: 1GB
ContainerPort:
Type: Number
Default: 80
LoadBalancerPort:
Type: Number
Default: 443
HttpPort:
Type: Number
Default: 80
HealthCheckPath:
Type: String
Default: /
MinContainers:
Type: Number
Default: 1
MaxContainers:
Type: Number
Default: 10
ContainerRegistery:
Type: String
Description: ECR Repo Name + tag name
SSLCertificateARN:
Type: String
Default: arn:aws:acm:us-east-1:757641753030:certificate/b7c279cd-8658-4a08-8155-e1eb9786aa7b
Environment:
Type: String
Default: testing
Description: Environment name to be used with service.
AllowedPattern: '[a-zA-Z0-9\-_]+'
ConstraintDescription: Tag name should be alpha numberic letter
LoadBalancerType:
Type: String
Default: internet-facing
AllowedValues: [internet-facing, internal]
IsEnableHTTPS:
Type: String
Default: "true"
AllowedValues:
- "true"
- "false"
Description: To disable TLS, it's false. If not, choose true.
TargetCPUUtilization:
Type: Number
Default: 70
TargetMemoryUtilization:
Type: Number
Default: 70
ServiceName:
Type: String
Description: The name of the service to be created.
Conditions:
ShouldCreateHTTPS: !Equals [!Ref IsEnableHTTPS, "true"]
Resources:
PipelineBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Join ["-", [!Ref Environment, !Ref AWS::StackName]]
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, !Ref AWS::StackName]]
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Join ["", [!Ref AWS::StackName, TaskDefinition]]
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
# 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB
# 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB
# 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB
# 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments
# 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments
Cpu: !Ref FargateCPU
# 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU)
# 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU)
# 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU)
# Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU)
# Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU)
Memory: !Ref FargateMemory
# A role needed by ECS.
# "The ARN of the task execution role that containers in this task can assume. All containers in this task are granted the permissions that are specified in this role."
# "There is an optional task execution IAM role that you can specify with Fargate to allow your Fargate tasks to make API calls to Amazon ECR."
ExecutionRoleArn: !Ref ExecutionRole
TaskRoleArn: !Ref TaskRole
ContainerDefinitions:
- Name: !Ref AWS::StackName
Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ContainerRegistery}
PortMappings:
- ContainerPort: !Ref ContainerPort
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: fargate
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, TaskDefinition]]
ExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join ["-", [!Ref AWS::StackName, ExecutionRole]]
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, ExecutionRole]]
TaskRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join ["-", [!Ref AWS::StackName, TaskRole]]
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, TaskRole]]
AutoScalingRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join ["-", [!Ref AWS::StackName, AutoScalingRole]]
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole"
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, AutoScalingRole]]
ContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription:
!Join ["", [!Ref AWS::StackName, ContainerSecurityGroup]]
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref ContainerPort
ToPort: !Ref ContainerPort
SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, ContainerSecurityGroup]]
LoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription:
!Join ["", [!Ref AWS::StackName, LoadBalancerSecurityGroup]]
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref LoadBalancerPort
ToPort: !Ref LoadBalancerPort
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: !Ref HttpPort
ToPort: !Ref HttpPort
CidrIp: 0.0.0.0/0
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, lb-sg]]
Service:
DependsOn:
- ListenerHTTP
Type: AWS::ECS::Service
Properties:
ServiceName: !Ref AWS::StackName
Cluster: !Ref ClusterName
TaskDefinition: !Ref TaskDefinition
DeploymentConfiguration:
MinimumHealthyPercent: 100
MaximumPercent: 200
DesiredCount: !Ref MinContainers
# This may need to be adjusted if the container takes a while to start up
HealthCheckGracePeriodSeconds: 300
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
# change to DISABLED if you're using private subnets that have access to a NAT gateway
AssignPublicIp: DISABLED
Subnets:
- !Ref AppSubnetA
- !Ref AppSubnetB
SecurityGroups:
- !Ref ContainerSecurityGroup
LoadBalancers:
- ContainerName: !Ref AWS::StackName
ContainerPort: !Ref ContainerPort
TargetGroupArn: !Ref TargetGroup
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, svc]]
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 10
# will look for a 200 status code by default unless specified otherwise
HealthCheckPath: !Ref HealthCheckPath
HealthCheckTimeoutSeconds: 5
UnhealthyThresholdCount: 2
HealthyThresholdCount: 2
Name: !Join ["-", [!Ref AWS::StackName, tg]]
Port: !Ref ContainerPort
Protocol: HTTP
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60 # default is 300
TargetType: ip
VpcId: !Ref VPC
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, tg]]
ListenerHTTPS:
Condition: ShouldCreateHTTPS
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
LoadBalancerArn: !Ref LoadBalancer
Port: !Ref LoadBalancerPort
Protocol: HTTPS
Certificates:
- CertificateArn: !Ref SSLCertificateARN
ListenerHTTP:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- !If
- ShouldCreateHTTPS
- Type: redirect
RedirectConfig:
StatusCode: "HTTP_301"
Host: "#{host}"
Path: "/#{path}"
Port: 443
Protocol: "HTTPS"
Query: "#{query}"
- TargetGroupArn: !Ref TargetGroup
Type: forward
LoadBalancerArn: !Ref LoadBalancer
Port: !Ref HttpPort
Protocol: HTTP
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
LoadBalancerAttributes:
# this is the default, but is specified here in case it needs to be changed
- Key: idle_timeout.timeout_seconds
Value: 60
- Key: "routing.http2.enabled"
Value: "true"
Name: !Join ["-", [!Ref AWS::StackName, lb]]
# "internal" is also an option
Scheme: !Ref LoadBalancerType
SecurityGroups:
- !Ref LoadBalancerSecurityGroup
Subnets:
- !Ref LBSubnetA
- !Ref LBSubnetB
Tags:
- Key: "Environment"
Value: !Ref Environment
- Key: "Name"
Value: !Join ["-", [!Ref Environment, lb]]
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Join ["", [/fargate/, !Ref AWS::StackName, Logs]]
RetentionInDays: 60
AutoScalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MinCapacity: !Ref MinContainers
MaxCapacity: !Ref MaxContainers
ResourceId: !Join ["/", [service, !Ref ClusterName, !GetAtt Service.Name]]
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
# "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that allows Application Auto Scaling to modify your scalable target."
RoleARN: !GetAtt AutoScalingRole.Arn
# Create scaling policies that describe how to scale the service up and down.
CPUScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
DependsOn: AutoScalingTarget
Properties:
PolicyName: !Sub "${ServiceName}-cpu-target-tracking-policy"
PolicyType: TargetTrackingScaling
ResourceId: !Join ["/", [service, !Ref ClusterName, !GetAtt Service.Name]]
ScalableDimension: "ecs:service:DesiredCount"
ServiceNamespace: "ecs"
TargetTrackingScalingPolicyConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilization
TargetValue: !Ref TargetCPUUtilization
ScaleInCooldown: 60
ScaleOutCooldown: 60
MemoryScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
DependsOn: AutoScalingTarget
Properties:
PolicyName: !Sub "${ServiceName}-memory-target-tracking-policy"
PolicyType: TargetTrackingScaling
ResourceId: !Join ["/", [service, !Ref ClusterName, !GetAtt Service.Name]]
ScalableDimension: "ecs:service:DesiredCount"
ServiceNamespace: "ecs"
TargetTrackingScalingPolicyConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageMemoryUtilization
TargetValue: !Ref TargetMemoryUtilization
ScaleInCooldown: 60
ScaleOutCooldown: 60
Outputs:
ServiceURL:
Description: URL of the load balancer
Value: !Sub http://${LoadBalancer.DNSName}
Export:
Name: !Sub ${AWS::StackName}-ServiceURL
Let me upload above template into my S3 bucket created in the step0 using command*:*
aws s3 cp --region us-east-1 service.yaml s3://2048-game-templates-bucket
This stack includes parameter variables such as VPC, Subnets for application tasks containers, Subnets for LoadBalancer and Container Registry. Other variables such as CPU, Memory configuration and Auto Scaling target percentages are optional. (already set default values for those)
So, we would need to note down the Outputs section of the first stack regarding VPC ID, Public Subnet IDs for LoadBalancer and Private Subnet IDs for ECS service.
Then, we will create a service-parameters.json
file with appropriate values for subnets, VPC and ContainerRegistry.
[
{
"ParameterKey": "ClusterName",
"ParameterValue": "test-ecs-cluster"
},
{
"ParameterKey": "ContainerPort",
"ParameterValue": "80"
},
{
"ParameterKey": "ContainerRegistery",
"ParameterValue": "app-repository:latest"
},
{
"ParameterKey": "Environment",
"ParameterValue": "dev"
},
{
"ParameterKey": "ServiceName",
"ParameterValue": "2048-ecs"
},
{
"ParameterKey": "VPC",
"ParameterValue": ""
},
{
"ParameterKey": "AppSubnetA",
"ParameterValue": ""
},
{
"ParameterKey": "AppSubnetB",
"ParameterValue": ""
},
{
"ParameterKey": "LBSubnetA",
"ParameterValue": ""
},
{
"ParameterKey": "LBSubnetB",
"ParameterValue": ""
}
]
Please Don’t forget to substitute the ParameterValue section of above file with outputs from the base stack.
And we can create the service stack with following command:
aws cloudformation create-stack --region us-east-1 --stack-name game-2048-svc --template-url https://2048-game-templates-bucket.s3.us-east-1.amazonaws.com/service.yaml --parameters file://service-parameters.json --capabilities CAPABILITY_NAMED_IAM
Please note to replace the —template-url with your s3 file URL uploaded and relative path of your service-parameters.json file
After the stack is successfully created, you will get the Domain URL endpoint of LoadBalancer in the Outputs section of the template.
Step 4: Domain Record Creation Step
In this example, I’ve used my custom domain URL and certificate from AWS Certificate Manager (ACM) as default value in SSLCertificateARN
parameter value. If you don’t have custom domain available, you can just set IsEnableHTTPS
parameter value to false
and the template will only create HTTP listener.
To create a CNAME
record for your custom domain, go into your DNS provider and create a CNAME
type record with custom domain name and value is set to the ServiceURL
output returned from above step.
In my case, I use CloudFlare as my DNS provider, so I logged into my CloudFlare account and create a simple DNS record as follows:
Step 5: Verify the application is working
Finally, you can verify your application is up and running by going to your domain URL from browser:
Congratulations! If you see that page, that means you have successfully deployed a 2048 game in ECS with CloudFormation as IaC tool.
Key Takeaways
Infrastructure-as-Code: CloudFormation ensures reproducibility and versioning of your infrastruture.
Scalability: ECS Fargate handles load without managing servers. (Serverless computing)
Cost Control: Tear down and bring up resources easily whenever you need, wherever you need.
Troubleshooting Tips
Task Failures: Check ECS task logs in CloudWatch for any failures of tasks and not in Active status.
ALB Issues: Verify security group inbound rules are set correctly if you face timeouts when accessing the endpoint URL.
Image Pull Errors: Ensure ECR repository permissions are correct if you get any permissions error when starting ECS tasks.
Code Reference
All the code we used in this tutorial blog can be found here: https://github.com/Heinux-Training/Real-World-AWS-Case-Studies/tree/main/CloudFormation/2048-ECS-ALB
NOTE: I’ve used Generative AI assistant like Amazon Q as VS Code extension and produced some of the codes used in this blog.
Conclusion
Deploying the 2048 game on AWS ECS with CloudFormation demonstrates the power of Infrastructure-as-Code (IaC) and managed container services to streamline application deployment. By automating infrastructure provisioning, we ensure consistency, reduce human error, and enable rapid scaling—principles critical to modern DevOps practices. This tutorial highlights how tools like ECS Fargate eliminate server management overhead, while CloudFormation templates provide reusable, version-controlled blueprints for environments. Beyond the technical steps, this project teaches the value of modular design (decoupling infrastructure from code), cost optimization (leveraging serverless compute), and resilience (via ALB and multi-task deployments). These skills translate directly to real-world scenarios, empowering teams to deploy and iterate on applications faster.
Top comments (0)