I've spent a whole day to understand how to setup API gateway with my private NLB which is a LoadBalancer type of EKS service and expose some of the private REST API to public internet with api key protection. here is the terraform scripts to manage this journey
the api
it's where everything starts, the REST API
resource "aws_api_gateway_rest_api" "acme-api-gw-rest" {
name = "${var.env-name}-acme-api-gw-rest-${var.region-name}"
endpoint_configuration {
types = ["REGIONAL"]
}
description = "use to support transaction-id in global account"
tags = var.resource_tags
}
the vpc link
rest api vpc link only support NLB, I've created the NLB with eks service loadbalancer with ALB controller, so put it's ARN here
resource "aws_api_gateway_vpc_link" "acme-api-gw-vpc-link" {
name = "${var.env-name}-acme-api-gw-vpc-link-${var.region-name}"
description = "vpc link for acme rest api"
target_arns = [var.acme-private-nlb-arn]
tags = var.resource_tags
}
the resources
we could create as many resources as we want , the root resources could be referenced from aws_api_gateway_rest_api.acme-api-gw-rest.root_resource_id
,
here I create a resource on path /acme/common/echo/{msg}
{msg}
is the path parameter, we could pass to backend
resource "aws_api_gateway_resource" "acme-api-gw-rs-acme" {
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
parent_id = aws_api_gateway_rest_api.acme-api-gw-rest.root_resource_id
path_part = "acme"
}
resource "aws_api_gateway_resource" "acme-api-gw-rs-acme-common" {
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
parent_id = aws_api_gateway_resource.acme-api-gw-rs-acme.id
path_part = "common"
}
resource "aws_api_gateway_resource" "acme-api-gw-rs-acme-common-echo" {
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
parent_id = aws_api_gateway_resource.acme-api-gw-rs-acme-common.id
path_part = "echo"
}
resource "aws_api_gateway_resource" "acme-api-gw-rs-acme-common-echo-msg" {
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
parent_id = aws_api_gateway_resource.acme-api-gw-rs-acme-common-echo.id
path_part = "{msg}"
}
the method
we could attach GET/POST/PUT/DELETE/PATCH/HEAD or ANY method to a resource, if there is a path parameter, we need to declare in request_parameters
section, later we will use it in integration
resource "aws_api_gateway_method" "acme-api-gw-rs-acme-common-echo-msg-get" {
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
resource_id = aws_api_gateway_resource.acme-api-gw-rs-acme-common-echo-msg.id
http_method = "GET"
authorization = "NONE"
api_key_required = true
request_parameters = {
"method.request.path.msg" = true
}
}
resource "aws_api_gateway_method_response" "acme-api-gw-rs-acme-common-echo-msg-get-200" {
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
resource_id = aws_api_gateway_resource.acme-api-gw-rs-acme-common-echo-msg.id
http_method = aws_api_gateway_method.acme-api-gw-rs-acme-common-echo-msg-get.http_method
status_code = "200"
}
the integration
integration represent the backend url which doing the actual work, we use connection_id
to connect the integration into the VPC we created before and we also use request_parameters
to map the request parameter to the integration path parameter.
resource "aws_api_gateway_integration" "acme-api-gw-rs-acme-common-echo-msg-get-integration" {
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
resource_id = aws_api_gateway_resource.acme-api-gw-rs-acme-common-echo-msg.id
http_method = aws_api_gateway_method.acme-api-gw-rs-acme-common-echo-msg-get.http_method
connection_type = "VPC_LINK"
connection_id = aws_api_gateway_vpc_link.acme-api-gw-vpc-link.id
integration_http_method = "GET"
type = "HTTP"
uri = "http://${var.acme-private-nlb-dns}/acme/common/echo/{msg}"
request_parameters = {
"integration.request.path.msg" = "method.request.path.msg"
}
}
resource "aws_api_gateway_integration_response" "acme-api-gw-rs-acme-common-echo-msg-get-integration-response" {
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
resource_id = aws_api_gateway_resource.acme-api-gw-rs-acme-common-echo-msg.id
http_method = aws_api_gateway_method.acme-api-gw-rs-acme-common-echo-msg-get.http_method
status_code = aws_api_gateway_method_response.acme-api-gw-rs-acme-common-echo-msg-get-200.status_code
}
the deployment and stage
now we could deploy the API into a stage, so that it could have a url to call from public internet and we could use api key to protect the api as well, like below
now we could call the API like
curl --header 'X-API-Key: xxx' -H 'Content-Type: application/json' https://z5kp58ywm9.execute-api.ap-southeast-2.amazonaws.com/dev/acme/common/echo/abc
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
triggers = {
# NOTE: The configuration below will satisfy ordering considerations,
# but not pick up all future REST API changes. More advanced patterns
# are possible, such as using the filesha1() function against the
# Terraform configuration file(s) or removing the .id references to
# calculate a hash against whole resources. Be aware that using whole
# resources will show a difference after the initial implementation.
# It will stabilize to only change when resources change afterwards.
redeployment = sha1(jsonencode([
aws_api_gateway_resource.acme-api-gw-rs-acme.id,
aws_api_gateway_resource.acme-api-gw-rs-acme-common.id,
aws_api_gateway_resource.acme-api-gw-rs-acme-common-echo.id,
aws_api_gateway_resource.acme-api-gw-rs-acme-common-echo-msg.id,
aws_api_gateway_method.acme-api-gw-rs-acme-common-echo-msg-get.id,
aws_api_gateway_integration.acme-api-gw-rs-acme-common-echo-msg-get-integration.id,
]))
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_stage" "acme-api-gw-stage" {
deployment_id = aws_api_gateway_deployment.acme-api-gw-deployment.id
rest_api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
stage_name = var.env-name
}
resource "aws_api_gateway_usage_plan" "acme-api-gw-usage-plan" {
name = "${var.env-name}-acme-${var.region-name}"
description = "acme api usage plan"
product_code = "acme"
api_stages {
api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
stage = aws_api_gateway_stage.acme-api-gw-stage.stage_name
}
quota_settings {
limit = 10000
offset = 1
period = "WEEK"
}
throttle_settings {
burst_limit = 100
rate_limit = 50
}
}
resource "aws_api_gateway_api_key" "acme-api-gw-default" {
name = "${var.env-name}-acme-${var.region-name}"
}
resource "aws_api_gateway_usage_plan_key" "acme-api-gw-default" {
key_id = aws_api_gateway_api_key.acme-api-gw-default.id
key_type = "API_KEY"
usage_plan_id = aws_api_gateway_usage_plan.acme-api-gw-usage-plan.id
}
the custom domain
sometimes we want to expose the API with our own domain over https, so first we need to request certificate from AWS Certificate Manager
resource "aws_acm_certificate" "acme-cert" {
domain_name = var.domain
validation_method = "DNS"
subject_alternative_names = ["${var.env-name}-acme-gw.${var.region-name}.${var.domain}"]
tags = var.resource_tags
lifecycle {
create_before_destroy = true
}
}
then we could create the custom domain name,
- create the domain name
- register custom domain's DNS in route53 to our own domain, so that we could call the domian name
- connect the custom domain name with a stage we have deployed
now we could call the api from a custom domain name like below
curl --header 'X-API-Key: xxx' -H 'Content-Type: application/json' https://acme-api.acme.com/acme/common/echo/abc
resource "aws_api_gateway_domain_name" "acme-api-gw-domain" {
domain_name = "${var.env-name}-acme-document-processors-gw.${var.region-name}.${var.domain}"
regional_certificate_arn = aws_acm_certificate.acme-cert.arn
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_route53_record" "acme-api-gw-domain" {
name = aws_api_gateway_domain_name.acme-api-gw-domain.domain_name
type = "A"
zone_id = var.zone_id
alias {
evaluate_target_health = true
name = aws_api_gateway_domain_name.acme-api-gw-domain.regional_domain_name
zone_id = aws_api_gateway_domain_name.acme-api-gw-domain.regional_zone_id
}
}
resource "aws_api_gateway_base_path_mapping" "acme-api-gw-domain-mapping" {
api_id = aws_api_gateway_rest_api.acme-api-gw-rest.id
stage_name = aws_api_gateway_stage.acme-api-gw-stage.stage_name
domain_name = aws_api_gateway_domain_name.acme-api-gw-domain.domain_name
}