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.
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:
- 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.
- First-Class Networking: Each Fargate task receives its own elastic network interface (ENI) with a native private VPC IP address (
awsvpcmode), security groups, and seamless integration with Application Load Balancer Target Groups. - 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:
| Resource | Quantity | Unit 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:
- Task Execution Role (
executionRoleArn): Utilized by the ECS container agent to pull your image from private ECR and push container logs to CloudWatch. - 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
- Serverless Containers with Fargate: Eliminate EC2 management entirely. Let AWS manage hypervisor security, OS patching, and resource isolation while you focus on container manifests.
- Immutable ECR Tagging: Always set
--image-tag-mutability IMMUTABLEin production registries to prevent malicious or accidental tag overwrites. - 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.
awsvpcNative Networking: Every Fargate container gets its own VPC IP address and security group, enforcing microsegmentation between microservices.- Target Group
ipRegistration: When load balancing containers with Fargate, configure Target Groups with--target-type ipso 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!