Containerized Microservices on AWS ECS Fargate
Modern application scaling requires isolated, repeatable containerized deployments. While managing AWS EC2 virtual machines directly gives you control, it introduces significant operational overhead: you must manage OS security patches, scale server scaling pools, and orchestrate container ports manually.
To eliminate this overhead, production DevOps teams use AWS ECS Fargate, a serverless container orchestration platform. Under Fargate, you do not manage servers. You specify CPU, memory, and container blueprints, and AWS dynamically provisions compute at runtime.
Understanding IAM Role Division in ECS
One of the most common causes of ECS deployment failures is the confusion between the two distinct IAM roles that govern container operations:
- Task Execution Role (
execution_role_arn)- Used by the AWS ECS Agent before your container runs.
- Requires permissions to pull container images from AWS ECR and retrieve configuration secrets from AWS Secrets Manager.
- Task Role (
task_role_arn)- Used by your actual application code running inside the container.
- Requires permissions to interact with AWS services (e.g. read S3 buckets, publish messages to SQS, send emails via SES).
+───────────────────+
│ ECS Agent / Task │ ── 1. Pull Image (ECR) ──────> [ AWS ECR ]
│ Execution Role │ ── 2. Decrypt Secrets ───────> [ Secrets Manager ]
+───────────────────+
│
Launches
▼
+───────────────────+
│ Application Code │ ── 3. Read/Write Data ───────> [ AWS S3 ]
│ / Task Role │ ── 4. Publish Event ─────────> [ SQS Queue ]
+───────────────────+
Step 1: Provisioning the AWS ECR Repository & ECS Cluster
First, we create a private Elastic Container Registry (ECR) to store our container images and initialize our core ECS cluster boundary:
# modules/ecs/main.tf
# 1. Private ECR Repository
resource "aws_ecr_repository" "app" {
name = "${var.environment}-app-repo"
image_tag_mutability = "MUTABLE"
# Scan images for vulnerabilities on push
image_scanning_configuration {
scan_on_push = true
}
tags = {
Environment = var.environment
}
}
# 2. The ECS Cluster
resource "aws_ecs_cluster" "this" {
name = "${var.environment}-ecs-cluster"
setting {
name = "containerInsights"
value = "enabled" # Enable detailed metrics monitoring
}
}
Step 2: Defining the Task Definition Blueprint
An ECS Task Definition is the blueprint for your container. It outlines CPU allocations, memory limits, ports, environment variables, and log groupings:
# modules/ecs/main.tf (continued)
# CloudWatch Log Group for container logs
resource "aws_cloudwatch_log_group" "ecs_logs" {
name = "/ecs/${var.environment}-app"
retention_in_days = 7
}
# Define the Fargate Task Blueprint
resource "aws_ecs_task_definition" "app" {
family = "${var.environment}-app-task"
network_mode = "awsvpc" # Required for Fargate
requires_compatibilities = ["FARGATE"]
cpu = "256" # 0.25 vCPU
memory = "512" # 512 MB
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = "app"
image = "${aws_ecr_repository.app.repository_url}:latest"
essential = true
portMappings = [
{
containerPort = 8080
hostPort = 8080
protocol = "tcp"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.ecs_logs.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
# Inject Database Endpoint from module outputs
environment = [
{ name = "DB_HOST", value = var.db_host },
{ name = "DB_PORT", value = tostring(var.db_port) }
]
# Fetch DB credentials securely from Secrets Manager at runtime
secrets = [
{
name = "DB_PASSWORD"
valueFrom = "${var.db_secret_arn}:password::"
}
]
}
])
}
Step 3: Launching the ECS Service inside Private Subnets
An ECS Service controls the execution of tasks, maintaining your desired count (e.g. running 2 identical instances for high availability) and integrating them with your network:
# modules/ecs/main.tf (continued)
resource "aws_ecs_service" "app" {
name = "${var.environment}-app-service"
cluster = aws_ecs_cluster.this.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 2 # Keep 2 instances active for high availability
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids # Place containers in private subnets
security_groups = [var.app_security_group_id]
assign_public_ip = false # Security: Never allocate public IPs to app containers
}
lifecycle {
ignore_changes = [desired_count] # Allow autoscaling to modify count without terraform drift
}
}
By placing task definitions inside awsvpc network configurations and routing them strictly inside private subnets with no public IPs, you ensure that your containers are shielded from direct public intrusion and operate under enterprise-grade security structures.
Next Steps
Our containerized service is running, but it has no public ingress path. Clients cannot reach our API endpoint because the containers sit in private networks.
In the next lesson, we will provision an Application Load Balancer (ALB) in our public subnets to act as the secure traffic manager and distribute requests evenly across our Fargate tasks.