Skip to main content

Command Palette

Search for a command to run...

Provision an Amazon ECS Cluster Using Terraform

Published
9 min read

Description

Terraform is an infrastructure-as-code tool that makes handling infrastructure more straightforward and manageable. A Terraform module is a collection of configuration files that encapsulate groups of resources dedicated to one task. Modules can be used to package and reuse resource configurations, making it easier to manage larger application components like an Amazon ECS Cluster.

In this demo, you will provision an Amazon ECS Cluster into an existing Amazon VPC.

Learning objectives

  • Define a Terraform module that deploys Amazon ECS resources

  • Apply an Auto Scaling Group Policy to respond to ECS metrics

  • Deploy an Amazon ECS Cluster into an existing Amazon VPC using Terraform

Prerequisites

  • IDE with configured IAM credentials

  • Installed Terraform with IDE

  • cloning repo: https://github.com/noweder/ECS-Terraform.git

  • Below resources pre-created in your AWS account to be referenced in later steps:

    • 1 Virtual Private Cloud

    • 2 Public Subnets

    • 2 Private Subnets

    • Public-facing Application Load Balancer

    • Internal-facing Application Load Balancer

Target Architecture

Demo Steps

  • Initiate Terraform by running the following command from the cloned directory path: terrafrom init

  • Reviewing the Terraform Variables file

    The variables.tf file defines the name, description, and expected data type for each variable referenced in the main.tf file. Feel free to review the data types of each variable.

    You will configure the terraform.tfvars file in the next step. This file will include the actual values for each variable. These values will be retrieved from the existing infrastructure.

    The outputs.tf file defines the expected output values for the deployment. In this demo, the CloudWatch Log Group names and the ECS Cluster ARN will be output after a successful deployment.

  • Defining an Amazon ECS Cluster Using Terraform

    In this step, you will configure an Amazon ECS cluster, service, and task definition in the main Terraform configuration file. This lab step will also explore the terraform.tfvars file that provides configuration values to the ECS Cluster.

    The ECS Cluster will contain two ECS Services.

    A frontend service will deploy tasks (containers) behind the public-facing Application Load Balancer (ALB) and in each of the public subnets. These tasks are based on an existing ECR image that serves a simple webpage and data retrieved from the backend.

    The backend service tasks will be behind the internal-facing application load balancer, inside each private subnet. The ECR image used for the backend service generates sample data that is passed to the frontend service.

    Once the ECS Cluster is deployed, you will be able to access the entire application by navigating to the public ALB's URL.

    Double-click the terraform.tfvars file to open it in the editor, then paste in the following configuration values:

      app_name = "lab"
      ecs_role_arn = "arn:aws:iam::574227279091:role/lab-ecs-task-execution-role"
      ecs_services = {
        frontend = {
          image          = "574227279091.dkr.ecr.us-west-2.amazonaws.com/frontend:1.0.0"
          cpu            = 256
          memory         = 512
          container_port = 8080
          host_port      = 8080
          desired_count  = 2
          is_public      = true
          protocol       = "HTTP"
          auto_scaling = {
            max_capacity    = 3
            min_capacity    = 2
            cpu_threshold    = 50
            memory_threshold = 50
          }
        }
        backend = {
          image          = "574227279091.dkr.ecr.us-west-2.amazonaws.com/backend:1.0.0"
          cpu            = 256
          memory         = 512
          container_port = 8080
          host_port      = 8080
          desired_count  = 2
          is_public      = false
          protocol       = "HTTP"
          auto_scaling = {
            max_capacity    = 3
            min_capacity    = 2
            cpu_threshold    = 75
            memory_threshold = 75
          }
        }
      }
      internal_alb_dns = "internal-lab-internal-400236659.us-west-2.elb.amazonaws.com"
      private_subnet_ids = [
        "subnet-0198a9e41d0bca224",
        "subnet-017d8d0f81ff2975e"
      ]
      public_subnet_ids = [
        "subnet-05f01cddf0795094f",
        "subnet-097cabee00a37f8b5"
      ]
      security_group_ids = [
        "sg-020dd20b13ae12807",
        "sg-0269de162c343c363"
      ]
      target_group_arns = {
        backend = {
          arn = "arn:aws:elasticloadbalancing:us-west-2:574227279091:targetgroup/backend-tg/8798f2ad7abee6f8"
        }
        frontend = {
          arn = "arn:aws:elasticloadbalancing:us-west-2:574227279091:targetgroup/frontend-tg/5d5b9e91c3b1ba49"
        }
      }
    

    The most important value defined in this file is the ecs_services map. This map of objects includes the frontend and backend service configurations. The use of maps, objects, and lists allows you to template an application effectively. Terraform provides meta-arguments such as for_each, that can be used to traverse a map or list to reduce the number of resources defined in your main.tf file.

  • Open the main.tf file and paste below code at the end:

    resource "aws_ecs_cluster" "ecs_cluster" {
      name = lower("${var.app_name}-cluster")
    }
    
    # ECS Services
    resource "aws_ecs_service" "service" {
      for_each = var.ecs_services
      name            = "${each.key}-service"
      cluster         = aws_ecs_cluster.ecs_cluster.id
      task_definition = aws_ecs_task_definition.ecs_task_definition[each.key].arn
      launch_type     = "FARGATE"
      desired_count   = each.value.desired_count
      network_configuration {
        subnets          = each.value.is_public == true ? var.public_subnet_ids : var.private_subnet_ids
        assign_public_ip = each.value.is_public
        security_groups  = var.security_group_ids
      }
      load_balancer {
        target_group_arn = var.target_group_arns[each.key].arn
        container_name   = each.key
        container_port   = each.value.container_port
      }
    }
    
    # ECS Task Definitions
    resource "aws_ecs_task_definition" "ecs_task_definition" {
      for_each = var.ecs_services
      family                   = "\({lower(var.app_name)}-\){each.key}"
      execution_role_arn       = var.ecs_role_arn
      requires_compatibilities = ["FARGATE"]
      network_mode             = "awsvpc"
      memory                   = each.value.memory
      cpu                      = each.value.cpu
      container_definitions = jsonencode([
        {
          name      = each.key
          image     = each.value.image
          cpu       = each.value.cpu
          memory    = each.value.memory
          essential = true
          environment = [
            { name = "INTERNAL_ALB", value = var.internal_alb_dns },
            { name = "SERVICE_HOST", value = var.internal_alb_dns },
            { name = "SERVER_SERVLET_CONTEXT_PATH", value = each.value.is_public == true ? "/" : "/${each.key}" },
            { name = "SERVICES", value = "backend" },
            { name = "SERVICE", value = each.key },
            { name = "SERVICE_NAME", value = each.key }
          ]
          portMappings = [
            {
              containerPort = each.value.container_port
            }
          ]
          logConfiguration = {
            logDriver = "awslogs"
            options = {
              awslogs-group         = "${lower(each.key)}-logs"
              awslogs-region        = data.aws_region.current.name
              awslogs-stream-prefix = var.app_name
            }
          }
        }
      ])
    }
    

    The first resource is an ECS Cluster. The cluster is named lab-cluster after retrieving the app_name variable from the terraform.tfvars file.

    The ECS Service definition uses the for_each meta-argument to create one service for each object defined in the ecs_services variable. The each.key will resolve to frontend and backend. Instances of each.value are followed by the corresponding attribute name of the object. The desired_count of tasks for each service uses each.value.desired_count and resolves to 2. Surrounding these dynamic calls with ${ } allows you to resolve and concatenate the value into a string, i.e. name = frontend-service.

    The network_configuration references the is_public attribute of each ECS service to determine which subnet to deploy the services into. For each service, if this attribute is set to true, the service is deployed into the public subnets, and each task is assigned a public IP address. If set to false, no public IP is assigned, and the service is deployed into the private subnets.

    The target_group_arns map determines which ALB each service is associated with. The backend target group is associated with the internal ALB, which is not publicly accessible. The frontend target group is associated with the public ALB since it will serve the application's webpage.

    The ecs_task_definition resource defines the tasks, or containers, within each ECS service. All ECS tasks are set to the Fargate launch type and assume the same task execution IAM Role. This role provides the task agent with permission to perform AWS API calls on your behalf.

    container_definitions include which ECR image to use, CPU and memory configurations, as well as environment variables to be passed into the container. The environment variables in this task definition will be referenced by both frontend and backend services and are specific to the ECR image used in this lab.

    The task portMappings allow the containers to access the container port to send or receive traffic. Data will travel between frontend and backend tasks using this port.

    Finally, the logConfiguration defines the awslogs log driver which is used to send logs from the containers to Amazon CloudWatch Logs. The options define which CloudWatch Logs Group and AWS Region to send logs to.

  • Applying CloudWatch Monitoring and Auto Scaling to an ECS Cluster With Terraform

    • In this lab step, you will define CloudWatch Logs that store container logs for each of the ECS Services. You will also configure your ECS Services with Auto Scaling Group policies that scale the service task count in or out depending on certain ECS metrics.

    • Paste the following code to the end of the main.tf file:

      # CloudWatch Log Groups
      resource "aws_cloudwatch_log_group" "ecs_cw_log_group" {
        for_each = toset(keys(var.ecs_services))
        name     = lower("${each.key}-logs")
      }
      
      # ECS Auto Scaling Configuration
      resource "aws_appautoscaling_target" "service_autoscaling" {
        for_each = var.ecs_services
        max_capacity       = each.value.auto_scaling.max_capacity
        min_capacity       = each.value.auto_scaling.min_capacity
        resource_id        = "service/\({aws_ecs_cluster.ecs_cluster.name}/\){aws_ecs_service.service[each.key].name}"
        scalable_dimension = "ecs:service:DesiredCount"
        service_namespace  = "ecs"
      }
      
      # Auto Scaling Policies
      resource "aws_appautoscaling_policy" "ecs_policy_memory" {
        for_each = var.ecs_services
        name               = "${var.app_name}-memory-autoscaling"
        policy_type        = "TargetTrackingScaling"
        resource_id        = aws_appautoscaling_target.service_autoscaling[each.key].resource_id
        scalable_dimension = aws_appautoscaling_target.service_autoscaling[each.key].scalable_dimension
        service_namespace  = aws_appautoscaling_target.service_autoscaling[each.key].service_namespace
        target_tracking_scaling_policy_configuration {
          predefined_metric_specification {
            predefined_metric_type = "ECSServiceAverageMemoryUtilization"
          }
          target_value = each.value.auto_scaling.memory_threshold
        }
      }
      resource "aws_appautoscaling_policy" "ecs_policy_cpu" {
        for_each = var.ecs_services
        name               = "${var.app_name}-cpu-autoscaling"
        policy_type        = "TargetTrackingScaling"
        resource_id        = aws_appautoscaling_target.service_autoscaling[each.key].resource_id
        scalable_dimension = aws_appautoscaling_target.service_autoscaling[each.key].scalable_dimension
        service_namespace  = aws_appautoscaling_target.service_autoscaling[each.key].service_namespace
        target_tracking_scaling_policy_configuration {
          predefined_metric_specification {
            predefined_metric_type = "ECSServiceAverageCPUUtilization"
          }
          target_value = each.value.auto_scaling.cpu_threshold
        }
      }
      
  • In this lab step, you will deploy and test your ECS application using the Terraform CLI.

    1. In the browser IDE terminal, enter the following command to validate and summarize your deployment:
        
        [**Copy code**](https://platform.qa.com/lab/provisioning-an-amazon-ecs-cluster-using-terraform/session-page/#)
        
        ```plaintext
        terraform plan -no-color > plan.txt 
        ```
        
        The command will send the output to the **plan.txt** file rather than displaying it in the terminal. There will be a total of 13 resources outlined in this plan and sorted in alphabetical order.
        
        This command also runs `terraform validate` before outputting the plan. Any misaligned variable types or misconfigured resources will be output if found.
        
        Feel free to explore this file and the resolved resource values.
        
    2. Enter the following command to deploy your ECS Cluster:
        
        [**Copy code**](https://platform.qa.com/lab/provisioning-an-amazon-ecs-cluster-using-terraform/session-page/#)
        
        ```plaintext
        terraform apply --auto-approve
        ```
        
        *Note*: The `terraform apply` process will take a few seconds to apply the changes to AWS, but the ECS Cluster may take up to 3 minutes to become accessible.
        
        The following message will display in the terminal, along with the predefined output variables:
        
        1. Open the ALB URL in a new browser tab to confirm the application is running
            
            <img src="https://assets.platform.qa.com/labs/uploads/provisioning-ecs-cluster-terraform/steps/4_deploy/assets/final-webpage.png" alt="" style="" />
            
            The application is a simple **API Results** webpage. Each time you refresh the page, the **frontend** ECS tasks retrieve a new set of data from the **backend** tasks, then display them in a table organized by **Record ID**
            
        
        ### **Summary**
        
        By completing this lab, you have accomplished the following tasks:
        
        * Defined a Terraform module that deploys Amazon ECS resources
            
        * Applied an Auto Scaling Group Policy to respond to ECS metrics
            
        * Deployed an Amazon ECS Cluster into an existing Amazon VPC using Terraform