Cloud 18 min read

AWS CLI First #14: Production Container Workflows: ECR Push & ECS Fargate Task Provisioning

Deploy serverless container microservices from your terminal: configure private ECR repositories with image vulnerability scanning, authenticate Docker, register ECS Fargate Task Definitions, and orchestrate auto-scaled container services behind an Application Load Balancer.

Mohammad Rizky Prawira portrait
Mohammad Rizky Prawira
terminal Series Guide • Part 14 of 17

AWS with CLI: The Terminal-First Mastery Path

Part of the CLI-first comprehensive learning path from foundational setup to production cloud architectures.

Virtual machines revolutionized cloud infrastructure, but containers redefined how engineering teams package, ship, and run distributed microservices. However, managing container host clusters—patching Docker engines, managing EC2 auto scaling pools, and balancing container density—introduces significant operational burden.

AWS solves this through Amazon Elastic Container Service (ECS) with AWS Fargate:

  1. Serverless Compute Engine: With Fargate, you no longer manage EC2 instances or cluster capacity. You define container requirements (vCPU and Memory), and AWS provisions the isolated microVM execution environment on demand.
  2. First-Class Networking: Each Fargate task receives its own elastic network interface (ENI) with a native private VPC IP address (awsvpc mode), security groups, and seamless integration with Application Load Balancer Target Groups.
  3. Enterprise Image Governance: Amazon Elastic Container Registry (ECR) provides private, encrypted Docker registries with automated vulnerability scanning and lifecycle cleanup.

In this guide, we orchestrate a complete enterprise container deployment purely from the terminal. We will provision a hardened ECR registry, authenticate Docker, build and push a production container image, register an ECS Task Definition with CloudWatch logging, and run an auto-scaled ECS Fargate Service attached to an Application Load Balancer.


💰 Estimated Lab Cost & Resource Architecture

1. Cost Breakdown for this Lab

All resources in this lab are provisioned on-demand in the Asia Pacific (Singapore) ap-southeast-1 region:

ResourceQuantityUnit Cost (Hourly)Estimated Lab Cost (1 Hour)
AWS Fargate Compute (0.25 vCPU, 0.5 GB RAM)2 Running Tasks$0.0101 per task / hour~$0.020
Amazon ECR Storage< 100 MB Container Image$0.10 / GB / month< $0.001
Application Load Balancer (ALB)1 Load Balancer$0.0225 / hour~$0.023
Amazon CloudWatch Logs< 10 MB Logs$0.50 / GB ingested< $0.001
Total Estimated Lab Cost~$0.044 USD

2. Architecture Diagram

+----------------------------------------------------------------+
|               ECS Fargate Serverless Container Architecture     |
+-------------------------------+--------------------------------+
                                |
                                v
+-------------------------------+--------------------------------+
|       Application Load Balancer (Internet Ingress on :80)      |
+-------------------------------+--------------------------------+
                                |
                                v
+-------------------------------+--------------------------------+
|         ALB Target Group (HTTP Health Checks on :8080)         |
+-------------------------------+--------------------------------+
                                |
                +---------------+---------------+
                | (Traffic Routed via awsvpc)   |
                v                               v
+-------------------------------+ +------------------------------+
|     Availability Zone 1a      | |     Availability Zone 1b     |
|   (Subnet: 10.0.1.0/24)       | |   (Subnet: 10.0.2.0/24)      |
|                               | |                              |
| +---------------------------+ | | +--------------------------+ |
| |   Fargate Task #1         | | | |   Fargate Task #2        | |
| |   (0.25 vCPU, 0.5 GB RAM) | | | |   (0.25 vCPU, 0.5 GB RAM)| |
| |   IP: 10.0.1.x            | | | |   IP: 10.0.2.x           | |
| +---------------------------+ | | +--------------------------+ |
+-------------------------------+ +------------------------------+
                                |
                                v
+----------------------------------------------------------------+
|  Container Control Plane & Image Storage:                      |
|  - ECR: Private Image Repo (KMS Encrypted, Tag Immutability)   |
|  - CloudWatch: Log Group /ecs/production-microservice          |
|  - IAM Execution Role: ECR Pull + CloudWatch Write Permissions |
+----------------------------------------------------------------+

