Interface型のVPCエンドポイントはサービスごとに
aws_vpc_endpoint リソースを1つずつ書く必要があり、5本・10本と増えるにつれてHCLが冗長になりがちです。for_each を使えば、サービス名マップを1か所で宣言するだけで全エンドポイントを同じ設定で一括作成・管理できます。この記事では、TerraformでInterface型VPCエンドポイントを複数サービスまとめて定義する設計パターンを解説します。サブネットのマルチAZ配置・専用セキュリティグループ・
private_dns_enabled の設定根拠を実際のHCLコードと実行例で説明します。この記事のポイント
・Interface型エンドポイントはsubnet_ids・security_group_ids・private_dns_enabledの3点セットが必須
・for_eachでlocalsのサービスマップを回すとDRYな一括定義が実現できる
・マルチAZにサブネットを分散するだけでエンドポイントもHA構成になる
・private_dns_enabledにはVPCのenable_dns_hostnames/supportが有効である前提条件がある
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜInterface型VPCエンドポイントをTerraformで一括管理するのか
VPCエンドポイントのInterface型は、AWSのPrivateLinkを使ってVPC内のENI経由でサービスAPIと通信します。インターネットゲートウェイやNAT Gatewayを経由しないため、ネットワーク経路がVPC内で完結します。ECS Fargateやプライベートサブネット内のEC2から呼び出すサービス(SSM・EC2 Messages・ECR・Secrets Manager・CloudWatch Logsなど)はそれぞれ専用エンドポイントが必要で、対象サービスが増えるほど手作業の量も増えます。Terraformで管理することで以下のメリットが得られます。
・再現性:同じコードでdev・stgや本番に同一構成のエンドポイント群を展開できる
・変更追跡:GitのPRにエンドポイントの追加・削除・設定変更の経緯が残る
・一括変更:セキュリティグループルールの修正をすべてのエンドポイントに同時反映できる
コンソールで1つずつ作ると「dev環境には存在するがstgには作っていないエンドポイント」が生まれやすく、環境差分のデバッグに時間を取られます。for_each設計にすれば、サービス名リストを1行追加するだけで全環境に適用できます。
aws_vpc_endpointの基本構文とInterface型の必須設定
1. Interface型エンドポイントの最小構成
Interface型のaws_vpc_endpoint に必要な属性は vpc_id・service_name・vpc_endpoint_type・subnet_ids・security_group_ids の5つです。以下はSSMのエンドポイントを単体で定義した例です(東京リージョン)。resource "aws_vpc_endpoint" "ssm" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.ap-northeast-1.ssm" vpc_endpoint_type = "Interface" # ENIを配置するサブネット(マルチAZを推奨) subnet_ids = [ aws_subnet.private_1a.id, aws_subnet.private_1c.id, ] # エンドポイント専用のセキュリティグループ security_group_ids = [aws_security_group.vpc_endpoint.id] # デフォルトDNS名をエンドポイントIPに解決させる private_dns_enabled = true tags = { Name = "ssm-endpoint" } }
service_name はリージョンごとに異なります。data.aws_region.current.name を使うとリージョン移植性が上がります。data "aws_region" "current" {} # service_name をリージョン変数で組み立てる service_name = "com.amazonaws.${data.aws_region.current.name}.ssm"
2. 専用セキュリティグループを用意する
Interface型エンドポイントはVPC内のENIにバインドされるため、セキュリティグループが必要です。ルールはシンプルで、VPC CIDRからのHTTPS(443番ポート)インバウンドを許可するだけです。アウトバウンドは通常、デフォルトのすべて許可のままで問題ありません。resource "aws_security_group" "vpc_endpoint" { name = "vpc-endpoint-sg" description = "Allow HTTPS from VPC to interface endpoints" vpc_id = aws_vpc.main.id ingress { description = "HTTPS from VPC" 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"] } tags = { Name = "vpc-endpoint-sg" } }
cidr_blocks をプライベートサブネットのCIDRに限定してください。3. private_dns_enabledとVPCのDNS前提条件
private_dns_enabled = true を設定すると、サービスのデフォルトDNS名(例: ssm.ap-northeast-1.amazonaws.com)がエンドポイントのプライベートIPに解決されるようになります。アプリケーションやSDKのエンドポイントURLを変更せずに済む、最も重要な設定です。ただし、この設定にはVPC側で2つのDNSオプションが有効になっている前提があります。
resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" # この2つがtrueでないとprivate_dns_enabled = trueが機能しない enable_dns_hostnames = true enable_dns_support = true tags = { Name = "main-vpc" } }
enable_dns_support = true ですが、enable_dns_hostnames はデフォルト false です。既存VPCをTerraformにインポートした場合は必ず確認してください。for_eachで複数サービスを一括定義する設計パターン
1. localsブロックでサービスマップを宣言する
エンドポイントの追加・削除を1か所で管理できるようにするため、locals ブロックでサービス名マップを定義します。キーはTerraformの識別子(英数字とアンダースコア)、値はAWSのサービス名文字列のサフィックスです。locals { # ECS Fargate + SSM Session Manager で最低限必要なエンドポイント群 interface_endpoints = { ssm = "ssm" ssmmessages = "ssmmessages" ec2messages = "ec2messages" ecr_api = "ecr.api" ecr_dkr = "ecr.dkr" logs = "logs" secretsmanager = "secretsmanager" } }
2. for_eachでエンドポイントを束ねる
for_each に local.interface_endpoints を渡すと、マップのキーごとに aws_vpc_endpoint リソースが生成されます。data "aws_region" "current" {} resource "aws_vpc_endpoint" "interface" { for_each = local.interface_endpoints vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${data.aws_region.current.name}.${each.value}" vpc_endpoint_type = "Interface" subnet_ids = [ aws_subnet.private_1a.id, aws_subnet.private_1c.id, ] security_group_ids = [aws_security_group.vpc_endpoint.id] private_dns_enabled = true tags = { Name = "${each.key}-endpoint" } }
ssm・ecr_api など)をリソースアドレスの識別子として使います。例えばSSMのエンドポイントは aws_vpc_endpoint.interface["ssm"] として state に登録されます。新しいサービスを追加しても既存エンドポイントの state には影響しません。terraform plan の出力では7リソースの作成が表示されます(実際の環境での実行例・リージョンとVPC IDはマスク済み)。$ terraform plan Terraform will perform the following actions: # aws_vpc_endpoint.interface["ec2messages"] will be created + resource "aws_vpc_endpoint" "interface" { + service_name = "com.amazonaws.ap-northeast-1.ec2messages" + vpc_endpoint_type = "Interface" + private_dns_enabled = true ... } # aws_vpc_endpoint.interface["ecr_api"] will be created ...(以下5リソース分続く) Plan: 7 to add, 0 to change, 0 to destroy.
3. マルチAZサブネット配置でHA構成にする
subnet_ids に複数AZのサブネットIDを渡すと、AWSが各AZにENIを自動で1つずつ作成します。1つのAZが障害を起こしても残りのAZのENIが継続してトラフィックを受け付けるため、HA構成になります。サブネットを
for_each で管理している場合は values() を組み合わせてサブネットIDのリストを渡せます。resource "aws_subnet" "private" { for_each = { "ap-northeast-1a" = "10.0.1.0/24" "ap-northeast-1c" = "10.0.2.0/24" "ap-northeast-1d" = "10.0.3.0/24" } vpc_id = aws_vpc.main.id cidr_block = each.value availability_zone = each.key tags = { Name = "private-${each.key}" } } resource "aws_vpc_endpoint" "interface" { for_each = local.interface_endpoints vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${data.aws_region.current.name}.${each.value}" vpc_endpoint_type = "Interface" # values()でサブネットIDの一覧を渡す(3AZ分のENIが作成される) subnet_ids = values(aws_subnet.private)[*].id security_group_ids = [aws_security_group.vpc_endpoint.id] private_dns_enabled = true tags = { Name = "${each.key}-endpoint" } }
トラブルシューティング|よくあるエラーと対処法
Q1. terraform applyで「InvalidSubnetID.NotFound」が返る
subnet_ids に指定したサブネットIDが存在しない、またはVPCと異なるリージョンに存在するケースで発生します。aws_subnet.private の参照が正しいか、terraform state list でサブネットリソースが state に登録されているかを確認してください。サブネットをコンソールで手動作成していてTerraform管理外の場合は、
data "aws_subnet" でサブネットIDを参照するか、terraform import でIaC管理下に取り込んでから subnet_ids に渡してください。Q2. private_dns_enabledをtrueにしてもDNSが解決できない
VPCのenable_dns_hostnames または enable_dns_support が false になっているケースがほとんどです。次のAWS CLIコマンドで現状を確認できます。# VPCのDNS設定を確認する(vpc-idは実際のIDに置き換え) $ aws ec2 describe-vpc-attribute \ --vpc-id vpc-0a1b2c3d4e5f \ --attribute enableDnsHostnames { "VpcId": "vpc-0a1b2c3d4e5f", "EnableDnsHostnames": { "Value": false } } # Value: false の場合はenable_dns_hostnames = trueをterraform applyで反映する
aws_vpc リソースの enable_dns_hostnames = true を確認し、terraform apply で反映してください。Q3. エンドポイントを作成しても接続できない(タイムアウトになる)
セキュリティグループのインバウンドルールが原因のことが多いです。VPC CIDRからのHTTPS(443番)を許可しているか確認してください。エンドポイントを配置したサブネットと接続元のEC2・Fargateタスクが異なるサブネットにある場合でも、ルートテーブルの設定は不要ですが、接続元セキュリティグループからのアウトバウンドHTTPSも確認してください。# エンドポイントのステータスと配置を確認する $ aws ec2 describe-vpc-endpoints \ --filters "Name=vpc-id,Values=vpc-0a1b2c3d4e5f" \ --query "VpcEndpoints[*].{Service:ServiceName,State:State,Subnets:SubnetIds}" \ --output table ---------------------------------------------------------------------- | DescribeVpcEndpoints | +--------------------------+----------+------------------------------+ | Service | State | Subnets | +--------------------------+----------+------------------------------+ | com.amazonaws.ap-..ssm | available| subnet-xxx, subnet-yyy | | com.amazonaws.ap-..ecr | available| subnet-xxx, subnet-yyy | +--------------------------+----------+------------------------------+
State が available であればエンドポイント自体は正常です。接続できない場合はセキュリティグループのルールを再確認してください。本記事のまとめ
TerraformでInterface型VPCエンドポイントを一括定義するポイントをまとめます。| 設定項目 | ポイント |
|---|---|
vpc_endpoint_type = "Interface" |
Interface型を明示。省略するとGateway型になる |
subnet_ids |
複数AZのサブネットIDを渡すとHA構成になる |
security_group_ids |
VPC CIDRからのHTTPS(443)インバウンドを許可するSGを専用で作る |
private_dns_enabled = true |
VPCのenable_dns_hostnames/supportが両方trueである前提 |
| for_each + locals | サービス名マップで複数エンドポイントをDRYに束ねる |
| service_name | data.aws_region.currentでリージョン部分を動的に組み立てる |
>> Terraform実践セミナーの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:Terraformで環境ごとにstateの保管先を切り替える設計|部分設定ファイルと環境変数によるinit時の分岐
- この記事の属するカテゴリ:Terraformへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます