Lesson 11 of 15 4 minAdvanced Track

Containerized Microservices on AWS ECS Fargate

Provision secure, stateless container infrastructure using AWS ECS Fargate, ECR, Task Definitions, and execution roles.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

Key Takeaways

  • ECS Fargate provisions serverless container compute, eliminating the need to manage EC2 instances.
  • Differentiate the Task Execution Role (used by ECS agent to pull images) from the Task Role (used by your application code).
  • Task definitions should be treated as immutable blueprints that are version-controlled and updated via deployment pipelines.
Recommended Prerequisites
terraform-aws-10-rds-multi-az-ha

Premium outcome

Provision, secure, and automate production-grade cloud infrastructure at scale.

Backend and platform engineers who want to design, deploy, and automate robust production environments on AWS.

You leave with

  • A secure, modular, multi-environment AWS landing zone designed from scratch
  • A fully integrated GitOps deployment pipeline using GitHub Actions and Terraform S3 Backend
  • Hands-on expertise deploying containerized microservices (ECS Fargate + RDS) with secure IAM gating

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:

  1. 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.
  2. 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.

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.