AWS
advanced
90-120 minutes

ECS Fargate: Serverless Containers

Deploy containers to AWS ECS with Fargate. No servers to manage, automatic scaling, and production-ready infrastructure.

What you'll accomplish

Your application running on ECS Fargate with load balancing and auto-scaling

Prerequisites

  • AWS account with CLI configured
  • Docker installed locally
  • Basic container knowledge

What is ECS Fargate?

ECS (Elastic Container Service) is AWS's container orchestration service. Fargate is a serverless compute engine for containers - you don't manage any servers.

Traditional VPS          Lightsail + Docker       ECS Fargate
┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐
│ You manage:     │      │ You manage:     │      │ You manage:     │
│ - OS updates    │      │ - OS updates    │      │ - Containers    │
│ - Docker        │      │ - Docker        │      │                 │
│ - Scaling       │      │ - Scaling       │      │ AWS manages:    │
│ - Load balancer │      │ - Container     │      │ - Everything    │
│ - Container     │      │                 │      │   else          │
└─────────────────┘      └─────────────────┘      └─────────────────┘

When to Use ECS Fargate

Best for:

  • Production applications needing high availability
  • Auto-scaling based on traffic
  • Teams who don't want to manage servers
  • Microservices architectures

Consider alternatives if:

  • Simple app with steady traffic → Lightsail
  • Need to minimize costs → EC2 instances
  • Want even simpler deployment → App Runner
Note

ECS Fargate costs more than running your own servers, but saves significant operations time. The trade-off is worth it for many teams.

Core Concepts

Before we begin, understand these ECS terms:

ConceptWhat It IsAnalogy
ClusterGroup of services/tasksA data center
Task DefinitionContainer blueprintA Dockerfile on steroids
TaskRunning instance of task definitionA running container
ServiceManages tasks, keeps them runningPM2 or systemd
Container Registry (ECR)Where images are storedDocker Hub, but private

Step 1: Set Up ECR (Container Registry)

First, create a place to store your Docker images.

1.1 Create ECR Repository

1.2 Build and Push Your Image

bash
# Get your AWS account ID
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION="us-east-1"

# Login to ECR
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com

# Build your image
docker build -t myapp .

# Tag for ECR
docker tag myapp:latest $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/myapp:latest

# Push to ECR
docker push $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/myapp:latest
Note

ECR login tokens expire after 12 hours. Re-run the login command if you get authentication errors.

Step 2: Create an ECS Cluster

A cluster is where your containers will run.

Using AWS Console (Visual Method)

  1. Go to ECS Console
  2. Click Create cluster
  3. Enter cluster name: myapp-cluster
  4. Under Infrastructure, select AWS Fargate (serverless)
  5. Click Create

Using AWS CLI

bash
aws ecs create-cluster \
  --cluster-name myapp-cluster \
  --capacity-providers FARGATE FARGATE_SPOT \
  --default-capacity-provider-strategy \
    capacityProvider=FARGATE,weight=1 \
    capacityProvider=FARGATE_SPOT,weight=1
Note

FARGATE_SPOT uses spare AWS capacity at up to 70% discount. Great for non-critical workloads or development environments.

Step 3: Create a Task Definition

The task definition tells ECS how to run your container.

3.1 Create Task Definition File

Create task-definition.json:

json
{
  "family": "myapp",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "myapp",
      "image": "YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/myapp:latest",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 3000,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {
          "name": "NODE_ENV",
          "value": "production"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/myapp",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 60
      }
    }
  ]
}

3.2 Create the IAM Role

ECS needs a role to pull images and write logs:

bash
# Create trust policy
cat > trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

# Create the role
aws iam create-role \
  --role-name ecsTaskExecutionRole \
  --assume-role-policy-document file://trust-policy.json

# Attach the AWS managed policy
aws iam attach-role-policy \
  --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

3.3 Create CloudWatch Log Group

bash
aws logs create-log-group --log-group-name /ecs/myapp --region us-east-1

3.4 Register Task Definition

bash
aws ecs register-task-definition --cli-input-json file://task-definition.json

Step 4: Set Up Networking

