5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Terraform対応】Private REST API Gateway にカスタムドメインを設定する手順(2024年11月新機能対応)

Last updated at Posted at 2025-05-22

REST API Gatewayでのプライベートカスタムドメイン設定について

2024年11月、Amazon API Gateway が プライベート REST API に対するカスタムドメイン名のサポート を開始しました。

マネジメントコンソールで実装しているクラスメソッドさんの記事がありますので、事前に以下の記事に目を通しておくと良いかもしれません。

実装したもの

こんな感じのことを行いました。

image.png

図に示したとおり、プライベートカスタムドメインを実現するには、

  • プライベートカスタムドメインそのもの
  • 名前解決先で、実際にAPIGWを叩くインターフェースVPCエンドポイント
  • ドメイン名アクセスの関連付け
  • ドメインとAPIGWのマッピング
  • パブリック証明書
  • パブリック証明書のDNS検証を行うためのパブリックホストゾーン
  • VPC内で名前解決を行うためのプライベートホストゾーン

これらのリソースを作成し、紐づけていく必要があります。

以下の公式ドキュメントに方法が載ってはいますが、それでも実装につまることはあったので、備忘録として自分の言葉で記事として残します。

実装の手順

VPCとVPCエンドポイントの作成

image.png

vpc.tf
# --------------------------------------------------------
# VPC
resource "aws_vpc" "main" {
  cidr_block           = "10.128.0.0/16"
  enable_dns_hostnames = true

resource "aws_subnet" "private_subnet" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.128.1.0/24"
  availability_zone       = var.availability_zone
  map_public_ip_on_launch = false

resource "aws_route_table" "private_rt" {
  vpc_id = aws_vpc.main.id
}


# --------------------------------------------------------
# VPCエンドポイント(Private_API_Gateway)

resource "aws_security_group" "private_api_gateway_sg" {
  vpc_id = aws_vpc.main.id

  ingress {
    description      = "Allow CloudShell (and any in-VPC client) to reach the Private API GW endpoint"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    cidr_blocks      = [ aws_vpc.main.cidr_block ]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }


resource "aws_vpc_endpoint" "private_api_gateway" {
  vpc_id              = aws_vpc.main.id
  subnet_ids          = [aws_subnet.private_subnet.id]
  service_name        = var.private_api_gateway_endpoint_service_name
  security_group_ids  = [aws_security_group.private_api_gateway_sg.id]
  vpc_endpoint_type   = "Interface"
  private_dns_enabled = true
}

# --------------------------------------------------------
# Outputs (後ほどプライベートホストゾーンの定義で使用します)

output "api_gateway_endpoint_dns_name" {
  value = aws_vpc_endpoint.private_api_gateway.dns_entry[0].dns_name
}

output "api_gateway_endpoint_hosted_zone_id" {
  value = aws_vpc_endpoint.private_api_gateway.dns_entry[0].hosted_zone_id
}

output "vpc_endpoint_id" {
  description = "Interface VPC endpoint ID for private API Gateway"
  value       = aws_vpc_endpoint.private_api_gateway.id
}


VPC内部での名前解決を有効化してください。
CloudShell動作確認用に443ポートを開けていますが、ここはあけなくも大丈夫です。

Route53 パブリックホストゾーンとプライベートホストゾーンの作成

image.png

ACM証明書を有効にするにはDNS検証が必要なので、その検証を行うためにパブリックホストゾーンを作成します。(ドメインの取得かサブドメインの委任は済ませてください)
また、先ほど作成したVPC内で名前解決を行うためにプライベートホストゾーンも作成します。

route53.tf
# --------------------------------------------------------
# Route53 (Public Hosted Zone)

resource "aws_route53_zone" "public_hostzone" {
  name    = var.hostzone_name
  comment = "Public Hosted Zone for ${var.hostzone_name}"
}

# --------------------------------------------------------
# Route53 (Private Hosted Zone)

resource "aws_route53_zone" "private_hostzone" {
  name    = var.hostzone_name
  comment = "Private Hosted Zone for ${var.hostzone_name}"
  vpc {
    vpc_id     = var.vpc_id_apne1
    vpc_region = "ap-northeast-1"
  }
}

resource "aws_route53_record" "a_record" { # ここがポイント
  zone_id = aws_route53_zone.private_hostzone.zone_id
  name    = "apigw-apne1.${var.hostzone_name}"
  type    = "A"
  alias {
    name                   = var.api_gateway_endpoint_dns_name_apne1
    zone_id                = var.api_gateway_endpoint_apne1_zone_id
    evaluate_target_health = true
  }
}

# --------------------------------------------------------
# Outputs

output "route53_public_zone_id" {
  description = "Route53 public zone ID"
  value       = aws_route53_zone.public_hostzone.zone_id
}

output "route53_public_zone_name" {
  description = "Route53 public zone name"
  value       = aws_route53_zone.public_hostzone.name
}

output "route53_private_zone_name" {
  description = "Route53 private zone name"
  value       = aws_route53_zone.private_hostzone.name
}

ここのAレコードでエイリアスを有効にして、先ほど作成したVPCエンドポイントを名前解決先として指定することがポイントです。

ACM証明書の取得

image.png

*.example.comの証明書を取得します。
APIGateway プライベートドメインを作成するためにACM証明書が必要なので発行します。
ここについては、パブリック証明書でも問題なく動作することを確認したので、プライベート認証局を建てる必要はないです。

acm.tf
# --------------------------------------------------------
# ACM (Public Certificate)

resource "aws_acm_certificate" "cert" {
  domain_name               = "*.${var.domain_name}"
  subject_alternative_names = [var.domain_name] 
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

# --------------------------------------------------------
# DNS検証

resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  name            = each.value.name
  type            = each.value.type
  zone_id         = var.route53_zone_id
  records         = [each.value.record]
  ttl             = 60
  allow_overwrite = true
}
resource "aws_acm_certificate_validation" "cert_validation" {
  certificate_arn         = aws_acm_certificate.cert.arn
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}

# --------------------------------------------------------
# Outputs (プライベートカスタムドメインで使用します)

output "certificate_arn" {
  description = "ARN of the ACM certificate"
  value       = aws_acm_certificate.cert.arn
}

プライベート REST API Gatewayの作成

image.png

HTTPではなく、RESTで作成してください。
リソースポリシーには、最初に作成したVPCエンドポイントを追加して、アクセスを有効化してください。

private_rest_api_gateway.tf
# --------------------------------------------------------
# プライベート REST API
resource "aws_api_gateway_rest_api" "private_api" {
  name = "private-rest-api"
  endpoint_configuration {
    types = ["PRIVATE"]
  }
  policy = jsonencode({
    Version   = "2012-10-17",
    Statement = [
      {
        Sid      = "AllowFromSpecificVPCE"
        Effect   = "Allow"
        Principal= "*"
        Action   = "execute-api:Invoke"
        Resource = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:*/*/*/*"
        Condition = {
          StringEquals = {
            "aws:SourceVpce" = var.allowed_vpce_id # 先ほど作成したVPCエンドポイントを指定
          }
        }
      },
      {
        Sid      = "DenyOthers"
        Effect   = "Deny"
        Principal= "*"
        Action   = "execute-api:Invoke"
        Resource = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:*/*/*/*"
        Condition = {
          StringNotEquals = {
            "aws:SourceVpce" = var.allowed_vpce_id # 先ほど作成したVPCエンドポイントを指定
          }
        }
      }
    ]
  })
}

# --------------------------------------------------------
# ルートリソースに /hello を追加(例)
resource "aws_api_gateway_resource" "hello" {
  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   = "hello"
}

# GET メソッド
resource "aws_api_gateway_method" "get_hello" {
  rest_api_id   = aws_api_gateway_rest_api.private_api.id
  resource_id   = aws_api_gateway_resource.hello.id
  http_method   = "GET"
  authorization = "NONE"
}

# モック統合
resource "aws_api_gateway_integration" "mock_hello" {
  rest_api_id = aws_api_gateway_rest_api.private_api.id
  resource_id = aws_api_gateway_resource.hello.id
  http_method = aws_api_gateway_method.get_hello.http_method
  type        = "MOCK"
  request_templates = {
    "application/json" = "{\"statusCode\": 200}"
  }
}

# デプロイ & ステージ
resource "aws_api_gateway_deployment" "this" {
  rest_api_id = aws_api_gateway_rest_api.private_api.id
  triggers = {
    redeploy = sha1(jsonencode(aws_api_gateway_rest_api.private_api)) # 変更検知用
  }
}

resource "aws_api_gateway_stage" "prod" {
  deployment_id = aws_api_gateway_deployment.this.id
  rest_api_id   = aws_api_gateway_rest_api.private_api.id
  stage_name    = "prod"
}

プライベートカスタムドメイン、マッピング、関連付けの作成

image.png

最後に、本題のこれらのリソースを作成していきます。
こちら側にもリソースポリシーが必要ですのでお気をつけください。

api_gateway_custom_domain.tf
# --------------------------------------------------------
# # 1. プライベートカスタムドメイン
resource "aws_api_gateway_domain_name" "private_domain" {
    domain_name = "apigw.${var.domain_name}"
    certificate_arn = var.certificate_arn # 事前に作成したパブリック証明書を指定します。
    security_policy = "TLS_1_2"
    endpoint_configuration {
        types = ["PRIVATE"]
    }
    policy = jsonencode({
    Version   = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Action    = "execute-api:Invoke"
        Resource  = "execute-api:/*"
        Principal = "*"
      },
      {
        Effect    = "Deny"
        Action    = "execute-api:Invoke"
        Resource  = "execute-api:/*"
        Principal = "*"
        Condition = {
          StringNotEquals = {
            "aws:SourceVpce" = var.allowed_vpce_id # 最初に作成したVPCエンドポイントを指定 
          }
        }
      },
    ]
  })
}

# --------------------------------------------------------
# # 2. VPCE ↔ ドメイン関連付け (DomainNameAccessAssociation)
resource "aws_api_gateway_domain_name_access_association" "vpce_association" {
    access_association_source = var.all_search_account_vpce_id
    access_association_source_type = "VPCE"
    domain_name_arn = aws_api_gateway_domain_name.private_domain.arn
}

# --------------------------------------------------------
# # 3. API ↔ ドメインのマッピングについては、2025/04/28現在awsccのみ対応している。
resource "awscc_apigateway_base_path_mapping_v2" "base_path_mapping" {
  domain_name_arn = aws_api_gateway_domain_name.private_domain.arn
  rest_api_id     = var.api_id
  base_path       = var.api_stage
  stage           = var.api_stage
}

まとめ

以上の工程でREST Private API Gatewayに、プライベートカスタムドメインを設定することができました。
誤りや誤解を生む表現がございましたら、コメントでご指摘いただけると助かります。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?