# Provision an Amazon ECS Cluster Using Terraform

# **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](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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1742508422259/3f633785-b072-4433-9311-5df5b4ed8e61.png align="center")

## Demo Steps

*   Initiate Terraform by running the following command from the cloned directory path: `terrafrom init`
    
*   Reviewing the Terraform Variables file
    
    The [**variables.tf**](http://variables.tf) file defines the name, description, and expected data type for each variable referenced in the [**main.tf**](http://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**](http://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:
    
    ```plaintext
      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`](http://main.tf) file.
    
*   Open the main.tf file and paste below code at the end:
    
    ```plaintext
    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**](http://main.tf) file:
        
        ```plaintext
        # 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.
    
    ```plaintext
    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
            
            ![](https://assets.platform.qa.com/labs/uploads/provisioning-ecs-cluster-terraform/steps/4_deploy/assets/final-webpage.png align="left")
            
            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
    ```
