API Gateway to Private HTTP Endpoint via VPC Link

Sign Up to Build

About this Architecture

Here is some information about this architecture.

How to Build This Solution

Here are the steps you can follow to build this solution on your own.

This course teaches you how to establish a secure connection between an API Gateway and a private HTTP endpoint using a VPC Link. By utilizing Amazon VPC, you can privately access resources without exposing them to the public Internet. This project provides valuable insights into the security mechanisms of AWS and demonstrates how to tightly control access to your applications.

Get Your AWS Credentials

If you're using the Skillmix Labs feature, open the lab settings (the beaker icon) on the right side of the code editor. Then, click the Start Lab button to start hte lab environment.

Wait for the credentials to load. Then run this in the terminal.

Be sure to enter in your own access key and secret key and name your profile 'smx-lab'.

$ aws configure --profile smx-lab
AWS Access Key ID [None]: 
AWS Secret Access Key [None]: 
Default region name [None]: us-west-2
Default output format [None]: 

Note: If you're using your own AWS account you'll need to ensure that you've created and configured a named AWS CLI profile named smx-lab.

Create the Terraform Config

First we'll create the terraform config. This config is used to specify the providers that are required for the Terraform configuration. In this case, we are specifying that we require the AWS provider from HashiCorp with a version constraint of ~> 4.0.0.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0.0"
    }
  }
  required_version = ">= 1.0.11"
}

Create the aws Provider Config

Next, we'll create the aws config which is used to configure the AWS provider in Terraform. This config specifies the AWS profile to use, which is 'smx-lab', and the region to operate in, which is 'us-west-2'.

provider "aws" {
  profile = "smx-lab"
  region  = "us-west-2"
}

Create the vpc_id Variable

Next, we'll create the vpc_id config. This config is used to specify the ID of the VPC that will be used in our Terraform project.

variable "vpc_id" {
  type    = string
  default = "vpc-123a45b6"
}

Create the private_subnets Variable

Next, we'll create the private_subnets config. This config is used to define a list of private subnets.

variable "private_subnets" {
  type    = list(any)
  default = ["subnet-5432bca1", "subnet-1abc2345"]
}

Create the aws_security_group Resource

Next, we'll create the aws_security_group config. This config is used to define a security group for a load balancer.

