Skip to main content

Command Palette

Search for a command to run...

¿Cómo Crear una API Gateway REST Privada con Dominio Personalizado en AWS?

Implementaremos un API Gateway REST privada en AWS, con un VPC Endpoint Interface y expuesta mediante un dominio personalizado privado.

Updated
4 min read

Cuando se desarrollan microservicios internos o APIs para sistemas confidenciales, es común que necesitemos exponer estas APIs solo dentro del entorno privado de AWS, evitando cualquier exposición pública. Para ello, AWS ofrece API Gateway con endpoint PRIVATE combinado con VPC Endpoint Interface.

En este artículo veremos como crear y configurar:

  • API Gateway REST configurado como PRIVATE

  • Un VPC Endpoint Interface para execute-api

  • Un dominio privado personalizado (ej: api.infra.local)

  • Validación SSL con ACM

  • Alias en Route 53 hacia el VPC Endpoint


⚠️ Advertencias y Problemas Resueltos

Durante la implementación nos enfrentamos a las siguientes restricciones y errores relacionados con las versiones de Terraform:

Problema: Error 404 al usar aws_api_gateway_base_path_mapping con dominios privados

  • Mensaje:
Error: creating API Gateway Base Path Mapping (operation error API Gateway: CreateBasePathMapping, https response error StatusCode: 404, RequestID: xxx, NotFoundException: Invalid domain name identifier specified)
  • Causa: En versiones anteriores a Terraform v5.88, el proveedor tenía un bug donde la combinación del campo domain_name junto con domain_name_id generaba una construcción incorrecta en la llamada interna a la API de AWS.

Por ejemplo:

resource "aws_api_gateway_base_path_mapping" "example" {
  domain_name    = aws_api_gateway_domain_name.example.domain_name
  domain_name_id = aws_api_gateway_domain_name.example.id
  api_id         = aws_api_gateway_rest_api.example.id
  stage_name     = aws_api_gateway_stage.example.stage_name
  base_path      = ""
}

Generaba internamente algo como:

api.dev.mobiles.secure.alfalfita.click//api.dev.mobiles.secure.alfalfita.click/okmnxgsnua

