diff --git a/apigw-rest-vpclink-pvt-alb-terraform/.gitignore b/apigw-rest-vpclink-pvt-alb-terraform/.gitignore new file mode 100644 index 000000000..e4abf1de0 --- /dev/null +++ b/apigw-rest-vpclink-pvt-alb-terraform/.gitignore @@ -0,0 +1,29 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files +*.tfvars +*.tfvars.json + +# Ignore override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include tfplan files +*tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# No additional exclusions needed diff --git a/apigw-rest-vpclink-pvt-alb-terraform/README.md b/apigw-rest-vpclink-pvt-alb-terraform/README.md new file mode 100644 index 000000000..6ef957b77 --- /dev/null +++ b/apigw-rest-vpclink-pvt-alb-terraform/README.md @@ -0,0 +1,105 @@ +# REST Amazon API Gateway to Private HTTP Endpoint via Amazon VPC Link V2 + +This pattern demonstrates direct integration between REST API Gateway and a private Application Load Balancer using VPC Link V2. Previously, connecting REST API Gateway to a private ALB required VPC Link V1 with an intermediary Network Load Balancer, adding complexity and cost. VPC Link V2 eliminates this requirement, enabling direct ALB integration for simplified architecture and reduced operational overhead. + +This Terraform template deploys a REST API Gateway with Amazon VPC Link V2 integration to a private Amazon Application Load Balancer and Amazon ECS Fargate cluster. + +### Prerequisites: +* An existing VPC with private subnets +* Private subnets must have internet access (via NAT Gateway) to pull container images from Docker Hub + +### Deployed resources: +* Security Groups for ALB and ECS tasks +* ECS Fargate cluster with service and task definitions +* Private Application Load Balancer with listener and target group +* Amazon VPC Link V2 connecting API Gateway to the private ALB +* REST API Gateway with proxy integration to the ALB + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigw-rest-vpclink-pvt-alb-terraform/ + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed and configured +* [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +2. Change directory to the pattern directory: + ``` + cd serverless-patterns/apigw-rest-vpclink-pvt-alb-terraform + ``` +3. Update the `terraform.tfvars` file with your VPC ID and private subnet IDs: + ```hcl + vpc_id = "vpc-xxxxxxxxx" + + private_subnets = [ + "subnet-xxxxxxxxx", + "subnet-yyyyyyyyy" + ] + ``` +4. Run the below command to initialize, download, and install the defined providers. In case you are not already familiar with the Terraform CLI, refer Terraform [documentation](https://www.terraform.io/cli/commands) to learn more about the various commands. + ``` + terraform init + ``` +5. Deploy the AWS resources for the pattern as specified in the `main.tf` file. Input variables are configured in `variables.tf`. But, there are different ways to pass variables to the CLI. + + Use the below command to review the changes before deploying. + ``` + terraform plan + ``` + Deploy: + ``` + terraform apply --auto-approve + ``` +6. Note the output from the Terraform deployment process. These contain the resource names and/or ARNs which are used for testing. + +## How it works + +This pattern demonstrates secure integration between a public REST API Gateway endpoint and a private Application Load Balancer with an ECS Fargate cluster. Traffic flows through Amazon VPC Link V2, which provides a secure, private connection from API Gateway to the internal ALB without exposing backend resources to the public internet. + +The integration uses the `--integration-target` parameter with AWS CLI (via Terraform null_resource) to properly configure the REST API Gateway with Amazon VPC Link V2, as this feature requires explicit ALB ARN specification. The ALB distributes traffic to ECS Fargate tasks running in private subnets. + +## Testing + +The stack outputs the REST API endpoint. Test it by accessing any path through the API: + +```bash +curl https:///index.html +``` + +You should see the nginx welcome page HTML. To check just the status code: + +```bash +curl -s -o /dev/null -w "%{http_code}" https:///index.html ; echo +``` + +Expected response: **200** + +## Cleanup + +1. Change to the below directory inside the cloned git repo: + ``` + cd serverless-patterns/apigw-rest-vpclink-pvt-alb-terraform + ``` +2. Delete the resources + ```bash + terraform destroy + ``` +3. Enter 'yes' when prompted. + +4. Check if all the resources were deleted successfully. + ```bash + terraform show + ``` +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-rest-vpclink-pvt-alb-terraform/example-pattern.json b/apigw-rest-vpclink-pvt-alb-terraform/example-pattern.json new file mode 100644 index 000000000..02e02f0a0 --- /dev/null +++ b/apigw-rest-vpclink-pvt-alb-terraform/example-pattern.json @@ -0,0 +1,62 @@ +{ + "title": "REST API Gateway to Private ALB and ECS Fargate via VPC Link V2", + "description": "This pattern demonstrates REST API Gateway integration with a private Application Load Balancer and ECS Fargate cluster using VPC Link V2", + "language": "HCL", + "level": "200", + "framework": "Terraform", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys a REST API Gateway endpoint that integrates with a private Application Load Balancer using VPC Link V2", + "The private ALB routes traffic to an ECS Fargate cluster running containerized applications", + "The pattern creates all required security groups, IAM roles, and networking components for secure private integration" + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-rest-vpclink-pvt-alb-terraform", + "templateURL": "serverless-patterns/apigw-rest-vpclink-pvt-alb-terraform", + "projectFolder": "apigw-rest-vpclink-pvt-alb-terraform", + "templateFile": "apigw-rest-vpclink-pvt-alb-terraform/main.tf" + } + }, + "resources": { + "bullets": [ + { + "text": "REST API private integration using VPC link", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-private-integration.html" + }, + { + "text": "Working with VPC links for REST APIs", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-vpc-link.html" + }, + { + "text": "Tutorial: Build a REST API with API Gateway private integration", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-with-private-integration.html" + } + ] + }, + "deploy": { + "text": [ + "terraform init && terraform apply" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "terraform destroy" + ] + }, + "authors": [ + { + "name": "Abhishek Agawane", + "image": "https://drive.google.com/file/d/1E-5koDaKEaMUtOctX32I9TLwfh3kgpAq/view?usp=drivesdk", + "bio": "Abhishek Agawane is a Security Consultant at Amazon Web Services with more than 8 years of industry experience. He helps organizations architect resilient, secure, and efficient cloud environments, guiding them through complex challenges and large-scale infrastructure transformations. He has helped numerous organizations enhance their cloud operations through targeted optimizations, robust architectures, and best-practice implementations.", + "linkedin": "agawabhi" + } + ] +} diff --git a/apigw-rest-vpclink-pvt-alb-terraform/main.tf b/apigw-rest-vpclink-pvt-alb-terraform/main.tf new file mode 100644 index 000000000..b06177a57 --- /dev/null +++ b/apigw-rest-vpclink-pvt-alb-terraform/main.tf @@ -0,0 +1,333 @@ +# Required providers configuration +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.80" + } + } + required_version = ">= 1.9" +} + +provider "aws" { + profile = "default" + region = var.aws_region +} + +############################################## +# PART 1: Private ALB with ECS Fargate Target +############################################## + +# ALB security group +resource "aws_security_group" "alb_sg" { + name = "rest-api-alb-sg" + description = "Security group for private ALB" + vpc_id = var.vpc_id + + ingress { + description = "Allow HTTP from anywhere" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "rest-api-alb-sg" + } +} + +# ALB egress rule to ECS +resource "aws_security_group_rule" "alb_to_ecs" { + type = "egress" + description = "Allow traffic to ECS tasks" + from_port = 80 + to_port = 80 + protocol = "tcp" + security_group_id = aws_security_group.alb_sg.id + source_security_group_id = aws_security_group.ecs_sg.id +} + +# ECS task security group +resource "aws_security_group" "ecs_sg" { + name = "rest-api-ecs-sg" + description = "Security group for ECS tasks" + vpc_id = var.vpc_id + + egress { + description = "Allow all outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "rest-api-ecs-sg" + } +} + +# ECS ingress rule from ALB +resource "aws_security_group_rule" "ecs_from_alb" { + type = "ingress" + description = "Allow traffic from ALB" + from_port = 80 + to_port = 80 + protocol = "tcp" + security_group_id = aws_security_group.ecs_sg.id + source_security_group_id = aws_security_group.alb_sg.id +} + +# ECS Cluster +resource "aws_ecs_cluster" "main" { + name = "rest-api-cluster" + + tags = { + Name = "rest-api-cluster" + } +} + +# ECS Task Execution Role +resource "aws_iam_role" "ecs_task_execution_role" { + name = "rest-api-ecs-task-execution-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy" { + role = aws_iam_role.ecs_task_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +# ECS Task Role +resource "aws_iam_role" "ecs_task_role" { + name = "rest-api-ecs-task-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) +} + +# ECS Task Definition +resource "aws_ecs_task_definition" "app" { + family = "rest-api-task" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = "512" + memory = "1024" + execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + task_role_arn = aws_iam_role.ecs_task_role.arn + + container_definitions = jsonencode([ + { + name = "web" + image = "nginx" + essential = true + portMappings = [ + { + containerPort = 80 + protocol = "tcp" + } + ] + } + ]) + + tags = { + Name = "rest-api-task" + } +} + +# ECS Service +resource "aws_ecs_service" "app" { + name = "rest-api-service" + cluster = aws_ecs_cluster.main.id + task_definition = aws_ecs_task_definition.app.arn + desired_count = 2 + deployment_maximum_percent = 200 + deployment_minimum_healthy_percent = 50 + enable_ecs_managed_tags = false + health_check_grace_period_seconds = 60 + launch_type = "FARGATE" + + network_configuration { + subnets = var.private_subnets + security_groups = [aws_security_group.ecs_sg.id] + } + + load_balancer { + target_group_arn = aws_lb_target_group.ecs_tg.arn + container_name = "web" + container_port = 80 + } + + depends_on = [aws_lb_target_group.ecs_tg, aws_lb_listener.http] + + tags = { + Name = "rest-api-service" + } +} + +# Create private Application Load Balancer +resource "aws_lb" "private_alb" { + name = "rest-api-private-alb" + internal = true + load_balancer_type = "application" + security_groups = [aws_security_group.alb_sg.id] + subnets = var.private_subnets + + tags = { + Name = "rest-api-private-alb" + } +} + +# Create target group for ECS +resource "aws_lb_target_group" "ecs_tg" { + name = "rest-api-ecs-tg" + port = 80 + protocol = "HTTP" + vpc_id = var.vpc_id + target_type = "ip" + + tags = { + Name = "rest-api-ecs-tg" + } +} + +# Create ALB listener +resource "aws_lb_listener" "http" { + load_balancer_arn = aws_lb.private_alb.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.ecs_tg.arn + } +} + +############################################## +# PART 2: VPC Link V2 +############################################## + +resource "aws_apigatewayv2_vpc_link" "vpclink" { + name = "rest-api-vpclink-v2" + security_group_ids = [] + subnet_ids = var.private_subnets + + tags = { + Name = "rest-api-vpclink-v2" + } +} + +############################################## +# PART 3: REST API with VPC Link Integration +############################################## + +# Create REST API +resource "aws_api_gateway_rest_api" "rest_api" { + name = "rest-api-vpclink-demo" + description = "REST API with VPC Link V2 to private ALB" + + endpoint_configuration { + types = ["REGIONAL"] + } +} + +# Create proxy resource +resource "aws_api_gateway_resource" "proxy" { + rest_api_id = aws_api_gateway_rest_api.rest_api.id + parent_id = aws_api_gateway_rest_api.rest_api.root_resource_id + path_part = "{proxy+}" +} + +# Create ANY method +resource "aws_api_gateway_method" "proxy_any" { + rest_api_id = aws_api_gateway_rest_api.rest_api.id + resource_id = aws_api_gateway_resource.proxy.id + http_method = "ANY" + authorization = "NONE" + + request_parameters = { + "method.request.path.proxy" = true + } +} + +# Create VPC Link integration with VPC Link V2 +# Note: REST API + VPC Link V2 integration requires using AWS CLI directly +# as Terraform AWS provider doesn't fully support the integration_target parameter yet +resource "null_resource" "vpclink_integration" { + triggers = { + rest_api_id = aws_api_gateway_rest_api.rest_api.id + resource_id = aws_api_gateway_resource.proxy.id + vpc_link_id = aws_apigatewayv2_vpc_link.vpclink.id + alb_arn = aws_lb.private_alb.arn + } + + provisioner "local-exec" { + command = <<-EOT + aws apigateway put-integration \ + --rest-api-id ${aws_api_gateway_rest_api.rest_api.id} \ + --resource-id ${aws_api_gateway_resource.proxy.id} \ + --http-method ANY \ + --type HTTP_PROXY \ + --integration-http-method ANY \ + --connection-type VPC_LINK \ + --connection-id ${aws_apigatewayv2_vpc_link.vpclink.id} \ + --integration-target ${aws_lb.private_alb.arn} \ + --uri http://${aws_lb.private_alb.dns_name}/{proxy} \ + --request-parameters '{"integration.request.path.proxy":"method.request.path.proxy"}' + EOT + } + + depends_on = [ + aws_apigatewayv2_vpc_link.vpclink, + aws_lb_listener.http, + aws_api_gateway_method.proxy_any + ] +} + +# Create deployment +resource "aws_api_gateway_deployment" "deployment" { + rest_api_id = aws_api_gateway_rest_api.rest_api.id + + triggers = { + redeployment = sha1(jsonencode([ + aws_api_gateway_resource.proxy.id, + aws_api_gateway_method.proxy_any.id, + null_resource.vpclink_integration.id, + ])) + } + + lifecycle { + create_before_destroy = true + } + + depends_on = [null_resource.vpclink_integration] +} + +# Create stage +resource "aws_api_gateway_stage" "prod" { + deployment_id = aws_api_gateway_deployment.deployment.id + rest_api_id = aws_api_gateway_rest_api.rest_api.id + stage_name = "prod" +} diff --git a/apigw-rest-vpclink-pvt-alb-terraform/outputs.tf b/apigw-rest-vpclink-pvt-alb-terraform/outputs.tf new file mode 100644 index 000000000..a093b6c21 --- /dev/null +++ b/apigw-rest-vpclink-pvt-alb-terraform/outputs.tf @@ -0,0 +1,24 @@ +output "rest_api_endpoint" { + value = aws_api_gateway_stage.prod.invoke_url + description = "REST API Gateway Endpoint URL" +} + +output "vpc_link_id" { + value = aws_apigatewayv2_vpc_link.vpclink.id + description = "VPC Link V2 ID" +} + +output "alb_dns_name" { + value = aws_lb.private_alb.dns_name + description = "Private ALB DNS name" +} + +output "ecs_cluster_name" { + value = aws_ecs_cluster.main.name + description = "ECS cluster name" +} + +output "ecs_service_name" { + value = aws_ecs_service.app.name + description = "ECS service name" +} diff --git a/apigw-rest-vpclink-pvt-alb-terraform/variables.tf b/apigw-rest-vpclink-pvt-alb-terraform/variables.tf new file mode 100644 index 000000000..50a46d5b8 --- /dev/null +++ b/apigw-rest-vpclink-pvt-alb-terraform/variables.tf @@ -0,0 +1,15 @@ +variable "aws_region" { + description = "AWS region" + type = string + default = "us-east-1" +} + +variable "vpc_id" { + description = "VPC ID where resources will be created" + type = string +} + +variable "private_subnets" { + description = "List of private subnet IDs" + type = list(string) +}