resource "aws_security_group" "lb_security_group" {
  description = "LoadBalancer Security Group"
  vpc_id      = "${var.vpc_id}"

  ingress {
    description = "Allow from anyone on port 80"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Create the aws_security_group_rule Resource

Next, we'll create the aws_security_group_rule config. This config is used to define a security group rule that allows incoming traffic on port 80 from any IP address.

resource "aws_security_group_rule" "sg_ingress_rule_all_to_lb" {
  type              = "ingress"
  description       = "Allow from anyone on port 80"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  ipv6_cidr_blocks  = ["::/0"]
  security_group_id = "${aws_security_group.lb_security_group.id}"
}

Create the aws_security_group_rule Resource

Next, we'll create the aws_security_group_rule config. This config is used to define a security group rule for egress traffic from a load balancer to an ECS cluster.

resource "aws_security_group_rule" "sg_egress_rule_lb_to_ecs_cluster" {
  type                   = "egress"
  description            = "Target group egress"
  from_port              = 80
  to_port                = 80
  protocol               = "tcp"
  security_group_id      = "${aws_security_group.lb_security_group.id}"
  source_security_group_id = "${aws_security_group.ecs_security_group.id}"
}

Create the aws_security_group Resource

Next, we'll create the aws_security_group config for the ECS Security Group. This config is used to define the security group settings for the Amazon Elastic Container Service (ECS) instances.

resource "aws_security_group" "ecs_security_group" {
  description = "ECS Security Group"
  vpc_id      = "${var.vpc_id}"

  egress {
    description      = "Allow all outbound traffic by default"
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
  }
}

Create the aws_security_group_rule Resource

Next, we'll create the aws_security_group_rule config. This config is used to define an ingress rule for a security group. In this case, the rule allows incoming traffic on port 80 from a load balancer to an ECS cluster.

resource "aws_security_group_rule" "sg_ingress_rule_ecs_cluster_from_lb" {
  type                     = "ingress"
  description              = "Ingress from Load Balancer"
  from_port                = 80
  to_port                  = 80
  protocol                 = "tcp"
  security_group_id        = "${aws_security_group.ecs_security_group.id}"
  source_security_group_id = "${aws_security_group.lb_security_group.id}"
}

Create the aws_lb Resource

Next, we'll create the aws_lb config. This config is used to define an Application Load Balancer (ALB) in AWS.

resource "aws_lb" "ecs_alb" {
  load_balancer_type = "application"
  internal           = true
  subnets            = "${var.private_subnets}"
  security_groups    = ["${aws_security_group.lb_security_group.id}"]
}

Create the aws_lb_target_group Resource

Next, we'll create the aws_lb_target_group config. This config is used to define a target group for an Application Load Balancer (ALB) in AWS. The target group is responsible for routing incoming traffic to registered targets, which can be IP addresses or instances.

resource "aws_lb_target_group" "alb_ecs_tg" {
  port        = 80
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = "${var.vpc_id}"
}

Create the aws_lb_listener Resource

Next, we'll create the aws_lb_listener config. This config is used to define a listener for an Application Load Balancer (ALB) in AWS. The listener is responsible for routing incoming traffic to the appropriate target group based on the specified rules.

resource "aws_lb_listener" "ecs_alb_listener" {
  load_balancer_arn = "${aws_lb.ecs_alb.arn}"
  port              = "80"
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = "${aws_lb_target_group.alb_ecs_tg.arn}"
  }
}

Create the aws_ecs_cluster Resource

Next, we'll create the aws_ecs_cluster config. This config is used to define an Amazon ECS cluster. In this example, we are creating a cluster with the name 'demo-ecs-cluster'.

resource "aws_ecs_cluster" "ecs_cluster" {
  name = "demo-ecs-cluster"
}

Create the aws_ecs_service Resource

Next, we'll create the aws_ecs_service config. This config is used to define an ECS service in AWS.

resource "aws_ecs_service" "demo-ecs-service" {
  name                            = "demo-ecs-svc"
  cluster                         = "${aws_ecs_cluster.ecs_cluster.id}"
  task_definition                 = "${aws_ecs_task_definition.ecs_taskdef.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"
  depends_on                      = ["${aws_lb_target_group.alb_ecs_tg}", "${aws_lb_listener.ecs_alb_listener}"]

  load_balancer {
    target_group_arn  = "${aws_lb_target_group.alb_ecs_tg.arn}"
    container_name    = "web"
    container_port    = 80
  }

  network_configuration {
    security_groups = ["${aws_security_group.ecs_security_group.id}"]
    subnets         = "${var.private_subnets}"
  }
}

Create the aws_ecs_task_definition Resource

Next, we'll create the aws_ecs_task_definition config. This config is used to define the task definition for an Amazon Elastic Container Service (ECS) task.

resource "aws_ecs_task_definition" "ecs_taskdef" {
  family                   = "service"
  container_definitions    = "${jsonencode([{'name': 'web', 'image': 'nginx', 'essential': true, 'portMappings': [{'containerPort': 80, 'protocol': 'tcp'}]}])}"
  cpu                      = 512
  memory                   = 1024
  execution_role_arn       = "${aws_iam_role.ecs_task_exec_role.arn}"
  task_role_arn            = "${aws_iam_role.ecs_task_role.arn}"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
}

This config creates an ECS task definition with the following properties: - family: The name of the task definition family. - container_definitions: A JSON-encoded string that defines the container(s) to be launched as part of the task. - cpu: The number of CPU units to reserve for the task. - memory: The amount of memory (in MiB) to allocate to the task. - execution_role_arn: The ARN of the IAM role that allows the ECS service to make calls to other AWS services on your behalf. - task_role_arn: The ARN of the IAM role that provides permissions for the containers in the task. - requires_compatibilities: The launch type required by the task. - network_mode: The network mode to use for the task.

This config is essential for defining and launching ECS tasks in AWS.

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to create an AWS IAM role called 'ecs_task_exec_role'. The role is used to allow ECS tasks to assume this role and perform actions on your behalf.

resource "aws_iam_role" "ecs_task_exec_role" {
  name = "ecs_task_exec_role"
  assume_role_policy = "${jsonencode({
    'Version': '2012-10-17',
    'Statement': [
      {
        'Action': 'sts:AssumeRole',
        'Effect': 'Allow',
        'Principal': {
          'Service': 'ecs-tasks.amazonaws.com'
        }
      }
    ]
  })}"
}

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to define an IAM role named 'ecs_task_role' that can be assumed by the 'ecs-tasks.amazonaws.com' service. The role is created with an assume role policy that allows the 'ecs-tasks.amazonaws.com' service to assume the role.

