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..42d342c1a --- /dev/null +++ b/apigw-rest-vpclink-pvt-alb-terraform/README.md @@ -0,0 +1,96 @@ +# REST API Gateway to Private HTTP Endpoint via VPC Link V2 + +This Terraform template deploys a complete serverless integration pattern connecting a public REST API Gateway to a private ECS Fargate cluster via VPC Link V2. + +### Prerequisites: +* An existing VPC with private subnets +* Private subnets must have internet access (via NAT Gateway or Internet 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 +* 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 allows integration of public REST API Gateway endpoint to a private Application Load Balancer with an ECS Fargate cluster behind it. It allows to build a secure pattern without exposing the private subnet resources and can be accessed only via a VPC Link V2. + +The integration uses the `--integration-target` parameter with AWS CLI to properly configure the REST API Gateway with VPC Link V2, as this feature is not yet fully supported in the Terraform AWS provider. + +## Testing + +The stack creates and outputs the REST API endpoint. Open a browser and try out the generated API endpoint. You should see the Nginx home page. +Or, run the below command with the appropriate API endpoint. You should get a 200 response code. + +```bash +curl -s -o /dev/null -w "%{http_code}" ; echo +``` + +## 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 2021 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..d4140dd6a --- /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": "https://www.linkedin.com/in/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) +}