📦 Phase 1: Hardened Private ECR Registry Provisioning

Before running a container in ECS, the image must reside in a secure container registry. We configure an ECR repository with immutable image tags (preventing accidental image overwrites) and automated vulnerability scanning on push:

REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
REPO_NAME="production-api-microservice"

# Create private ECR repository with security guardrails
aws ecr create-repository \
  --repository-name "$REPO_NAME" \
  --image-tag-mutability IMMUTABLE \
  --image-scanning-configuration scanOnPush=true \
  --encryption-configuration encryptionType=AES256 \
  --tags "Key=Environment,Value=Production" \
  --region "$REGION" \
  --profile "$PROFILE"

# Retrieve Repository URI
ECR_URI=$(aws ecr describe-repositories \
  --repository-names "$REPO_NAME" \
  --region "$REGION" \
  --profile "$PROFILE" \
  --query "repositories[0].repositoryUri" \
  --output text)

echo "Private ECR Repository URI: $ECR_URI"

Applying an ECR Lifecycle Policy (FinOps)

Stale, untagged images accumulate gigabytes of storage over time. Enforce an automated lifecycle rule to expire untagged container builds after 7 days:

cat <<'EOF' > ecr-lifecycle.json
{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Expire untagged images older than 7 days",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 7
      },
      "action": {
        "type": "expire"
      }
    }
  ]
}
EOF

aws ecr put-lifecycle-policy \
  --repository-name "$REPO_NAME" \
  --lifecycle-policy-text file://ecr-lifecycle.json \
  --region "$REGION" \
  --profile "$PROFILE"

rm -f ecr-lifecycle.json
echo "ECR Lifecycle Policy enforced."

🔑 Phase 2: Authenticating Docker with ECR & Pushing Images

The standard Docker CLI cannot use static AWS access keys directly. AWS ECR provides an authentication token that generates a temporary 12-hour authorization bridge for standard docker login:

1. Authenticating Local Docker CLI

aws ecr get-login-password --region "$REGION" --profile "$PROFILE" | \
  docker login --username AWS --password-stdin "$ECR_URI"

2. Creating a Production Microservice Dockerfile

Let’s build a lightweight Node.js/Python or Go HTTP microservice. For our lab, we will use a self-contained, high-performance Alpine Nginx image configured on port 8080:

mkdir -p ./container-app
cat <<'EOF' > ./container-app/Dockerfile
FROM alpine:3.20
RUN apk add --no-cache nginx curl
COPY default.conf /etc/nginx/http.d/default.conf
RUN mkdir -p /var/www/html
RUN echo '{"service": "orders-microservice", "version": "1.0.0", "status": "healthy"}' > /var/www/html/health
RUN echo '<!DOCTYPE html><html><body style="background:#0f172a;color:#38bdf8;font-family:sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;"><h1>🐳 AWS ECS Fargate Microservice v1.0</h1></body></html>' > /var/www/html/index.html
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
EOF

cat <<'EOF' > ./container-app/default.conf
server {
    listen 8080;
    server_name _;
    location /health {
        root /var/www/html;
        add_header Content-Type application/json;
    }
    location / {
        root /var/www/html;
        index index.html;
    }
}
EOF

3. Building, Tagging & Pushing the Container Image

Build the container image targeting Linux ARM64/AMD64 and push it directly to your private ECR repository:

IMAGE_TAG="v1.0.0"

# Build container image
docker build --platform linux/amd64 -t "${REPO_NAME}:${IMAGE_TAG}" ./container-app/

# Tag image with ECR URI
docker tag "${REPO_NAME}:${IMAGE_TAG}" "${ECR_URI}:${IMAGE_TAG}"

# Push to private AWS ECR
docker push "${ECR_URI}:${IMAGE_TAG}"

echo "Container image successfully pushed to ECR: ${ECR_URI}:${IMAGE_TAG}"
rm -rf ./container-app

Verify that the image is registered and inspect the automated vulnerability scan results:

aws ecr describe-images \
  --repository-name "$REPO_NAME" \
  --image-ids imageTag="$IMAGE_TAG" \
  --region "$REGION" \
  --profile "$PROFILE" \
  --query "imageDetails[0].[imageTags[0],imageScanStatus.status,imageSizeInBytes]" \
  --output table