resource "aws_iam_role" "ecs_task_role" {
  name = "ecs_task_role"
  assume_role_policy = "${jsonencode({
    'Version': '2012-10-17',
    'Statement': [
      {
        'Action': 'sts:AssumeRole',
        'Effect': 'Allow',
        'Principal': {
          'Service': 'ecs-tasks.amazonaws.com'
        }
      }
    ]
  })}"
}

Create the aws_apigatewayv2_vpc_link Resource

Next, we'll create the aws_apigatewayv2_vpc_link config. This config is used to create a VPC link in AWS API Gateway version 2.

resource "aws_apigatewayv2_vpc_link" "vpclink_apigw_to_alb" {
  name                = "vpclink_apigw_to_alb"
  security_group_ids  = []
  subnet_ids          = "${var.private_subnets}"
}

Create the aws_apigatewayv2_api Resource

Next, we'll create the aws_apigatewayv2_api config. This config is used to define an API Gateway v2 HTTP endpoint with the name serverlessland-pvt-endpoint and the protocol type HTTP.

resource "aws_apigatewayv2_api" "apigw_http_endpoint" {
  name           = "serverlessland-pvt-endpoint"
  protocol_type  = "HTTP"
}

Create the aws_apigatewayv2_integration Resource

Next, we'll create the aws_apigatewayv2_integration config. This config is used to define an integration between an API Gateway and another service, in this case, an ALB (Application Load Balancer).

resource "aws_apigatewayv2_integration" "apigw_integration" {
  api_id                  = "${aws_apigatewayv2_api.apigw_http_endpoint.id}"
  integration_type        = "HTTP_PROXY"
  integration_uri         = "${aws_lb_listener.ecs_alb_listener.arn}"
  integration_method      = "ANY"
  connection_type         = "VPC_LINK"
  connection_id           = "${aws_apigatewayv2_vpc_link.vpclink_apigw_to_alb.id}"
  payload_format_version  = "1.0"
  depends_on              = [
    "${aws_apigatewayv2_vpc_link.vpclink_apigw_to_alb}",
    "${aws_apigatewayv2_api.apigw_http_endpoint}",
    "${aws_lb_listener.ecs_alb_listener}"
  ]
}

Create the aws_apigatewayv2_route Resource

Next, we'll create the aws_apigatewayv2_route config. This config is used to define a route in the AWS API Gateway version 2. It specifies the API ID, route key, target integration, and any dependencies.

resource "aws_apigatewayv2_route" "apigw_route" {
  api_id      = "${aws_apigatewayv2_api.apigw_http_endpoint.id}"
  route_key   = "ANY /{proxy+}"
  target      = "integrations/${aws_apigatewayv2_integration.apigw_integration.id}"
  depends_on  = ["${aws_apigatewayv2_integration.apigw_integration}"]
}

Create the aws_apigatewayv2_stage Resource

Next, we'll create the aws_apigatewayv2_stage config. This config is used to define a stage for an API Gateway version 2 (APIGWv2) HTTP endpoint.

resource "aws_apigatewayv2_stage" "apigw_stage" {
  api_id       = "${aws_apigatewayv2_api.apigw_http_endpoint.id}"
  name         = "$default"
  auto_deploy  = true
  depends_on   = ["${aws_apigatewayv2_api.apigw_http_endpoint}"]
}

Create the apigw_endpoint Output

Next, we'll create the apigw_endpoint config. This config is used to define the output value for the API Gateway Endpoint.

# Generated API GW endpoint URL that can be used to access the application running on a private ECS Fargate cluster.
output "apigw_endpoint" {
  value = aws_apigatewayv2_api.apigw_http_endpoint.api_endpoint
    description = "API Gateway Endpoint"
}

Deploy the Solution

Let's deploy this thing! If you haven't done so, start the Skillmix lab session and get the account credentials. Configure your Terraform environment to use those credentials.

Then, open a terminal or command prompt, navigate to the folder with your Terraform file, and execute these commands:

# initiatlize the project 
$ terraform init 

# show the plan 
$ terraform plan 

# apply the changes 
$ terraform apply

Wait for the changes to be applied before proceeding.

Testing

The stack creates and outputs the 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.

curl -s -o /dev/null -w "%{http_code}" <API endpoint> ; echo

Source: https://github.com/aws-samples/serverless-patterns/tree/main/apigw-vpclink-pvt-alb-terraform