Create an API with a private integration to an AWS ECS service with Terraform (IaC)

devops4mecode

DevOps4Me Global

Posted on February 13, 2023

Create an API with a private integration to an AWS ECS service with Terraform (IaC)

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

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


Enter fullscreen mode Exit fullscreen mode

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



data "aws_availability_zones" "available_zones" {
  state = "available"
}


Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

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)
}


Enter fullscreen mode Exit fullscreen mode

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"]
  }
}


Enter fullscreen mode Exit fullscreen mode

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"
  }
}


Enter fullscreen mode Exit fullscreen mode

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]
}


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

Execute

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



aws configure


Enter fullscreen mode Exit fullscreen mode

aws configure

Then we run command to initial our Terraform modules:



terraform init


Enter fullscreen mode Exit fullscreen mode

terraform init

After that, we run validation and formating command below:



terraform validate && terraform fmt


Enter fullscreen mode Exit fullscreen mode

terraform validate && terraform fmt

Next, we run command to create Terraform Plan:



terraform plan -no-color > tfplan.txt


Enter fullscreen mode Exit fullscreen mode

terraform plan

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


Enter fullscreen mode Exit fullscreen mode

Output

  1. VPC
    VPC

  2. Subnets

subnets

  1. Route Table

Route Table

  1. Elastic Public IP

EIP

  1. Internet Gateway

IGW

  1. NAT Gateway

NAT GW

  1. ECS Cluster

ECS Cluster

  1. Fargate Task Definition

Fargate

  1. AWS API Gateway

API Gateway

  1. VPC PrivateLink

VPC PrivateLink

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

The result we able to call our private services below:

Result

Clean Up

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



terraform destroy -auto-approve

Enter fullscreen mode Exit fullscreen mode




Source Code

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

💖 💪 🙅 🚩
devops4mecode
DevOps4Me Global

Posted on February 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related