AWS for DevOps — Project 2: ECS Fargate with CI/CD

6 minute read

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_SPOT with 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

Next → Project 3: Serverless REST API

Updated: