LAB17_AWS_ECS Fargate Cluster with Task Definition and Service

ECS Fargate Cluster with Task Definition and Service

☁ AWS ⭐ Beginner ECS Fargate Task Definition Task Role CloudWatch Logs

Scenario

You have containerised the web application, built a Docker image, and pushed it to a private ECR repository. Now you want to run it on AWS Fargate without managing any EC2 instances. Fargate is serverless container compute — you define CPU/memory and AWS handles the underlying host.

Create an ECS cluster, a task definition for the container, and a Fargate service that keeps 2 tasks running at all times.

Your Objectives
  • Create an aws_ecs_cluster with Container Insights enabled.
  • Create an execution role (ecs-tasks.amazonaws.com) that allows Fargate to pull images and write logs. Attach the AmazonECSTaskExecutionRolePolicy managed policy.
  • Create a task role — a separate IAM role your application code assumes (for calling AWS services). Keep it empty for now.
  • Create an aws_ecs_task_definition with Fargate launch type, 256 CPU units, 512 MB memory, and awslogs log driver.
  • Create an aws_ecs_service with desired_count = 2 in two private subnets.

Additional Context

Fargate uses two distinct IAM roles. The execution role is used by AWS (the Fargate control plane) to set up the task — pulling the image and writing logs. The task role is assumed by your running container code to call AWS APIs. Confusing them is the most common ECS IAM mistake.

Container definitions are a JSON string inside the task definition resource. Use jsonencode() to avoid raw JSON strings in your configuration.

versions.tf

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

variables.tf

variable "aws_region"      { type = string; default = "us-east-1" }
variable "cluster_name"    { type = string; default = "web-app-cluster" }
variable "container_image" { type = string; description = "Full ECR image URI, e.g. 123456789012.dkr.ecr.us-east-1.amazonaws.com/web-app-prod:v1.0.0" }
variable "vpc_id"          { type = string }
variable "private_subnets" { type = list(string); description = "Subnets for Fargate tasks." }
variable "environment"     { type = string; default = "prod" }

main.tf

# main.tf

provider "aws" {
  region = var.aws_region
}

# ECS Cluster with Container Insights enabled (CloudWatch metrics per task).
resource "aws_ecs_cluster" "main" {
  name = var.cluster_name

  setting {
    name  = "containerInsights"
    value = "enabled"
  }

  tags = { Environment = var.environment, ManagedBy = "terraform" }
}

# Trust policy for ECS tasks.
data "aws_iam_policy_document" "ecs_assume" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals { type = "Service"; identifiers = ["ecs-tasks.amazonaws.com"] }
  }
}

# Execution role — used by Fargate to pull ECR images and write CloudWatch logs.
resource "aws_iam_role" "execution" {
  name               = "${var.cluster_name}-execution-role"
  assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
}

resource "aws_iam_role_policy_attachment" "execution" {
  role       = aws_iam_role.execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# Task role — assumed by the running container. Add permissions here as needed.
resource "aws_iam_role" "task" {
  name               = "${var.cluster_name}-task-role"
  assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
  tags               = { Environment = var.environment }
}

# CloudWatch Log Group for container logs.
resource "aws_cloudwatch_log_group" "ecs" {
  name              = "/ecs/${var.cluster_name}"
  retention_in_days = 30
}

# Security group for Fargate tasks — allows outbound internet, restrict inbound.
resource "aws_security_group" "tasks" {
  name   = "ecs-tasks-sg-${var.environment}"
  vpc_id = var.vpc_id
  egress { from_port = 0; to_port = 0; protocol = "-1"; cidr_blocks = ["0.0.0.0/0"] }
  ingress { from_port = 8080; to_port = 8080; protocol = "tcp"; cidr_blocks = ["10.0.0.0/16"] }
  tags = { Environment = var.environment }
}

# Task Definition — specifies the container(s) to run and their resource limits.
resource "aws_ecs_task_definition" "web" {
  family                   = "${var.cluster_name}-web"
  network_mode             = "awsvpc"       # required for Fargate
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"          # 0.25 vCPU
  memory                   = "512"          # 512 MB
  execution_role_arn       = aws_iam_role.execution.arn
  task_role_arn            = aws_iam_role.task.arn

  # Container definition as JSON — use jsonencode() to avoid escaping issues.
  container_definitions = jsonencode([{
    name      = "web"
    image     = var.container_image
    essential = true

    portMappings = [{
      containerPort = 8080
      protocol      = "tcp"
    }]

    # Log configuration — send all stdout/stderr to CloudWatch.
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"         = aws_cloudwatch_log_group.ecs.name
        "awslogs-region"        = var.aws_region
        "awslogs-stream-prefix" = "web"
      }
    }

    environment = [
      { name = "APP_ENV"; value = var.environment }
    ]
  }])
}

# ECS Service — keeps the desired number of tasks running.
resource "aws_ecs_service" "web" {
  name            = "${var.cluster_name}-web-svc"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.web.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = var.private_subnets
    security_groups  = [aws_security_group.tasks.id]
    assign_public_ip = false
  }

  # Replace tasks automatically if the task definition changes.
  deployment_minimum_healthy_percent = 50
  deployment_maximum_percent         = 200
}

outputs.tf

output "cluster_arn"          { value = aws_ecs_cluster.main.arn }
output "service_name"         { value = aws_ecs_service.web.name }
output "task_definition_arn"  { value = aws_ecs_task_definition.web.arn }

Workflow Commands

terraform init
terraform apply \
  -var="container_image=123456789012.dkr.ecr.us-east-1.amazonaws.com/web-app-prod:v1.0.0" \
  -var="vpc_id=vpc-xxxx" \
  -var='private_subnets=["subnet-aaa","subnet-bbb"]'

# List running tasks
aws ecs list-tasks --cluster $(terraform output -raw cluster_arn)

# View container logs
aws logs tail /ecs/web-app-cluster --follow

terraform destroy -var="container_image=..." -var="vpc_id=..." -var='private_subnets=["..."]'
✓ Expected Output After Apply
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

Outputs:

cluster_arn         = "arn:aws:ecs:us-east-1:123456789012:cluster/web-app-cluster"
service_name        = "web-app-cluster-web-svc"
task_definition_arn = "arn:aws:ecs:us-east-1:123456789012:task-definition/web-app-cluster-web:1"