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)