ECS Fargate requires a VPC with subnets and security groups.

4.1 Find Your VPC and Subnets

bash
# Get default VPC
aws ec2 describe-vpcs \
  --filters "Name=isDefault,Values=true" \
  --query "Vpcs[0].VpcId" \
  --output text

# Get subnets in the VPC
aws ec2 describe-subnets \
  --filters "Name=vpc-id,Values=YOUR_VPC_ID" \
  --query "Subnets[*].SubnetId" \
  --output text

4.2 Create a Security Group

bash
# Create security group
aws ec2 create-security-group \
  --group-name myapp-ecs-sg \
  --description "Security group for myapp ECS tasks" \
  --vpc-id YOUR_VPC_ID

# Allow inbound traffic on your app port
aws ec2 authorize-security-group-ingress \
  --group-id YOUR_SECURITY_GROUP_ID \
  --protocol tcp \
  --port 3000 \
  --cidr 0.0.0.0/0

# Allow inbound HTTP
aws ec2 authorize-security-group-ingress \
  --group-id YOUR_SECURITY_GROUP_ID \
  --protocol tcp \
  --port 80 \
  --cidr 0.0.0.0/0

# Allow inbound HTTPS
aws ec2 authorize-security-group-ingress \
  --group-id YOUR_SECURITY_GROUP_ID \
  --protocol tcp \
  --port 443 \
  --cidr 0.0.0.0/0

Step 5: Create an Application Load Balancer

For production, use a load balancer to distribute traffic.

5.1 Create the Load Balancer

bash
# Create ALB
aws elbv2 create-load-balancer \
  --name myapp-alb \
  --subnets subnet-xxx subnet-yyy \
  --security-groups YOUR_SECURITY_GROUP_ID \
  --scheme internet-facing \
  --type application

5.2 Create Target Group

bash
aws elbv2 create-target-group \
  --name myapp-tg \
  --protocol HTTP \
  --port 3000 \
  --vpc-id YOUR_VPC_ID \
  --target-type ip \
  --health-check-path /api/health \
  --health-check-interval-seconds 30

5.3 Create Listener

bash
aws elbv2 create-listener \
  --load-balancer-arn YOUR_ALB_ARN \
  --protocol HTTP \
  --port 80 \
  --default-actions Type=forward,TargetGroupArn=YOUR_TARGET_GROUP_ARN

Step 6: Create an ECS Service

Now tie everything together with a service:

bash
aws ecs create-service \
  --cluster myapp-cluster \
  --service-name myapp-service \
  --task-definition myapp \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-xxx,subnet-yyy],securityGroups=[YOUR_SECURITY_GROUP_ID],assignPublicIp=ENABLED}" \
  --load-balancers "targetGroupArn=YOUR_TARGET_GROUP_ARN,containerName=myapp,containerPort=3000"
Note

desired-count: 2 runs two copies for high availability. Even if one crashes, your app stays up.

Step 7: Verify Deployment

Check Service Status

bash
# View service
aws ecs describe-services \
  --cluster myapp-cluster \
  --services myapp-service

# View running tasks
aws ecs list-tasks --cluster myapp-cluster --service-name myapp-service

Check Load Balancer

bash
# Get ALB DNS name
aws elbv2 describe-load-balancers \
  --names myapp-alb \
  --query "LoadBalancers[0].DNSName" \
  --output text

# Test your app
curl http://myapp-alb-xxxxx.us-east-1.elb.amazonaws.com/api/health

View Logs

bash
# Via CLI
aws logs tail /ecs/myapp --follow

# Or in console: CloudWatch → Log groups → /ecs/myapp

Step 8: Configure Auto Scaling

Scale automatically based on CPU or request count.

8.1 Register Scalable Target

bash
aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/myapp-cluster/myapp-service \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 1 \
  --max-capacity 10

8.2 Create Scaling Policy

bash
# Scale based on CPU
aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/myapp-cluster/myapp-service \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name myapp-cpu-scaling \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 70.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
    },
    "ScaleOutCooldown": 60,
    "ScaleInCooldown": 60
  }'

This keeps CPU around 70% by adding or removing tasks.

Step 9: Deploy Updates

