Create an API with a private integration to an AWS ECS service with Terraform (IaC)
Tags: devops, automation, infrastructureascode, ecs
Published: 2023-02-13 15:30:28

## Introduction

You may connect Amazon API Gateway API routes to VPC-restricted resources using VPC links. A VPC connection is an abstraction layer on top of other networking resources and functions like any other integration endpoint for an application programming interface (API). This makes it easier to set up secure connections.

## Use-case

We want to setup a private AWS API Gateway to our backend service that used AWS serverless service such a Fargate and it's deployed in AWS ECS Cluster. The architecture below that we want to create in this blog post.

![Use-Case Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9talmb0jt1q4m3x0b68g.png)

## Steps

**Terraform**
We need to create our main Terraform file(**main.tf**); proceed to execute below command in your prefer directory.

```
mkdir automation && cd automation
vi main.tf
```

On the top we set our AWS Availability Zone (AZ):

```
data "aws_availability_zones" "available_zones" {
  state = "available"
}
```
We also need to create our versions.tf file as below:

```
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}
provider "aws" {
  region = "ap-southeast-1"
  default_tags {
    tags = {
      Name = "do4m-demo"
    }
  }
}
```
Then, we need output.tf file to get the 
Lastly, for our prerequisites step, we create variables.tf file for all required Fargate application count we neeed.

```
variable "app_count" {
  type    = number
  default = 1
}
```
**VPC Setup**
First, you use a Terraform create a Amazon VPC which includig VPC subnets(Private & Public), Internet Gateway, NAT Gateway for Private subnets and route table for subnets association.

```
#VPC Setting
resource "aws_vpc" "default" {
  cidr_block = "10.32.0.0/16"
}

resource "aws_subnet" "public" {
  count                   = 2
  cidr_block              = cidrsubnet(aws_vpc.default.cidr_block, 8, 2 + count.index)
  availability_zone       = data.aws_availability_zones.available_zones.names[count.index]
  vpc_id                  = aws_vpc.default.id
  map_public_ip_on_launch = true
}

resource "aws_subnet" "private" {
  count             = 2
  cidr_block        = cidrsubnet(aws_vpc.default.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available_zones.names[count.index]
  vpc_id            = aws_vpc.default.id
}

resource "aws_internet_gateway" "gateway" {
  vpc_id = aws_vpc.default.id
}

resource "aws_route" "internet_access" {
  route_table_id         = aws_vpc.default.main_route_table_id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.gateway.id
}

resource "aws_eip" "gateway" {
  count      = 2
  vpc        = true
  depends_on = [aws_internet_gateway.gateway]
}

resource "aws_nat_gateway" "gateway" {
  count         = 2
  subnet_id     = element(aws_subnet.public.*.id, count.index)
  allocation_id = element(aws_eip.gateway.*.id, count.index)
}

resource "aws_route_table" "private" {
  count  = 2
  vpc_id = aws_vpc.default.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = element(aws_nat_gateway.gateway.*.id, count.index)
  }
}

resource "aws_route_table_association" "private" {
  count          = 2
  subnet_id      = element(aws_subnet.private.*.id, count.index)
  route_table_id = element(aws_route_table.private.*.id, count.index)
}
```

**Security Group**

```
resource "aws_security_group" "lb" {
  name   = "do4m-alb-sg"
  vpc_id = aws_vpc.default.id

  ingress {
    protocol    = "tcp"
    from_port   = 80
    to_port     = 80
    cidr_blocks = ["0.0.0.0/0"]
  }

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

**AWS ALB Setting**

```
resource "aws_lb" "default" {
  name            = "do4m-lb"
  subnets         = aws_subnet.public.*.id
  security_groups = [aws_security_group.lb.id]
}

resource "aws_lb_target_group" "hello_world" {
  name        = "do4m-target-group"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.default.id
  target_type = "ip"
}

resource "aws_lb_listener" "hello_world" {
  load_balancer_arn = aws_lb.default.id
  port              = "80"
  protocol          = "HTTP"

  default_action {
    target_group_arn = aws_lb_target_group.hello_world.id
    type             = "forward"
  }
}
```

**AWS ECS and Fargate Setting**

```
resource "aws_ecs_task_definition" "hello_world" {
  family                   = "hello-world-app"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 1024
  memory                   = 2048

  container_definitions = <<DEFINITION
[
  {
    "image": "registry.gitlab.com/architect-io/artifacts/nodejs-hello-world:latest",
    "cpu": 1024,
    "memory": 2048,
    "name": "hello-world-app",
    "networkMode": "awsvpc",
    "portMappings": [
      {
        "containerPort": 3000,
        "hostPort": 3000
      }
    ]
  }
]
DEFINITION
}

