Skip to main content
This example demonstrates a comprehensive ECS deployment using the root module. It creates a Fargate cluster with an EC2 Auto Scaling Group capacity provider, deploys a service with FireLens logging, Service Connect, and an Application Load Balancer.

What is created

  • ECS cluster with Fargate, Fargate Spot, and EC2 ASG capacity providers
  • ECS service (ecsdemo-frontend) with:
    • FluentBit sidecar container for log forwarding
    • Service Connect with Cloud Map namespace
    • ALB integration with blue/alternate target groups
    • Predictive autoscaling policy
    • Tasks IAM role with ReadOnlyAccess and S3 list permissions
  • Application Load Balancer with HTTP listener and forwarding rules
  • EC2 Auto Scaling Group (ECS-optimized AMI, t3.large)
  • VPC with private/public subnets across 3 AZs
  • Cloud Map HTTP namespace for Service Connect

Prerequisites

  • AWS credentials configured
  • Terraform >= 1.5.7
  • AWS Provider >= 6.34

Code

main.tf
provider "aws" {
  region = local.region
}

data "aws_availability_zones" "available" {
  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}

locals {
  region = "eu-west-1"
  name   = "ex-${basename(path.cwd)}"

  vpc_cidr = "10.0.0.0/16"
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)

  container_name = "ecsdemo-frontend"
  container_port = 3000

  tags = {
    Name       = local.name
    Example    = local.name
    Repository = "https://github.com/terraform-aws-modules/terraform-aws-ecs"
  }
}

module "ecs" {
  source = "terraform-aws-modules/ecs/aws"

  cluster_name = local.name

  cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT"]
  default_capacity_provider_strategy = {
    FARGATE = {
      weight = 50
      base   = 20
    }
    FARGATE_SPOT = {
      weight = 50
    }
  }

  capacity_providers = {
    ASG = {
      auto_scaling_group_provider = {
        auto_scaling_group_arn         = module.autoscaling.autoscaling_group_arn
        managed_draining               = "ENABLED"
        managed_termination_protection = "ENABLED"

        managed_scaling = {
          maximum_scaling_step_size = 5
          minimum_scaling_step_size = 1
          status                    = "ENABLED"
          target_capacity           = 60
        }
      }
    }
  }

  services = {
    ecsdemo-frontend = {
      cpu    = 1024
      memory = 4096

      # Predictive autoscaling
      autoscaling_policies = {
        predictive = {
          policy_type = "PredictiveScaling"
          predictive_scaling_policy_configuration = {
            mode = "ForecastOnly"
            metric_specification = [{
              target_value = 60
              customized_scaling_metric_specification = {
                metric_data_query = [{
                  id = "cpu_util"
                  metric_stat = {
                    stat = "Average"
                    metric = {
                      metric_name = "CPUUtilization"
                      namespace   = "AWS/ECS"
                      dimension = [
                        { name = "ServiceName", value = "ecsdemo-frontend" },
                        { name = "ClusterName", value = "ex-complete" }
                      ]
                    }
                  }
                  return_data = true
                }]
              }
              predefined_load_metric_specification = {
                predefined_metric_type = "ECSServiceTotalCPUUtilization"
              }
            }]
          }
        }
      }

      container_definitions = {
        fluent-bit = {
          cpu       = 512
          memory    = 1024
          essential = true
          image     = nonsensitive(data.aws_ssm_parameter.fluentbit.value)
          user      = "0"
          firelensConfiguration = {
            type = "fluentbit"
          }
          memoryReservation                      = 50
          cloudwatch_log_group_retention_in_days = 30
        }

        (local.container_name) = {
          cpu       = 512
          memory    = 1024
          essential = true
          image     = "public.ecr.aws/aws-containers/ecsdemo-frontend:776fd50"

          healthCheck = {
            command = ["CMD-SHELL", "curl -f http://localhost:${local.container_port}/health || exit 1"]
          }

          portMappings = [{
            name          = local.container_name
            containerPort = local.container_port
            hostPort      = local.container_port
            protocol      = "tcp"
          }]

          capacity_provider_strategy = {
            ASG = {
              base              = 20
              capacity_provider = "ASG"
              weight            = 50
            }
          }

          readonlyRootFilesystem = false

          dependsOn = [{ containerName = "fluent-bit", condition = "START" }]

          enable_cloudwatch_logging = false
          logConfiguration = {
            logDriver = "awsfirelens"
            options = {
              Name                    = "stdout"
              log-driver-buffer-limit = "2097152"
            }
          }
          memoryReservation = 100

          restartPolicy = {
            enabled              = true
            ignoredExitCodes     = [1]
            restartAttemptPeriod = 60
          }
        }
      }

      deployment_configuration = {
        strategy = "LINEAR"
        linear_configuration = {
          step_percent              = 20
          step_bake_time_in_minutes = 1
        }
      }

      service_connect_configuration = {
        namespace = aws_service_discovery_http_namespace.this.arn
        service = [{
          client_alias = {
            port     = local.container_port
            dns_name = local.container_name
          }
          port_name      = local.container_name
          discovery_name = local.container_name
        }]
      }

      load_balancer = {
        service = {
          target_group_arn = module.alb.target_groups["ex-ecs"].arn
          container_name   = local.container_name
          container_port   = local.container_port

          advanced_configuration = {
            alternate_target_group_arn = module.alb.target_groups["ex-ecs-alt"].arn
            production_listener_rule   = module.alb.listener_rules["ex-http/ex-forward"].arn
          }
        }
      }

      tasks_iam_role_name        = "${local.name}-tasks"
      tasks_iam_role_description = "Example tasks IAM role for ${local.name}"

      tasks_iam_role_policies = {
        ReadOnlyAccess = "arn:aws:iam::aws:policy/ReadOnlyAccess"
      }
      tasks_iam_role_statements = [{
        actions   = ["s3:List*"]
        resources = ["arn:aws:s3:::*"]
      }]

      subnet_ids                    = module.vpc.private_subnets
      vpc_id                        = module.vpc.vpc_id
      availability_zone_rebalancing = "ENABLED"

      security_group_ingress_rules = {
        alb_3000 = {
          from_port                    = local.container_port
          description                  = "Service port"
          referenced_security_group_id = module.alb.security_group_id
        }
      }
      security_group_egress_rules = {
        all = {
          cidr_ipv4   = "0.0.0.0/0"
          ip_protocol = "-1"
        }
      }
    }
  }

  tags = local.tags
}

Key highlights

  • Multiple capacity providers: Fargate, Fargate Spot, and EC2 ASG all registered to the cluster. Individual containers can override with capacity_provider_strategy.
  • FireLens logging: The FluentBit sidecar runs as essential = true and the app container sets enable_cloudwatch_logging = false with logDriver = "awsfirelens".
  • Linear deployment: deployment_configuration.strategy = "LINEAR" with 20% step increments every minute.
  • Service Connect: Uses a Cloud Map HTTP namespace ARN for service-to-service discovery.
  • Tasks IAM role: Attached with ReadOnlyAccess policy and a custom s3:List* statement.

Fargate Example

Simpler Fargate-only deployment with blue/green.

EC2 Autoscaling Example

Standalone cluster + service modules with EC2 ASG.

Build docs developers (and LLMs) love