🏛️ Phase 3: ECS Cluster & IAM Task Execution Roles

Amazon ECS uses two distinct IAM roles:

  1. Task Execution Role (executionRoleArn): Utilized by the ECS container agent to pull your image from private ECR and push container logs to CloudWatch.
  2. Task Role (taskRoleArn): Assumed by your application code inside the container to interact with other AWS services (e.g. DynamoDB, S3).

1. Creating the Task Execution Role

REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"

aws iam get-role --role-name "ecsTaskExecutionRole" --profile "$PROFILE" >/dev/null 2>&1 || \
  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"}]}' \
    --profile "$PROFILE"

aws iam attach-role-policy \
  --role-name "ecsTaskExecutionRole" \
  --policy-arn "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" \
  --profile "$PROFILE"

EXEC_ROLE_ARN=$(aws iam get-role --role-name "ecsTaskExecutionRole" --profile "$PROFILE" --query "Role.Arn" --output text)
echo "Task Execution Role ARN: $EXEC_ROLE_ARN"

2. Creating CloudWatch Log Group for Container Logs

aws logs create-log-group \
  --log-group-name "/ecs/production-microservice" \
  --region "$REGION" \
  --profile "$PROFILE" 2>/dev/null || true

aws logs put-retention-policy \
  --log-group-name "/ecs/production-microservice" \
  --retention-in-days 7 \
  --region "$REGION" \
  --profile "$PROFILE"

3. Creating the ECS Cluster

Provision an ECS Cluster configured with Container Insights telemetry:

aws ecs create-cluster \
  --cluster-name "Production-Microservices-Cluster" \
  --settings "name=containerInsights,value=enabled" \
  --tags "key=Environment,value=Production" \
  --region "$REGION" \
  --profile "$PROFILE"

echo "ECS Cluster provisioned."

📝 Phase 4: Crafting the Fargate Task Definition

A Task Definition is the blueprint for your containerized application. We specify the FARGATE launch type, awsvpc network mode, CPU/memory limits, and log streaming configuration:

cat <<JSON > task-definition.json
{
  "family": "orders-microservice-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "$EXEC_ROLE_ARN",
  "containerDefinitions": [
    {
      "name": "orders-api",
      "image": "${ECR_URI}:${IMAGE_TAG}",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 8080,
          "hostPort": 8080,
          "protocol": "tcp"
        }
      ],
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 15
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/production-microservice",
          "awslogs-region": "$REGION",
          "awslogs-stream-prefix": "orders-api"
        }
      }
    }
  ]
}
JSON

aws ecs register-task-definition \
  --cli-input-json file://task-definition.json \
  --region "$REGION" \
  --profile "$PROFILE"

rm -f task-definition.json
echo "Task Definition registered."

🚀 Phase 5: Deploying ECS Fargate Service with Load Balancing

We bind our containerized microservice to our Application Load Balancer so incoming HTTP requests on port 80 are routed directly to our container tasks listening on port 8080.

1. Creating Target Group for Container Tasks

Create a Target Group with target-type ip (mandatory for awsvpc Fargate tasks):

REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"

VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=Production-VPC" --region "$REGION" --profile "$PROFILE" --query "Vpcs[0].VpcId" --output text)
PUB_SUB_A=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Public-Subnet-A" --region "$REGION" --profile "$PROFILE" --query "Subnets[0].SubnetId" --output text)
PUB_SUB_B=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Public-Subnet-B" --region "$REGION" --profile "$PROFILE" --query "Subnets[0].SubnetId" --output text)

ECS_TG_ARN=$(aws elbv2 create-target-group \
  --name "Production-ECS-TG" \
  --protocol HTTP \
  --port 8080 \
  --vpc-id "$VPC_ID" \
  --target-type ip \
  --health-check-protocol HTTP \
  --health-check-path "/health" \
  --health-check-interval-seconds 15 \
  --health-check-timeout-seconds 5 \
  --healthy-threshold-count 2 \
  --unhealthy-threshold-count 2 \
  --region "$REGION" \
  --profile "$PROFILE" \
  --query "TargetGroups[0].TargetGroupArn" \
  --output text)