resource "aws_security_group" "hello_world_task" {
  name   = "do4m-task-sg"
  vpc_id = aws_vpc.default.id

  ingress {
    protocol        = "tcp"
    from_port       = 3000
    to_port         = 3000
    security_groups = [aws_security_group.lb.id]
  }

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

resource "aws_ecs_cluster" "main" {
  name = "do4m-cluster"
}

resource "aws_ecs_service" "hello_world" {
  name            = "hello-world-service"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.hello_world.arn
  desired_count   = var.app_count
  launch_type     = "FARGATE"

  network_configuration {
    security_groups = [aws_security_group.hello_world_task.id]
    subnets         = aws_subnet.private.*.id
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.hello_world.id
    container_name   = "hello-world-app"
    container_port   = 3000
  }

  depends_on = [aws_lb_listener.hello_world]
}
```

**AWS API Gateway and VPC PrivateLink Setting**

```
#1: API Gateway
resource "aws_apigatewayv2_api" "api" {
  name          = "do4m-api-gateway"
  protocol_type = "HTTP"
}
#2: VPC Link
resource "aws_apigatewayv2_vpc_link" "vpc_link" {
  name               = "development-vpclink"
  security_group_ids = [aws_security_group.lb.id]
  subnet_ids         = aws_subnet.private.*.id
}
#3: API Integration
resource "aws_apigatewayv2_integration" "api_integration" {
  api_id             = aws_apigatewayv2_api.api.id
  integration_type   = "HTTP_PROXY"
  connection_id      = aws_apigatewayv2_vpc_link.vpc_link.id
  connection_type    = "VPC_LINK"
  description        = "VPC integration"
  integration_method = "ANY"
  integration_uri    = aws_lb_listener.hello_world.arn
  depends_on         = [aws_lb.default]
}
#4: APIGW Route
resource "aws_apigatewayv2_route" "default_route" {
  api_id    = aws_apigatewayv2_api.api.id
  route_key = "$default"
  target    = "integrations/${aws_apigatewayv2_integration.api_integration.id}"
}
#5: APIGW Stage
resource "aws_apigatewayv2_stage" "default_stage" {
  api_id      = aws_apigatewayv2_api.api.id
  name        = "$default"
  auto_deploy = true
}
```

**Execute**

First, we need to configure AWS account before we can run Terraform:

```
aws configure
```

![aws configure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ub0ac2crf59gijenryoj.png)

Then we run command to initial our Terraform modules:

```
terraform init
```

![terraform init](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yl4ca2ksbryr58n1vk38.png)

After that, we run validation and formating command below:

```
terraform validate && terraform fmt
```

![terraform validate && terraform fmt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b5d9zss53rd3y2bwxobi.png)

Next, we run command to create Terraform Plan:

```
terraform plan -no-color > tfplan.txt
```

![terraform plan](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b3n88f4d23yiue8actxg.png)

Lastly, once we confirmed and validated our plan, we can execute Terraform Apply command to create all the AWS resources/services we set above:

```
terraform apply -auto-approve
```

**Output**

1. VPC
![VPC](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xr0icfejnw88i8kxqzs4.png)

2. Subnets

![subnets](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/szn4fpt39wjblxuukezn.png)

3. Route Table

![Route Table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5s9s6p223m6ca5b409db.png)

4. Elastic Public IP

![EIP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o5uanxsbyfznbush82hj.png)

5. Internet Gateway

![IGW](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17m9lf4owhgeany3pslg.png)

6. NAT Gateway

![NAT GW](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8z3j3us6pzteflmnmtio.png)

7. ECS Cluster

![ECS Cluster](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/thqv4y0uh1kc0bdfm6ie.png)

8. Fargate Task Definition

![Fargate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/drz3ym9rhv8c47lbsgbj.png)

9. AWS API Gateway

![API Gateway](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/htdhruscdkt47dgodecx.png)

10. VPC PrivateLink

![VPC PrivateLink](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o5of4ujrub902iay250f.png)

**API Testing**

Once your API has been created, you must test it to ensure it is functioning properly. Invoking your API from a web browser will save time and effort. In order to put your API through its paces
To use the API Gateway, log in to the console via https://console.aws.amazon.com/apigateway . Choose your API and you need to invoke URL.

![Invoke API](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4dhgrxmt8pe5ksuwangt.png)

The result we able to call our private services below:

![Result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ubsbdltqz3a9e4444cc4.png)

## Clean Up
To clean/remove all AWS resources we created in this post, we run Terraform destroy below:

```
terraform destroy -auto-approve
```

## Source Code
You can refer for full source via this link: https://github.com/devops4mecode/ecs-fargate-vpclink-apigw