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

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

https://aws.amazon.com/jp/about-aws/whats-new/2024/11/amazon-api-gateway-custom-domain-name-private-rest-apis/

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

https://dev.classmethod.jp/articles/api-gateway-custom-domain-name-private-rest/



# 実装したもの
こんな感じのことを行いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/d0add8ae-5dbd-4428-bb55-24411520c714.png)

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

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

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

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-private-custom-domains.html


# 実装の手順

## VPCとVPCエンドポイントの作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/ab744b6a-6c23-4e52-9715-148c7388fafd.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/6cfbf232-fcdf-49df-9ec1-73e6df4ce498.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エンドポイントを名前解決先として指定することがポイントです。

https://arc.net/l/quote/kaimcwdb


## ACM証明書の取得
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/a6628a24-e7fc-4767-af8d-d3888c1a3a9c.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/6adfdffb-e2f0-45e3-9d0d-98f204be60a4.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/98c4bfe1-2d28-4c67-a6bd-8284a1feaf83.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に、プライベートカスタムドメインを設定することができました。
誤りや誤解を生む表現がございましたら、コメントでご指摘いただけると助かります。