echo "ECS Target Group Created: $ECS_TG_ARN"

2. Creating ECS Task Security Group

Create a security group for the containers that authorizes ingress on port 8080 exclusively from the ALB:

ALB_SG_ID=$(aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=ALB-Public-SG" --region "$REGION" --profile "$PROFILE" --query "SecurityGroups[0].GroupId" --output text 2>/dev/null || true)

ECS_TASK_SG=$(aws ec2 create-security-group \
  --group-name "ECS-Fargate-Tasks-SG" \
  --description "Inbound access to containers from ALB" \
  --vpc-id "$VPC_ID" \
  --tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=ECS-Fargate-Tasks-SG}]' \
  --region "$REGION" \
  --profile "$PROFILE" \
  --query "GroupId" \
  --output text)

if [ -n "$ALB_SG_ID" ] && [ "$ALB_SG_ID" != "None" ]; then
  aws ec2 authorize-security-group-ingress \
    --group-id "$ECS_TASK_SG" \
    --protocol tcp \
    --port 8080 \
    --source-group "$ALB_SG_ID" \
    --region "$REGION" \
    --profile "$PROFILE"
else
  aws ec2 authorize-security-group-ingress \
    --group-id "$ECS_TASK_SG" \
    --protocol tcp \
    --port 8080 \
    --cidr "0.0.0.0/0" \
    --region "$REGION" \
    --profile "$PROFILE"
fi

echo "ECS Task Security Group: $ECS_TASK_SG"

3. Launching the Fargate Service

Launch an ECS Service with a desired count of 2 tasks, balanced across our Multi-AZ subnets:

aws ecs create-service \
  --cluster "Production-Microservices-Cluster" \
  --service-name "Orders-Microservice" \
  --task-definition "orders-microservice-task" \
  --desired-count 2 \
  --launch-type "FARGATE" \
  --network-configuration "{
    \"awsvpcConfiguration\": {
      \"subnets\": [\"$PUB_SUB_A\", \"$PUB_SUB_B\"],
      \"securityGroups\": [\"$ECS_TASK_SG\"],
      \"assignPublicIp\": \"ENABLED\"
    }
  }" \
  --load-balancers "targetGroupArn=$ECS_TG_ARN,containerName=orders-api,containerPort=8080" \
  --deployment-configuration "maximumPercent=200,minimumHealthyPercent=100" \
  --region "$REGION" \
  --profile "$PROFILE"

echo "ECS Fargate Service successfully created."

4. Inspecting Running Tasks & Target Health

Query the running tasks:

aws ecs list-tasks \
  --cluster "Production-Microservices-Cluster" \
  --service-name "Orders-Microservice" \
  --region "$REGION" \
  --profile "$PROFILE" \
  --output table

Check Target Group registration:

aws elbv2 describe-target-health \
  --target-group-arn "$ECS_TG_ARN" \
  --region "$REGION" \
  --profile "$PROFILE" \
  --query "TargetHealthDescriptions[*].[Target.Id,Target.Port,TargetHealth.State]" \
  --output table

You will see the private IP addresses of your Fargate containers automatically registered and reporting healthy!


🔄 Phase 6: Live Rolling Container Deployments

When you push a new container version (v1.1.0), how do you update the service with zero downtime?

ECS Fargate executes rolling blue/green deployments automatically. With maximumPercent=200 and minimumHealthyPercent=100, ECS spins up 2 new tasks, verifies their /health checks, registers them to the Target Group, and only then terminates the 2 old tasks:

# Force a rolling redeployment via CLI
aws ecs update-service \
  --cluster "Production-Microservices-Cluster" \
  --service "Orders-Microservice" \
  --force-new-deployment \
  --region "$REGION" \
  --profile "$PROFILE" \
  --output text

🧹 Phase 7: 1-Click Hardened Lab Teardown

Fargate tasks run serverlessly, but active services and ECR repositories must be purged to eliminate charges:

cat <<'EOF' > destroy-containers-lab.sh
#!/usr/bin/env bash
set -euo pipefail

REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"

