TerraformでVPCエンドポイントを一括定義する方法|インターフェース型のサブネット配置とプライベートDNS有効化の実装

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > TerraformでVPCエンドポイントを一括定義する方法|インターフェース型のサブネット配置とプライベートDNS有効化の実装
「SSMエージェントのVPCエンドポイントを作ったら、次はECR、その次はSecrets Manager……と結局コンソールでの手作業が止まらない」
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が有効である前提条件がある


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜ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_idservice_namevpc_endpoint_typesubnet_idssecurity_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" } }

エンドポイントごとに異なるセキュリティグループを作る必要はなく、同一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" } }

新規作成の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" } }

値をサービス名のサフィックスだけにして後でリージョン部分と結合する設計にすることで、リージョン移植性が高まります。新しいエンドポイントを追加するときはこのマップに1行追加するだけです。

2. for_eachでエンドポイントを束ねる

for_eachlocal.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" } }

Terraformはキー(ssmecr_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" } }

3AZへの分散は可用性を高めますが、ENI1本あたりの時間課金が3倍になります。コストを抑えたい場合は2AZのみに絞る設計も一般的です。本番環境では可用性要件と費用のトレードオフで判断してください。

トラブルシューティング|よくあるエラーと対処法

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_supportfalse になっているケースがほとんどです。次の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で反映する

Terraformで管理している場合は 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 | +--------------------------+----------+------------------------------+

Stateavailable であればエンドポイント自体は正常です。接続できない場合はセキュリティグループのルールを再確認してください。

本記事のまとめ

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でリージョン部分を動的に組み立てる
Interface型エンドポイントは1サービス1リソースの宣言が必要ですが、for_eachを使えばサービス名リストの追加1行で新しいエンドポイントを全環境に展開できます。セキュリティグループ・サブネット配置・プライベートDNSの3点セットを最初に正しく設計しておくことで、サービス追加時の作業量を最小限に抑えられます。
現場で通用するTerraformのモジュール設計やfor_each活用パターンを体系的に習得したい方へ、VPCエンドポイント設計を含むAWSインフラのIaC化をハンズオン形式で学べるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。