PrivateLinkのマルチアカウント設計、VPCピアリングから移行して気づいたこと
10超えのVPCが混在する環境でVPCピアリングの限界にぶつかった経験から、PrivateLink中心の設計に移行。エンドポイント管理の落とし穴やコスト感まで実務で得た知見をまとめました。
PrivateLinkに本気で向き合わざるを得なかった経緯
先日、マルチアカウント環境で10を超えるVPCが混在するシステムのネットワーク設計を丸ごと見直すプロジェクトを任された。それまで「VPCピアリングで済む話じゃないの?」と正直思ってたんだけど、実際に運用してみると思ったより奥が深くて、かなりハマった。
特に困ったのが、VPCピアリングのCIDRオーバーラップ問題と、サービスチームが増えるたびにルートテーブルの管理が指数関数的に増える問題だ。10アカウントあると、フルメッシュで接続しようとすると理論上45本のピアリングが必要になる。これを手動で管理してた時期は毎週どこかのルートが漏れてインシデントになってた。
その反省から、PrivateLinkを中心に据えた設計に移行した。AWS Organizations運用1年半で学んだSCP・RCP設計と組み合わせると効果が出やすいので、マルチアカウント運用をやってる人はあわせて読んでみてほしい。
PrivateLinkの3大設計パターンを整理する
実際に設計する前に、PrivateLinkには大きく3つのパターンがあることを頭に入れておきたい。どれが最適かはワークロードの性質とアカウント数次第なので、一概に「これが正解」とは言えないんだけど、判断基準はある程度見えてきた。
| パターン | 接続モデル | 向いているケース | 注意点 |
|---|---|---|---|
| Interface Endpoint(Consumer側) | ConsumerがPrivate NIC経由でServiceにアクセス | SaaS/AWSマネージドサービス利用 | エンドポイントNIC分のENI料金が発生 |
| PrivateLink + NLB(Provider側) | ProviderがNLBを前段に置いてエンドポイントサービス公開 | 社内共有サービスの提供 | NLBの追加コスト、ヘルスチェック設計が重要 |
| Gateway Endpoint | S3/DynamoDBへのルートベース転送 | S3/DynamoDBのみ | 他サービスには使えない |
正直ここで最初に間違えた。社内の認証サービスをマルチアカウントに公開したいケースで、最初はInterface Endpointだけ作ればいいと思ってたんだけど、Provider側はエンドポイントサービス(NLB + PrivateLink)を作る必要があるというのが抜けてて、設計を最初からやり直すことになった。痛い授業料だった。
実際のアーキテクチャ:マルチアカウント共有サービス公開パターン
一番本番で使っているのが、下記の「Hub-and-Spoke with PrivateLink」構成だ。
graph TB
subgraph shared["Shared Services Account"]
subgraph vpc_shared["VPC (10.0.0.0/16)"]
subgraph az_a["AZ-a"]
NLB_a["NLB Target\n(10.0.1.x)"]
svc_a["Auth Service\nECS Task"]
end
subgraph az_b["AZ-b"]
NLB_b["NLB Target\n(10.0.2.x)"]
svc_b["Auth Service\nECS Task"]
end
NLB["Network Load Balancer"]
VPCE_SVC["VPC Endpoint Service\n(com.amazonaws.vpce.xxx)"]
NLB --> NLB_a
NLB --> NLB_b
NLB_a --> svc_a
NLB_b --> svc_b
NLB --> VPCE_SVC
end
end
subgraph consumer_a["Consumer Account A"]
subgraph vpc_a["VPC (172.16.0.0/16)"]
EP_A["Interface Endpoint\n(10.0.x.x ENI)"]
APP_A["Application\nLambda/ECS"]
APP_A -->|"HTTPS 443"| EP_A
end
end
subgraph consumer_b["Consumer Account B"]
subgraph vpc_b["VPC (192.168.0.0/16)"]
EP_B["Interface Endpoint\n(10.0.x.x ENI)"]
APP_B["Application\nEKS Pod"]
APP_B -->|"HTTPS 443"| EP_B
end
end
EP_A -->|"PrivateLink"| VPCE_SVC
EP_B -->|"PrivateLink"| VPCE_SVC
この構成の何が嬉しいかというと、Consumer側のCIDRが何であっても関係ない点だ。ピアリングだとCIDRオーバーラップが致命的だけど、PrivateLinkはL4転送なのでIPが被っていても問題ない。10アカウントが乱立した環境には本当に助かった。
エンドポイントポリシーとドメイン解決、この2点でほぼ詰まる
設計パターンが決まったあと、実装で一番詰まったのがエンドポイントポリシーとプライベートDNS解決の2点だった。正直、公式ドキュメントを読んでもピンとこなくて、実際にデプロイして壊しながら理解した部分が大きい。
エンドポイントポリシーの設計
Interface Endpointにはリソースポリシーを付与できる。デフォルトはフルアクセスなんだけど、本番環境でそのままにしておくのはセキュリティ的にまずい(SOC2審査でCloudTrail・Config設計が半年泥沼になった実録でも触れたけど、監査指摘の定番だ)。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificRoles",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::123456789012:role/AppRole",
"arn:aws:iam::987654321098:role/ServiceRole"
]
},
"Action": [
"execute-api:Invoke"
],
"Resource": "arn:aws:execute-api:ap-northeast-1:*:*/*"
},
{
"Sid": "DenyNonTLS",
"Effect": "Deny",
"Principal": "*",
"Action": "*",
"Resource": "*",
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
このポリシーは「特定のIAMロールからのみ許可、かつTLS必須」という最低限の設定だ。ここにaws:SourceVpc条件を追加するとさらに絞れるんだけど、オンプレ経由のアクセスが入るケースだとSourceVpcが取れないことがあって少し悩ましい。
プライベートDNS解決の設計
AWSマネージドサービス(例:execute-api.ap-northeast-1.amazonaws.com)へのInterface Endpointでは、enableDnsHostnamesとenableDnsSupportをVPCに有効化した上でプライベートDNSを有効にすれば、アプリから見てエンドポイントURLを変更しなくて済む。
一方、社内サービス(カスタムエンドポイントサービス)のケースではプライベートDNSは自動では効かないので、Route 53 Private Hosted Zoneを手動で管理する必要がある。ここを知らずに「PrivateLinkにしたのにDNSが解決できない」ってなって1時間溶かした。個人的にはこれが一番「ドキュメントに書いてあるのに見落としがち」なポイントだと思う。
# Terraform例: プライベートDNSゾーン + エイリアスレコード
resource "aws_route53_zone" "private" {
name = "auth.internal.example.com"
vpc {
vpc_id = aws_vpc.consumer.id
}
}
resource "aws_route53_record" "endpoint" {
zone_id = aws_route53_zone.private.zone_id
name = "auth.internal.example.com"
type = "A"
alias {
name = aws_vpc_endpoint.auth.dns_entry[0].dns_name
zone_id = aws_vpc_endpoint.auth.dns_entry[0].hosted_zone_id
evaluate_target_health = true
}
}
これでConsumer側のアプリは https://auth.internal.example.com で普通にHTTPS通信できるようになる。地味だけど、ここをちゃんと整備しておくとサービスディスカバリが格段に楽になるし、アプリ側のコード変更も最小限で済む。
ハイブリッド接続(オンプレ←→AWS)でのPrivateLink活用
個人的にここが一番興味深かったし、設計難易度も高かった。オンプレとAWSをDirect Connectで接続している環境で、オンプレ側からAWSのPrivateLinkサービスにアクセスするパターンだ。
PrivateLinkのInterface EndpointはVPC内のENIとして作られるので、DirectConnect経由でVPCに到達できれば理論上はオンプレからも使える。ただし条件がある。
flowchart LR
subgraph onprem["オンプレミス"]
srv["アプリサーバー\n10.100.x.x"]
resolver_out["Route53 Resolver\nOutbound Endpoint"]
end
subgraph dc["Direct Connect"]
vif["Private VIF"]
end
subgraph aws["AWS (Transit Gateway経由)"]
subgraph hub_vpc["Hub VPC"]
tgw["Transit Gateway"]
resolver_in["Route53 Resolver\nInbound Endpoint"]
end
subgraph svc_vpc["Service VPC"]
ep_eni["Interface Endpoint\n(ENI)"]
nlb["NLB"]
svc["マイクロサービス"]
end
end
srv -->|"DNS: auth.internal"| resolver_out
resolver_out -->|"Forwarder"| vif
vif --> tgw
tgw --> resolver_in
resolver_in -->|"Private Hosted Zone解決"| ep_eni
srv -->|"TCP通信"| vif
vif --> tgw
tgw --> ep_eni
ep_eni --> nlb
nlb --> svc
DNS解決が鍵で、オンプレのDNSサーバーからAWSのRoute53 Resolver Inbound Endpointへフォワードする設定が必要になる。これを組まないとオンプレ側から auth.internal.example.com が解決できない。DNS周りの設定ミスで半日つぶした記憶があるので、ここは早めに動作確認を取ったほうがいい。
Direct Connect + Transit Gateway + PrivateLink という組み合わせのコスト感はこのくらいになる(東京リージョン、月次概算):
xychart-beta
title "PrivateLink構成コスト比較(月次・東京リージョン概算, USD)"
x-axis ["VPCピアリング\n(5接続)", "PrivateLink\n(5エンドポイント)", "PrivateLink\n+TGW", "PrivateLink\n+TGW+DX"]
y-axis "月次コスト (USD)" 0 --> 1200
bar [45, 360, 560, 1050]
一見PrivateLinkは高く見えるんだけど、データ転送量が多いワークロードだとピアリングも転送コストが積み上がるし、運用コスト(ルートテーブルの管理工数)を加味すると十分元が取れることが多い。ただこれは正直ワークロード次第で、「PrivateLinkにすれば絶対安くなる」とは言えないのが難しいところだ。
実運用で見えてきたエンドポイント管理の課題
3ヶ月ほど本格運用してみて、設計よりも日常運用のほうが地味にしんどいという現実がある。特に以下の3点でハマった。
1. エンドポイントの増殖問題
AWSのマネージドサービスを使うたびにInterface Endpointを追加していくと、あっという間に1VPCあたり20〜30個になる。各エンドポイントは1AZあたり$0.01/時間かかるので、3AZ構成なら1エンドポイント約$21.6/月。30個あると月650ドル以上になる。気づいたら「エンドポイント代だけで結構な額になってる」という状況は普通に起きる。
うちのチームで実践したのはエンドポイントの集約VPCパターンで、中央のHub VPCにエンドポイントをまとめてTransit Gateway経由で各VPCからアクセスさせる構成にした。これで各ConsumerはTGWアタッチメント料金だけ払えばよく、エンドポイントを重複して作らなくて済む。
2. ヘルスチェック設計
NLBをProviderとして使う場合、ターゲットのヘルスチェックが失敗するとエンドポイントサービス全体が使えなくなる。最初はHTTPのヘルスチェックをデフォルト設定(30秒間隔、3回失敗でアンヘルシー)で運用してたんだけど、デプロイ中にヘルスチェック失敗→接続断という事象が発生した。デフォルト設定を信用しすぎるのは危ない。
resource "aws_lb_target_group" "service" {
name = "auth-service-tg"
port = 443
protocol = "TCP"
target_type = "ip"
vpc_id = var.vpc_id
health_check {
enabled = true
protocol = "HTTPS"
path = "/health"
port = "traffic-port"
healthy_threshold = 2
unhealthy_threshold = 2 # 早めに検知
interval = 10 # 10秒間隔に短縮
timeout = 5
}
deregistration_delay = 30 # デプロイ時のドレインを短く
}
これで無停止デプロイ中の接続断がほぼ出なくなった。deregistration_delayを短くするのが地味に効いている。デフォルトの300秒のままだとデプロイのたびにヒヤヒヤすることになるので、早めに調整しておくことを強くすすめる。
3. CloudTrailでのPrivateLink経由アクセス追跡
インシデント対応の最新ベストプラクティス2026でも触れられているけど、セキュリティインシデント発生時にPrivateLink経由のアクセスを追跡できる体制が必要だ。CloudTrailはエンドポイント経由のAPI呼び出しでもsourceIPAddressにENIのプライベートIPが記録されるので、ENIのIDとIPを管理しておくことが重要になる。
うちでは以下のLambdaで週次で棚卸しをかけている:
import boto3
import json
from datetime import datetime
def lambda_handler(event, context):
ec2 = boto3.client('ec2', region_name='ap-northeast-1')
# Interface Endpointの一覧取得
endpoints = ec2.describe_vpc_endpoints(
Filters=[{'Name': 'vpc-endpoint-type', 'Values': ['Interface']}]
)['VpcEndpoints']
inventory = []
for ep in endpoints:
if ep['State'] != 'available':
continue
# ENI情報を取得
for dns in ep.get('DnsEntries', []):
inventory.append({
'endpoint_id': ep['VpcEndpointId'],
'service_name': ep['ServiceName'],
'vpc_id': ep['VpcId'],
'dns_name': dns.get('DnsName', ''),
'created_at': ep['CreationTimestamp'].isoformat(),
'tags': {t['Key']: t['Value'] for t in ep.get('Tags', [])}
})
# S3に棚卸し結果を保存
s3 = boto3.client('s3')
s3.put_object(
Bucket='your-audit-bucket',
Key=f"endpoint-inventory/{datetime.now().strftime('%Y-%m-%d')}.json",
Body=json.dumps(inventory, indent=2, default=str),
ContentType='application/json'
)
print(f"Inventoried {len(inventory)} endpoint DNS entries")
return {'statusCode': 200, 'count': len(inventory)}
これをConfig Ruleと組み合わせて、タグのないエンドポイントをアラートするようにしている。AWS Organizations セキュリティ設計|マルチアカウント統制と自動コンプライアンスで書かれているような予防的統制と組み合わせると効果が出やすい。
2026年時点での設計選択基準まとめ
正直、PrivateLinkは「使えば全部解決」じゃなくて、向き不向きがはっきりある。実際に運用して見えてきた選択基準をフロー図にまとめた。
flowchart TD
A["サービス間通信を設計したい"] --> B{"CIDRオーバーラップの\n可能性がある?"}
B -->|"あり"| C["PrivateLink一択"]
B -->|"なし"| D{"双方向通信が\n必要?"}
D -->|"双方向必要"| E["VPCピアリング or TGW"]
D -->|"一方向でOK"| F{"提供サービスが\n複数Consumerから使われる?"}
F -->|"Yes"| G["PrivateLink\n(Endpoint Service)"]
F -->|"No (1:1)"| H{"オンプレ接続\nも必要?"}
H -->|"Yes"| I["TGW + PrivateLink\nハイブリッド構成"]
H -->|"No"| J["VPCピアリングで十分"]
C --> K["NLB + Endpoint Service設計"]
G --> K
ピアリングのほうがシンプルで安い場面も普通にあるので、「モダンだからPrivateLink」という選び方はやめたほうがいい。これは最初のプロジェクトで全部PrivateLinkに移行しようとして、結局一部はピアリングに戻した実体験から言っている。「ツールを使うことが目的になってしまった」という典型的な失敗パターンだった。
皆さんのチームはどういう判断基準で使い分けてます?特にマルチアカウント + オンプレのハイブリッドをやってる人の知見が聞きたいところ。
まとめ
PrivateLink設計で1年運用してわかったことを整理すると、大体この5点に集約される。
| # | ポイント | ひとこと補足 |
|---|---|---|
| 1 | CIDRオーバーラップが発生しうるマルチアカウント環境にはPrivateLinkが最適解 | ルートテーブル管理の運用コストも大幅に削減できる |
| 2 | エンドポイントの集約(Hub VPC)パターンでENIコストを抑える | Consumer VPCごとにエンドポイントを作ると費用が爆増するので要注意 |
| 3 | プライベートDNS解決の設計はカスタムサービスとAWSマネージドで挙動が異なる | オンプレ接続を含む場合はRoute53 Resolver Inbound Endpointとのセットが必須 |
| 4 | NLBのヘルスチェック設定はデフォルトのままにしない | interval=10秒、unhealthy_threshold=2が現実的な設定 |
| 5 | エンドポイントの棚卸し自動化はセキュリティ監査と費用管理の両面で必須 | 放置するとゾンビエンドポイントが増えてコストが膨らむ |
次のアクション:まず既存環境のエンドポイント棚卸しスクリプトを動かしてみることをすすめる。思った以上に「誰が作ったかわからないエンドポイント」が発見されるはずで、そこから設計の見直しが始まると思う。Terraformのステート管理と合わせて整理するとなおよし(Terraformで3年分の失敗から学んだ、State管理とAI検証の正解も参考になるので読んでみてほしい)。