echo "=== 1. Deleting ECS Fargate Service & Cluster ==="
if aws ecs describe-services --cluster "Production-Microservices-Cluster" --services "Orders-Microservice" --region "$REGION" --profile "$PROFILE" 2>/dev/null | grep -q "ACTIVE"; then
  echo "Scaling ECS Service to 0 tasks..."
  aws ecs update-service \
    --cluster "Production-Microservices-Cluster" \
    --service "Orders-Microservice" \
    --desired-count 0 \
    --region "$REGION" \
    --profile "$PROFILE" >/dev/null

  echo "Deleting ECS Service..."
  aws ecs delete-service \
    --cluster "Production-Microservices-Cluster" \
    --service "Orders-Microservice" \
    --force \
    --region "$REGION" \
    --profile "$PROFILE" >/dev/null
fi

echo "Deleting ECS Cluster..."
aws ecs delete-cluster \
  --cluster "Production-Microservices-Cluster" \
  --region "$REGION" \
  --profile "$PROFILE" 2>/dev/null || true

echo "=== 2. Deleting Target Group & Security Groups ==="
ECS_TG_ARN=$(aws elbv2 describe-target-groups --names "Production-ECS-TG" --region "$REGION" --profile "$PROFILE" --query "TargetGroups[0].TargetGroupArn" --output text 2>/dev/null || true)
if [ -n "$ECS_TG_ARN" ] && [ "$ECS_TG_ARN" != "None" ]; then
  aws elbv2 delete-target-group --target-group-arn "$ECS_TG_ARN" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
fi

VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=Production-VPC" --region "$REGION" --profile "$PROFILE" --query "Vpcs[0].VpcId" --output text 2>/dev/null || true)
if [ -n "$VPC_ID" ] && [ "$VPC_ID" != "None" ]; then
  ECS_TASK_SG=$(aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=ECS-Fargate-Tasks-SG" --region "$REGION" --profile "$PROFILE" --query "SecurityGroups[0].GroupId" --output text 2>/dev/null || true)
  if [ -n "$ECS_TASK_SG" ] && [ "$ECS_TASK_SG" != "None" ]; then
    aws ec2 delete-security-group --group-id "$ECS_TASK_SG" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
  fi
fi

echo "=== 3. Deleting ECR Repository ==="
aws ecr delete-repository \
  --repository-name "production-api-microservice" \
  --force \
  --region "$REGION" \
  --profile "$PROFILE" 2>/dev/null || true

echo "=== 4. Deleting CloudWatch Log Group ==="
aws logs delete-log-group \
  --log-group-name "/ecs/production-microservice" \
  --region "$REGION" \
  --profile "$PROFILE" 2>/dev/null || true

rm -f destroy-containers-lab.sh
echo "=== Part 14 Teardown Complete! Zero Container Spend Remaining. ==="
EOF

chmod +x destroy-containers-lab.sh
./destroy-containers-lab.sh

🏆 Key Takeaways & Enterprise Architecture Patterns

  1. Serverless Containers with Fargate: Eliminate EC2 management entirely. Let AWS manage hypervisor security, OS patching, and resource isolation while you focus on container manifests.
  2. Immutable ECR Tagging: Always set --image-tag-mutability IMMUTABLE in production registries to prevent malicious or accidental tag overwrites.
  3. Task Execution Role vs. Task Role: Never bundle permissions. Use the execution role strictly for infrastructure hooks (ECR pull, CloudWatch logs) and the task role for business application permissions.
  4. awsvpc Native Networking: Every Fargate container gets its own VPC IP address and security group, enforcing microsegmentation between microservices.
  5. Target Group ip Registration: When load balancing containers with Fargate, configure Target Groups with --target-type ip so the ALB routes directly to the container’s task ENI.

🔮 What’s Next in Part 15?

What if your code doesn’t even need a container runtime?

In Part 15: Serverless from the Terminal: Lambda Functions & HTTP API Gateways, we explore the ultimate serverless paradigm. We will package zip artifacts, deploy ARM64 Python/Node.js functions, wire fine-grained IAM execution roles, and expose ultra-low latency endpoints with HTTP API Gateway v2—all with zero servers and zero idle costs!

Related & Recommended Guides

Continue exploring related systems architectures and engineering field notes.