Esta combinación con doble barra (//) es inválida y resultaba en el error Invalid domain name identifier specified.

  • Solución temporal: Usar un null_resource con local-exec que ejecute manualmente el CLI de AWS con el domain-name-id correcto.

Fix oficial en Terraform v5.88

Desde la versión Terraform v5.88, el proveedor de AWS para Terraform ya permite el uso adecuado de domain_name_id con dominios privados, sin necesidad de combinarlo con domain_name, y sin generar duplicaciones.

resource "aws_api_gateway_base_path_mapping" "private_api_mapping" {
  domain_name_id = aws_api_gateway_domain_name.private_custom_domain.domain_name_id
  api_id         = aws_api_gateway_rest_api.private.id
  stage_name     = aws_api_gateway_stage.stage.stage_name
  base_path      = ""
}

👀 Recomendación: Asegúrate de estar utilizando Terraform v5.88 o superior para evitar este problema.


Paso 1: Crear el Dominio Personalizado Privado

resource "aws_api_gateway_domain_name" "private_custom_domain" {
  domain_name     = "api.infra.local"
  certificate_arn = aws_acm_certificate.cert.arn
  security_policy = "TLS_1_2"

  endpoint_configuration {
    types = ["PRIVATE"]
  }
}

Para la validación de certificado se recomienda usar DNS con una zona Route 53 pública o privada según el caso.


Paso 2: Crear el VPC Endpoint Interface para API Gateway

resource "aws_vpc_endpoint" "execute_api" {
  vpc_id            = var.vpc_id
  service_name      = "com.amazonaws.${var.region}.execute-api"
  vpc_endpoint_type = "Interface"
  subnet_ids        = var.subnet_ids
  security_group_ids = var.sg_ids
  private_dns_enabled = false
}

Este endpoint permite a las instancias en la VPC comunicarse con la API Gateway privada.


Paso 3: Asociar el Custom Domain al VPC Endpoint

resource "aws_api_gateway_domain_name_access_association" "assoc" {
  access_association_source      = aws_vpc_endpoint.execute_api.id
  access_association_source_type = "VPCE"
  domain_name_arn                = aws_api_gateway_domain_name.private_custom_domain.arn
}

Paso 4: Crear la API Gateway REST Privada

resource "aws_api_gateway_rest_api" "private_api" {
  name = "mi-api-privada"

  endpoint_configuration {
    types            = ["PRIVATE"]
    vpc_endpoint_ids = [aws_vpc_endpoint.execute_api.id]
  }
}

Paso 5: Crear los Recursos y Métodos

resource "aws_api_gateway_resource" "validar" {
  rest_api_id = aws_api_gateway_rest_api.private_api.id
  parent_id   = aws_api_gateway_rest_api.private_api.root_resource_id
  path_part   = "validar"
}

resource "aws_api_gateway_method" "get" {
  rest_api_id   = aws_api_gateway_rest_api.private_api.id
  resource_id   = aws_api_gateway_resource.validar.id
  http_method   = "GET"
  authorization = "NONE"
}

Paso 6: Configurar Integración y Deployment

resource "aws_api_gateway_integration" "mock" {
  rest_api_id = aws_api_gateway_rest_api.private_api.id
  resource_id = aws_api_gateway_resource.validar.id
  http_method = aws_api_gateway_method.get.http_method
  type        = "MOCK"

  request_templates = {
    "application/json" = "{\"statusCode\": 200}"
  }
}

resource "aws_api_gateway_deployment" "deploy" {
  rest_api_id = aws_api_gateway_rest_api.private_api.id
  triggers = {
    redeploy = timestamp()
  }
  lifecycle {
    create_before_destroy = true
  }
}

Paso 7: Mapear la API al Dominio Privado

Desde Terraform v5.88+, se soporta el mapeo de base path en dominios privados:

resource "aws_api_gateway_base_path_mapping" "map" {
  domain_name_id = aws_api_gateway_domain_name.private_custom_domain.domain_name_id
  api_id         = aws_api_gateway_rest_api.private_api.id
  stage_name     = aws_api_gateway_stage.stage.stage_name
  base_path      = ""
}

Paso 8: Aplicar Políticas de Seguridad

Restringe acceso a la API por VPC o por VPC Endpoint:

data "aws_iam_policy_document" "private_policy" {
  statement {
    effect = "Deny"
    actions = ["execute-api:Invoke"]
    resources = ["${aws_api_gateway_rest_api.private_api.execution_arn}/*/*"]

    principals {
      type        = "AWS"
      identifiers = ["*"]
    }

    condition {
      test     = "StringNotEquals"
      variable = "aws:SourceVpce"
      values   = [aws_vpc_endpoint.execute_api.id]
    }
  }

  statement {
    effect = "Allow"
    actions = ["execute-api:Invoke"]
    resources = ["${aws_api_gateway_rest_api.private_api.execution_arn}/*/*"]
    principals {
      type        = "AWS"
      identifiers = ["*"]
    }
  }
}

Paso 9: Alias DNS en Route 53

resource "aws_route53_record" "custom_domain_alias" {
  zone_id = aws_route53_zone.private.id
  name    = "api.infra.local"
  type    = "A"

  alias {
    name                   = aws_vpc_endpoint.execute_api.dns_entry[0].dns_name
    zone_id                = aws_vpc_endpoint.execute_api.dns_entry[0].hosted_zone_id
    evaluate_target_health = false
  }
}

Conclusión

Con esta configuración, logramos una API REST totalmente privada, expuesta con HTTPS y dominio personalizado, con seguridad a nivel de red (VPC Endpoint) e IAM. Una solución ideal para integraciones internas seguras, cumplimiento de normativas y entornos confidenciales.

Para consumir tu API desde la misma VPC, puedes ejecutar:

curl -k https://api.infra.local/prod/validar