Create an API with a private integration to an AWS ECS service with Terraform (IaC)
DevOps4Me Global
Posted on February 13, 2023
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.
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
Then we run command to initial our Terraform modules:
terraform init
After that, we run validation and formating command below:
terraform validate && terraform fmt
Next, we run command to create Terraform Plan:
terraform plan -no-color > tfplan.txt
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
- Route Table
- Elastic Public IP
- Internet Gateway
- NAT Gateway
- ECS Cluster
- Fargate Task Definition
- AWS API Gateway
- 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.
The result we able to call our private services below:
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
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
February 13, 2023