# Deploy ECS containers on AWS Graviton processor using Terraform

## In this learning path

- [Introduction](https://learn.arm.com/learning-paths/servers-and-cloud-computing/ecs/)
- [Deploy containers using ECS on AWS Graviton processors](https://learn.arm.com/learning-paths/servers-and-cloud-computing/ecs/deployment/)
- [Deploy ECS containers on AWS Graviton processor using Terraform](https://learn.arm.com/learning-paths/servers-and-cloud-computing/ecs/terraform/)
- [Next Steps](https://learn.arm.com/learning-paths/servers-and-cloud-computing/ecs/_next-steps/)

You can also automate the deployment of containerized applications on AWS ECS with Terraform.  
This section incrementally constructs a Terraform file named `main.tf` to automate the same steps for deployment of Nginx on ECS.

## Create a repository in Amazon ECR for container images

ECR is an AWS service for sharing and deploying container applications.  
To create a repository in ECR, use a text editor to create a file named `main.tf` with the contents below:

```hcl
# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.45.0"
    }
  }
}
resource "aws_ecr_repository" "app_ecr_repo" {
  name = "myapp"
}
```

Run the `terraform init` command. This should be the first command executed after creating a new Terraform configuration. It creates the Terraform configuration files in your working directory.  

```bash
terraform init
```

The message `Terraform has been successfully initialized!` will be displayed on your terminal:

```
__output__
Initializing the backend...
__output__

__output__
Initializing provider plugins...
__output__
- Finding hashicorp/aws versions matching "4.45.0"...
__output__
- Installing hashicorp/aws v4.45.0...
__output__
- Installed hashicorp/aws v4.45.0 (signed by HashiCorp)
__output__

__output__
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
__output__

__output__
Terraform has been successfully initialized!
__output__

__output__
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
__output__

If you want to create a repository in ECR, run the `plan` command. You will be able to preview the above Terraform configuration file and the resources that will be created:

```bash
terraform plan
```

Terraform `plan` displays the resources that will be added, changed, or deployed to AWS. In this example, one resource `aws_ecr_repository.app_ecr_repo` will be added to AWS.

```
__output__
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
__output__
symbols:
  + create
__output__

Terraform will perform the following actions:

  # aws_ecr_repository.app_ecr_repo will be created
  + resource "aws_ecr_repository" "app_ecr_repo" {
      + arn                  = (known after apply)
      + id                   = (known after apply)
      + image_tag_mutability = "MUTABLE"
      + name                 = "myapp"
      + registry_id          = (known after apply)
      + repository_url       = (known after apply)
      + tags_all             = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
```

To provision the displayed infrastructure on AWS, run the `apply` command:

```bash
terraform apply
```

Enter yes when prompted to allow Terraform to execute the command.

```
__output__
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
__output__
symbols:
  + create
__output__

Terraform will perform the following actions:

  # aws_ecr_repository.app_ecr_repo will be created
  + resource "aws_ecr_repository" "app_ecr_repo" {
      + arn                  = (known after apply)
      + id                   = (known after apply)
      + image_tag_mutability = "MUTABLE"
      + name                 = "my-app-terraform"
      + registry_id          = (known after apply)
      + repository_url       = (known after apply)
      + tags_all             = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
__output__
aws_ecr_repository.app_ecr_repo: Creating...
__output__
aws_ecr_repository.app_ecr_repo: Creation complete after 0s [id=myapp]
__output__
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

Terraform has created the repository in ECR. You can confirm this on your Amazon Elastic Container Registry Repositories list.

![Image Alt Text:ecs33](https://learn.arm.com/learning-paths/servers-and-cloud-computing/ecs/./images/ecs33.png)

Push the container image to the new repository using `docker push` as in the previous section.

```bash
docker push [your account number].dkr.ecr.us-east-2.amazonaws.com/myapp
```

Refresh the repository’s page to verify you’ve successfully pushed the image to the Amazon ECR repository.

![Image Alt Text:ecs34](https://learn.arm.com/learning-paths/servers-and-cloud-computing/ecs/./images/ecs34.png)

## Create an ECS Cluster

You have created a repository and put a container image into the repository.  
Next, create a cluster to act as a target to run containers. Clusters run ECS tasks which specify the containers to create.  

To create a cluster to run your tasks, use a text editor to add the following configurations to your main.tf file:

```hcl
# main.tf
resource "aws_ecs_cluster" "my_cluster" {
  name = "app-cluster" # Name your cluster here
}
```

This instructs ECS to create a new cluster named `app-cluster`.  
Re-run the `apply` command to add these changes:

```bash
terraform apply
```

Navigate to Amazon ECS Clusters and verify that you can see the changes:

![Image Alt Text:ecs35](https://learn.arm.com/learning-paths/servers-and-cloud-computing/ecs/./images/ecs35.png)

## Configure an Amazon ECS task

To run the image, you can launch it as an ECS container.  
To deploy the image to ECS, you can create a task. A task tells ECS how you want to run your Docker container. A task describes the container’s critical specifications of how to launch your container.

These specifications include:

- Port mappings
- Application image
- CPU and RAM resources
- Container launch types such as EC2 or Fargate

Fargate is an AWS orchestration tool. It allows you to give AWS the role of managing your container life cycle and the hosting infrastructure. Fargate is serverless, which means you don’t need to provision your container using EC2 instances (virtual machines).  
You can use Fargate to run your container on ECS without provisioning a virtual machine on AWS.

Add below configuration in your `main.tf` file:

```hcl
# main.tf
resource "aws_ecs_task_definition" "app_task" {
  family                   = "app-first-task" # Name your task
  container_definitions    = <<DEFINITION
  [
    {
      "name": "app-first-task",
      "image": "${aws_ecr_repository.app_ecr_repo.repository_url}",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80
        }
      ],
      "memory": 512,
      "cpu": 256
    }
  ]
  DEFINITION
  requires_compatibilities = ["FARGATE"] # use Fargate as the launch type
  network_mode             = "awsvpc"    # add the AWS VPN network mode as this is required for Fargate
  memory                   = 512         # Specify the memory the container requires
  cpu                      = 256         # Specify the CPU the container requires
  execution_role_arn       = aws_iam_role.ecsTaskExecutionRole.arn
}
```

> Change `containerPort`, `hostPort`, `memory` & `cpu` as per your requirement.

As described in the above config block, Terraform will create a task named `app-first-task` and assign the resources needed to run the container using this task. This process includes assigning the deployed image, container ports, launch type, and the hardware requirements that the container needs to run.

Creating a task definition requires `ecsTaskExecutionRole` to be added to your IAM. The above task definition needs this role, and it is specified as `aws_iam_role.ecsTaskExecutionRole.arn`.  
Create a resource to execute this role as follows:

```hcl
# main.tf
resource "aws_iam_role" "ecsTaskExecutionRole" {
  name               = "ecsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}

data "aws_iam_policy_document" "assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

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

Run the `apply` command to add these changes to AWS.

```bash
terraform apply
```

Navigate to Amazon ECS Task Definitions and you can see the task definitions:

![Image Alt Text:ecs36](https://learn.arm.com/learning-paths/servers-and-cloud-computing/ecs/./images/ecs36.png)

## Launch the Container

You now have most of the configurations needed. You need to connect all the above-created specifications together to launch your container successfully.

### Create a VPC

You need to create a Virtual Private Cloud Module (VPC) and subnet to launch your cluster into. VPC and subnet allow you to connect to the internet, communicate with ECS, and expose the application to available zones.  
Create a default VPC and a subnet for your AWS availability zones by adding the resources below to your `main.tf` file:

```hcl
# main.tf
# Provide a reference to your default VPC
resource "aws_default_vpc" "default_vpc" {
}

# Provide references to your default subnets
resource "aws_default_subnet" "default_subnet_a" {
  # Use your own region here but reference to subnet 1a
  availability_zone = "us-east-2a"
}

resource "aws_default_subnet" "default_subnet_b" {
  # Use your own region here but reference to subnet 1b
  availability_zone = "us-east-2b"
}
```

### Implement a Load Balancer

Create a security group that will route HTTP traffic using a load balancer.  
Add a load balancer by adding the lines below to `main.tf`:

```hcl
# main.tf
resource "aws_alb" "application_load_balancer" {
  name               = "load-balancer-dev" #load balancer name
  load_balancer_type = "application"
  subnets = [ # Referencing the default subnets
    "${aws_default_subnet.default_subnet_a.id}",
    "${aws_default_subnet.default_subnet_b.id}"
  ]
  # security group
  security_groups = ["${aws_security_group.load_balancer_security_group.id}"]
}
```

The configuration creates a load balancer that will distribute the workloads across multiple resources to ensure application’s availability, scalability, and security.

### Create a security group for the load balancer

A security group allows HTTP traffic to access the ECS cluster. This is required for access to the application.  
Add the security group for the load balancer as by adding the lines below to `main.tf`:

```hcl
# main.tf
# Create a security group for the load balancer:
resource "aws_security_group" "load_balancer_security_group" {
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # Allow traffic in from all sources
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

Configure the load balancer with the VPC you created earlier by adding the lines below to `main.tf`:

```hcl
# main.tf
resource "aws_lb_target_group" "target_group" {
  name        = "target-group"
  port        = 80
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = aws_default_vpc.default_vpc.id # default VPC
}

resource "aws_lb_listener" "listener" {
  load_balancer_arn = aws_alb.application_load_balancer.arn #  load balancer
  port              = "80"
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.target_group.arn # target group
  }
}
```

### Create an ECS Service

The last step is to create an ECS Service to maintain the task definition in the ECS cluster.  
The service will run the cluster, task, and Fargate containers behind the created load balancer and distribute traffic across the containers that are associated with the service.

You can add the service by adding the following to your `main.tf`:

```hcl
# main.tf
resource "aws_ecs_service" "app_service" {
  name            = "app-first-service"                  # Name the service
  cluster         = aws_ecs_cluster.my_cluster.id        # Reference the created Cluster
  task_definition = aws_ecs_task_definition.app_task.arn # Reference the task that the service will spin up
  launch_type     = "FARGATE"
  desired_count   = 3 # Set up the number of containers to 3

  load_balancer {
    target_group_arn = aws_lb_target_group.target_group.arn # Reference the target group
    container_name   = aws_ecs_task_definition.app_task.family
    container_port   = 80 # Specify the container port
  }

  network_configuration {
    subnets          = ["${aws_default_subnet.default_subnet_a.id}", "${aws_default_subnet.default_subnet_b.id}"]
    assign_public_ip = true                                                # Provide the containers with public IPs
    security_groups  = ["${aws_security_group.service_security_group.id}"] # Set up the security group
  }
}
```

To access the ECS service over HTTP while ensuring the VPC is secure, create security groups that will only allow the traffic from the created load balancer.  
Create a `aws_security_group.service_security_group` resource as follows in `main.tf`:

```hcl
# main.tf
resource "aws_security_group" "service_security_group" {
  ingress {
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    security_groups = ["${aws_security_group.load_balancer_security_group.id}"] # Only allowing traffic in from the load balancer security group
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

Add an output configuration to `main.tf` that will extract the load balancer URL value from the state file and print it to the terminal.

```hcl
# main.tf
# Log the load balancer app URL
output "app_url" {
  value = aws_alb.application_load_balancer.dns_name
}
```

You are now ready to create and provision all infrastructure on AWS.  
Run the `validate` command to detect syntax errors, version errors, and other problems associated with your `main.tf` module.

```bash
terraform validate
```

Run the `plan` command to inspect what Terraform will perform and the resources it will create.

```bash
terraform plan
```

Run the `apply` command to provision infrastructure on AWS. Remember to enter yes when prompted to do so.

```bash
terraform apply
```

When the `apply` command is complete you will see the application’s URL on your terminal.

```
__output__
Plan: 9 to add, 0 to change, 0 to destroy.
__output__

Changes to Outputs:
  + app_url = (known after apply)
__output__

aws_default_subnet.default_subnet_b: Creating...
__output__

aws_default_subnet.default_subnet_a: Creating...
__output__

aws_default_vpc.default_vpc: Creating...
__output__

aws_security_group.load_balancer_security_group: Creating...
__output__

aws_default_subnet.default_subnet_b: Creation complete after 0s [id=subnet-59ed1524]
__output__

aws_default_subnet.default_subnet_a: Creation complete after 0s [id=subnet-9dee6cf6]
__output__

aws_default_vpc.default_vpc: Creation complete after 0s [id=vpc-a0d44ecb]
__output__

aws_lb_target_group.target_group: Creating...
__output__

aws_security_group.load_balancer_security_group: Creation complete after 1s [id=sg-063cda74d5b146ee8]
__output__

aws_alb.application_load_balancer: Creating...
__output__

aws_security_group.service_security_group: Creating...
__output__

aws_security_group.service_security_group: Creation complete after 2s [id=sg-023c421dcd0a6e502]
__output__

aws_ecs_service.app_service: Creating...
__output__

aws_alb.application_load_balancer: Still creating... [10s elapsed]
__output__

aws_ecs_service.app_service: Still creating... [10s elapsed]
__output__

aws_alb.application_load_balancer: Still creating... [20s elapsed]
__output__

aws_ecs_service.app_service: Still creating... [20s elapsed]
__output__

aws_alb.application_load_balancer: Still creating... [30s elapsed]
__output__

aws_ecs_service.app_service: Still creating... [30s elapsed]
__output__

aws_alb.application_load_balancer: Still creating... [40s elapsed]
__output__

aws_ecs_service.app_service: Still creating... [40s elapsed]
__output__

aws_alb.application_load_balancer: Still creating... [50s elapsed]
__output__

aws_ecs_service.app_service: Still creating... [50s elapsed]
__output__

aws_alb.application_load_balancer: Still creating... [1m0s elapsed]
__output__

aws_ecs_service.app_service: Still creating... [1m0s elapsed]
__output__

aws_alb.application_load_balancer: Still creating... [1m10s elapsed]
__output__

aws_alb.application_load_balancer: Still creating... [1m20s elapsed]
__output__

aws_ecs_service.app_service: Still creating... [1m10s elapsed]
__output__

aws_ecs_service.app_service: Still creating... [1m20s elapsed]
__output__

aws_ecs_service.app_service: Still creating... [1m30s elapsed]
__output__

aws_ecs_service.app_service: Creation complete after 2m19s [id=arn:aws:ecs:us-east-2:173141235168:service/app-cluster/app-first-service]
__output__

Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
```

You can also access the URL from your `load-balancer-dev` as the DNS name.  
Copy the URL to your browser.  
You will see NGINX running:

![Image Alt Text:ecs37](https://learn.arm.com/learning-paths/servers-and-cloud-computing/ecs/./images/ecs37.png)

> If you see a `503 Service Temporarily Unavailable` immediately after running the `terraform apply` command you should wait a few seconds for all infrastructure to be created and try again.

To destroy the infrastructure and avoid additional cost, run the `destroy` command:

```bash
terraform destroy
```