9.1 Update Script

Create deploy-ecs.sh:

bash
#!/bin/bash
set -e

CLUSTER="myapp-cluster"
SERVICE="myapp-service"
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION="us-east-1"
IMAGE_TAG=${1:-latest}

echo "Building and pushing image..."
docker build -t myapp .
docker tag myapp:latest $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/myapp:$IMAGE_TAG

aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
docker push $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/myapp:$IMAGE_TAG

echo "Updating ECS service..."
aws ecs update-service \
  --cluster $CLUSTER \
  --service $SERVICE \
  --force-new-deployment

echo "Waiting for deployment to complete..."
aws ecs wait services-stable --cluster $CLUSTER --services $SERVICE

echo "Deployment complete!"

9.2 Zero-Downtime Deployments

ECS handles rolling updates automatically:

  1. Starts new tasks with the new image
  2. Waits for health checks to pass
  3. Drains connections from old tasks
  4. Stops old tasks

Configure deployment settings in your service:

bash
aws ecs update-service \
  --cluster myapp-cluster \
  --service myapp-service \
  --deployment-configuration "minimumHealthyPercent=50,maximumPercent=200"

Step 10: Add HTTPS with ACM

10.1 Request Certificate

bash
aws acm request-certificate \
  --domain-name yourdomain.com \
  --validation-method DNS \
  --subject-alternative-names "*.yourdomain.com"

10.2 Add DNS Validation

Follow the instructions in ACM console to add CNAME records to your DNS.

10.3 Create HTTPS Listener

bash
aws elbv2 create-listener \
  --load-balancer-arn YOUR_ALB_ARN \
  --protocol HTTPS \
  --port 443 \
  --certificates CertificateArn=YOUR_CERTIFICATE_ARN \
  --default-actions Type=forward,TargetGroupArn=YOUR_TARGET_GROUP_ARN

# Redirect HTTP to HTTPS
aws elbv2 modify-listener \
  --listener-arn YOUR_HTTP_LISTENER_ARN \
  --default-actions '[{"Type":"redirect","RedirectConfig":{"Protocol":"HTTPS","Port":"443","StatusCode":"HTTP_301"}}]'

Using AWS Copilot (Simpler Alternative)

AWS Copilot simplifies ECS deployment significantly:

bash
# Install Copilot
brew install aws/tap/copilot-cli  # macOS
# or download from GitHub releases

# Initialize application
copilot init

# Deploy to production
copilot deploy

# View logs
copilot svc logs

# Scale
copilot svc update --count 3

Copilot creates all the infrastructure (VPC, subnets, ALB, ECS) automatically.

Cost Breakdown

ComponentMonthly Cost (example)
ECS Fargate (2 tasks, 0.25 vCPU, 0.5GB)~$15/month
Application Load Balancer~$16/month
ECR (1GB storage)~$0.10/month
Data Transfer (10GB)~$0.90/month
CloudWatch Logs (1GB)~$0.50/month
Total~$32-35/month
Note

Use FARGATE_SPOT for development environments to save up to 70%.

Troubleshooting

Tasks Keep Failing

bash
# Check stopped tasks
aws ecs describe-tasks \
  --cluster myapp-cluster \
  --tasks $(aws ecs list-tasks --cluster myapp-cluster --desired-status STOPPED --query 'taskArns[0]' --output text)

# Check logs
aws logs tail /ecs/myapp --since 1h

Health Check Failures

  • Ensure your /api/health endpoint returns 200
  • Check security group allows traffic on your port
  • Verify the container is listening on 0.0.0.0, not 127.0.0.1

Image Pull Errors

bash
# Verify ECR login
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com

# Check image exists
aws ecr describe-images --repository-name myapp

Service Won't Stabilize

  • Check task definition memory/CPU limits
  • Verify IAM role permissions
  • Look at CloudWatch logs for errors

What's Next?

You now have a production-ready container deployment on AWS. For custom domains and professional URLs, continue to the Route 53 lesson.

Note

Checkpoint: Your application should be accessible at your ALB's DNS name: http://myapp-alb-xxxxx.us-east-1.elb.amazonaws.com