TerraformPilot

Cloud Computing

Create an AWS ECS Fargate Cluster with Terraform

Deploy containerized applications on AWS ECS Fargate using Terraform with ALB, auto-scaling, and CloudWatch monitoring. Step-by-step guide with code examples...

LLuca Berton1 min read

Introduction

#

AWS ECS Fargate removes the need to manage EC2 instances for your containers. You define what to run, and AWS handles the underlying infrastructure. This guide shows how to deploy a complete ECS Fargate setup with Terraform, including networking, load balancing, auto-scaling, and monitoring.

Architecture Overview

#

The setup includes:

  • VPC with public and private subnets across 2 AZs
  • Application Load Balancer in public subnets
  • ECS Fargate tasks in private subnets
  • NAT Gateway for outbound internet access
  • Auto-scaling based on CPU utilization

Network Configuration

#
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"
 
  name = "ecs-vpc"
  cidr = "10.0.0.0/16"
 
  azs             = ["us-east-1a", "us-east-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
 
  enable_nat_gateway   = true
  single_nat_gateway   = true
  enable_dns_hostnames = true
  enable_dns_support   = true
 
  tags = {
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

ECS Cluster

#
resource "aws_ecs_cluster" "main" {
  name = "production-cluster"
 
  setting {
    name  = "containerInsights"
    value = "enabled"
  }
 
  configuration {
    execute_command_configuration {
      logging = "OVERRIDE"
      log_configuration {
        cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs.name
      }
    }
  }
}
 
resource "aws_ecs_cluster_capacity_providers" "main" {
  cluster_name       = aws_ecs_cluster.main.name
  capacity_providers = ["FARGATE", "FARGATE_SPOT"]
 
  default_capacity_provider_strategy {
    capacity_provider = "FARGATE"
    weight            = 1
    base              = 1
  }
 
  default_capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    weight            = 4
  }
}

Task Definition

#
resource "aws_ecs_task_definition" "app" {
  family                   = "my-app"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 512
  memory                   = 1024
  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"
      portMappings = [
        {
          containerPort = 8080
          protocol      = "tcp"
        }
      ]
      environment = [
        { name = "PORT", value = "8080" },
        { name = "ENV", value = "production" }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = aws_cloudwatch_log_group.app.name
          "awslogs-region"        = var.region
          "awslogs-stream-prefix" = "app"
        }
      }
      healthCheck = {
        command     = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
        interval    = 30
        timeout     = 5
        retries     = 3
        startPeriod = 60
      }
    }
  ])
}

Application Load Balancer

#
resource "aws_lb" "main" {
  name               = "ecs-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = module.vpc.public_subnets
 
  enable_deletion_protection = true
}
 
resource "aws_lb_target_group" "app" {
  name        = "app-tg"
  port        = 8080
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = module.vpc.vpc_id
 
  health_check {
    healthy_threshold   = 2
    unhealthy_threshold = 10
    timeout             = 10
    interval            = 15
    path                = "/health"
    matcher             = "200"
  }
 
  deregistration_delay = 30
}
 
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = var.certificate_arn
 
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

ECS Service with Auto-Scaling

#
resource "aws_ecs_service" "app" {
  name            = "app-service"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = 2
  launch_type     = "FARGATE"
 
  network_configuration {
    subnets          = module.vpc.private_subnets
    security_groups  = [aws_security_group.ecs.id]
    assign_public_ip = false
  }
 
  load_balancer {
    target_group_arn = aws_lb_target_group.app.arn
    container_name   = "app"
    container_port   = 8080
  }
 
  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }
 
  lifecycle {
    ignore_changes = [desired_count]
  }
}
 
resource "aws_appautoscaling_target" "ecs" {
  max_capacity       = 10
  min_capacity       = 2
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}
 
resource "aws_appautoscaling_policy" "cpu" {
  name               = "cpu-scaling"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.ecs.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
  service_namespace  = aws_appautoscaling_target.ecs.service_namespace
 
  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
    target_value       = 70
    scale_in_cooldown  = 300
    scale_out_cooldown = 60
  }
}

Security Groups

#
resource "aws_security_group" "alb" {
  name   = "alb-sg"
  vpc_id = module.vpc.vpc_id
 
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
 
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
 
resource "aws_security_group" "ecs" {
  name   = "ecs-sg"
  vpc_id = module.vpc.vpc_id
 
  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
 
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Best Practices

#
  1. Use FARGATE_SPOT for non-critical workloads to save up to 70%
  2. Enable deployment circuit breaker for automatic rollback on failures
  3. Set health check grace period to avoid killing containers during startup
  4. Use private subnets for tasks with NAT Gateway for outbound access
  5. Enable Container Insights for detailed monitoring
  6. Pin container image tags — avoid :latest in production

Hands-On Courses

#

Learn by doing with interactive courses on CopyPasteLearn:

Conclusion

#

ECS Fargate with Terraform gives you a fully managed container platform with infrastructure as code. The combination of ALB, auto-scaling, and CloudWatch provides a production-grade setup that scales automatically and recovers from failures.

#Terraform#AWS#DevOps#Infrastructure as Code#Cloud Computing

Share this article