AWS for DevOps — Project 2: ECS Fargate with CI/CD
Project Overview
Build a containerized application running on ECS Fargate with a full CI/CD pipeline — push code to a repository, automatically build a Docker image, push it to ECR, and deploy to ECS behind an Application Load Balancer.
Real-World Analogy: Think of it like a factory assembly line — code changes go in one end (CodeCommit), get built and tested (CodeBuild), packaged into containers (ECR), and deployed to production (ECS Fargate) — all automatically.
| Detail | Value |
|---|---|
| Difficulty | ⭐⭐⭐ (Advanced) |
| Time | 2-4 hours |
| Cost | ~$1-2 (Fargate + ALB for a few hours) |
| Services | ECS, Fargate, ECR, CodeCommit, CodeBuild, CodePipeline, ALB |
Architecture
┌──────────────────────────────────────────────────────────────┐
│ CI/CD Pipeline │
│ │
│ CodeCommit ──→ CodeBuild ──→ ECR ──→ CodeDeploy ──→ ECS │
│ (Source) (Build) (Store) (Deploy) (Run) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ VPC (from Project 1) │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ ALB │───→│ ECS Fargate Service │ │ │
│ │ │ (public)│ │ ┌────────┐ ┌────────┐ │ │ │
│ │ └─────────┘ │ │ Task 1 │ │ Task 2 │ │ │ │
│ │ │ └────────┘ └────────┘ │ │ │
│ │ │ (private subnets) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Step 1 — Create the Application
Create a simple Node.js application:
# Create project structure
$ mkdir -p ecs-fargate-app && cd ecs-fargate-app
app.js:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
version: process.env.APP_VERSION || '1.0.0',
timestamp: new Date().toISOString()
}));
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY app.js .
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:3000/ || exit 1
CMD ["node", "app.js"]
Step 2 — Create ECR Repository
# Create ECR repository
$ aws ecr create-repository \
--repository-name ecs-fargate-app \
--image-scanning-configuration scanOnPush=true \
--tags Key=Project,Value=practice-2
{
"repository": {
"repositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/ecs-fargate-app",
"repositoryName": "ecs-fargate-app"
}
}
# Log in to ECR
$ aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789012.dkr.ecr.us-east-1.amazonaws.com
# Build and push initial image
$ docker build -t ecs-fargate-app .
$ docker tag ecs-fargate-app:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/ecs-fargate-app:latest
$ docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/ecs-fargate-app:latest
Step 3 — Create ECS Cluster
# Create ECS cluster
$ aws ecs create-cluster \
--cluster-name prod-cluster \
--capacity-providers FARGATE FARGATE_SPOT \
--default-capacity-provider-strategy \
capacityProvider=FARGATE,weight=1 \
capacityProvider=FARGATE_SPOT,weight=3 \
--tags key=Project,value=practice-2
{
"cluster": {
"clusterName": "prod-cluster",
"status": "ACTIVE"
}
}
Cost Optimization: Using
FARGATE_SPOTwith weight 3 means 75% of tasks run on Spot capacity (up to 70% cheaper). Great for non-critical workloads.
Step 4 — Create Task Definition
# Create task execution role (allows ECS to pull images and write logs)
$ aws iam create-role \
--role-name ecsTaskExecutionRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ecs-tasks.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
$ aws iam attach-role-policy \
--role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
task-definition.json:
{
"family": "ecs-fargate-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "app",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/ecs-fargate-app:latest",
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"environment": [
{"name": "APP_VERSION", "value": "1.0.0"}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/fargate-app",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "app"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "wget -qO- http://localhost:3000/ || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3
}
}
]
}
# Create CloudWatch log group
$ aws logs create-log-group --log-group-name /ecs/fargate-app
# Register task definition
$ aws ecs register-task-definition --cli-input-json file://task-definition.json
{
"taskDefinition": {
"taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/ecs-fargate-app:1"
}
}
Step 5 — Create ALB and Target Group
# Create Application Load Balancer
$ aws elbv2 create-load-balancer \
--name prod-alb \
--subnets subnet-0pub1a subnet-0pub1b \
--security-groups sg-0alb \
--tags Key=Project,Value=practice-2
{
"LoadBalancers": [{
"LoadBalancerArn": "arn:aws:elasticloadbalancing:...:loadbalancer/app/prod-alb/abc123",
"DNSName": "prod-alb-123456.us-east-1.elb.amazonaws.com"
}]
}
# Create target group (IP type for Fargate)
$ aws elbv2 create-target-group \
--name fargate-app-tg \
--protocol HTTP \
--port 3000 \
--vpc-id vpc-0abc123def456789 \
--target-type ip \
--health-check-path "/" \
--health-check-interval-seconds 30 \
--healthy-threshold-count 2
{
"TargetGroups": [{
"TargetGroupArn": "arn:aws:elasticloadbalancing:...:targetgroup/fargate-app-tg/def456"
}]
}
# Create listener
$ aws elbv2 create-listener \
--load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/prod-alb/abc123 \
--protocol HTTP \
--port 80 \
--default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/fargate-app-tg/def456
Step 6 — Create ECS Service
# Create ECS service
$ aws ecs create-service \
--cluster prod-cluster \
--service-name fargate-app \
--task-definition ecs-fargate-app:1 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration '{
"awsvpcConfiguration": {
"subnets": ["subnet-0priv1a", "subnet-0priv1b"],
"securityGroups": ["sg-0app"],
"assignPublicIp": "DISABLED"
}
}' \
--load-balancers '[{
"targetGroupArn": "arn:aws:elasticloadbalancing:...:targetgroup/fargate-app-tg/def456",
"containerName": "app",
"containerPort": 3000
}]' \
--deployment-configuration '{
"maximumPercent": 200,
"minimumHealthyPercent": 100
}' \
--tags key=Project,value=practice-2
# Verify service is running
$ aws ecs describe-services \
--cluster prod-cluster \
--services fargate-app \
--query 'services[0].{Status:status,Running:runningCount,Desired:desiredCount}'
{
"Status": "ACTIVE",
"Running": 2,
"Desired": 2
}
# Test the application
$ curl http://prod-alb-123456.us-east-1.elb.amazonaws.com/
{
"status": "healthy",
"version": "1.0.0",
"timestamp": "2026-03-30T06:00:00.000Z"
}
Step 7 — Set Up CI/CD Pipeline
Create CodeCommit Repository
$ aws codecommit create-repository \
--repository-name ecs-fargate-app \
--repository-description "ECS Fargate Application" \
--tags Project=practice-2
Create buildspec.yml
buildspec.yml:
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region $AWS_DEFAULT_REGION |
docker login --username AWS --password-stdin
$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
build:
commands:
- echo Building Docker image...
- docker build -t $REPOSITORY_URI:$IMAGE_TAG .
- docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:latest
post_build:
commands:
- echo Pushing Docker image...
- docker push $REPOSITORY_URI:$IMAGE_TAG
- docker push $REPOSITORY_URI:latest
- echo Writing image definitions file...
- printf '[{"name":"app","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG
> imagedefinitions.json
artifacts:
files:
- imagedefinitions.json
Create CodeBuild Project
$ aws codebuild create-project \
--name ecs-fargate-app-build \
--source type=CODECOMMIT,location=https://git-codecommit.us-east-1.amazonaws.com/v1/repos/ecs-fargate-app \
--environment type=LINUX_CONTAINER,computeType=BUILD_GENERAL1_SMALL,image=aws/codebuild/amazonlinux2-x86_64-standard:4.0,privilegedMode=true,environmentVariables="[{name=AWS_ACCOUNT_ID,value=123456789012},{name=AWS_DEFAULT_REGION,value=us-east-1},{name=REPOSITORY_URI,value=123456789012.dkr.ecr.us-east-1.amazonaws.com/ecs-fargate-app}]" \
--artifacts type=NO_ARTIFACTS \
--service-role arn:aws:iam::123456789012:role/codebuild-service-role
Create CodePipeline
$ aws codepipeline create-pipeline --cli-input-json '{
"pipeline": {
"name": "ecs-fargate-pipeline",
"roleArn": "arn:aws:iam::123456789012:role/codepipeline-service-role",
"stages": [
{
"name": "Source",
"actions": [{
"name": "Source",
"actionTypeId": {
"category": "Source",
"owner": "AWS",
"provider": "CodeCommit",
"version": "1"
},
"configuration": {
"RepositoryName": "ecs-fargate-app",
"BranchName": "main"
},
"outputArtifacts": [{"name": "SourceOutput"}]
}]
},
{
"name": "Build",
"actions": [{
"name": "Build",
"actionTypeId": {
"category": "Build",
"owner": "AWS",
"provider": "CodeBuild",
"version": "1"
},
"configuration": {
"ProjectName": "ecs-fargate-app-build"
},
"inputArtifacts": [{"name": "SourceOutput"}],
"outputArtifacts": [{"name": "BuildOutput"}]
}]
},
{
"name": "Deploy",
"actions": [{
"name": "Deploy",
"actionTypeId": {
"category": "Deploy",
"owner": "AWS",
"provider": "ECS",
"version": "1"
},
"configuration": {
"ClusterName": "prod-cluster",
"ServiceName": "fargate-app"
},
"inputArtifacts": [{"name": "BuildOutput"}]
}]
}
]
}
}'
Pipeline Flow
git push → CodeCommit → CodeBuild (docker build) → ECR → ECS Deploy
│ │
└── Triggers automatically ──────────────────→ │
Rolling update
(zero downtime)
Step 8 — Test the Pipeline
# Push code to trigger the pipeline
$ cd ecs-fargate-app
$ git init
$ git remote add origin https://git-codecommit.us-east-1.amazonaws.com/v1/repos/ecs-fargate-app
$ git add .
$ git commit -m "Initial commit"
$ git push -u origin main
# Monitor pipeline execution
$ aws codepipeline get-pipeline-state --name ecs-fargate-pipeline \
--query 'stageStates[].{Stage:stageName,Status:latestExecution.status}'
[
{"Stage": "Source", "Status": "Succeeded"},
{"Stage": "Build", "Status": "Succeeded"},
{"Stage": "Deploy", "Status": "InProgress"}
]
# Wait for deployment, then test
$ curl http://prod-alb-123456.us-east-1.elb.amazonaws.com/
{
"status": "healthy",
"version": "1.0.0"
}
Step 9 — Clean Up
# Delete in reverse order
$ aws codepipeline delete-pipeline --name ecs-fargate-pipeline
$ aws codebuild delete-project --name ecs-fargate-app-build
$ aws codecommit delete-repository --repository-name ecs-fargate-app
$ aws ecs update-service --cluster prod-cluster --service fargate-app --desired-count 0
$ aws ecs delete-service --cluster prod-cluster --service fargate-app --force
$ aws ecs delete-cluster --cluster prod-cluster
$ aws elbv2 delete-listener --listener-arn <listener-arn>
$ aws elbv2 delete-target-group --target-group-arn <tg-arn>
$ aws elbv2 delete-load-balancer --load-balancer-arn <alb-arn>
$ aws ecr delete-repository --repository-name ecs-fargate-app --force
$ aws logs delete-log-group --log-group-name /ecs/fargate-app
What You Built
| Component | Purpose |
|---|---|
| ECR Repository | Container image storage |
| ECS Cluster | Container orchestration |
| Task Definition | Container specification |
| ECS Service | Running containers (2 tasks) |
| ALB + Target Group | Load balancing |
| CodeCommit | Source code repository |
| CodeBuild | Docker image builder |
| CodePipeline | CI/CD orchestration |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
Missing privilegedMode in CodeBuild |
Docker build fails | Set privilegedMode: true |
| Wrong target group type | Tasks fail to register | Use ip type for Fargate |
No assignPublicIp and no NAT |
Tasks can’t pull images | Use NAT or assignPublicIp: ENABLED |
| Missing execution role | Tasks fail to start | Attach AmazonECSTaskExecutionRolePolicy |
| No health check | ALB marks tasks unhealthy | Define health check in task definition |