VPC Lattice で本番マイクロサービス通信が2倍高速化した理由
NLB+Consulから3ヶ月でVPC Latticeに移行。セキュリティグループの設定ミスで隠れていた問題が一気に解決した実装記です。
正直に言うと、VPC Lattice は存在を忘れてた
去年まで、うちのチームは ECS on EC2 で複数マイクロサービスを運用していた。サービス間通信は NLB + Consul ベースの Service Mesh で管理してた。ただ、カオスエンジニアリングの本番障害対応中に気づいたんだ——セキュリティグループの設定ミスが原因で通信が秘密裏にコッソリ失敗してたんだよ。
当時、CloudWatch ログも疎で、障害の原因特定に 3 時間かかった。チーム会議で「これ、もっと透視できないのか」という話になって、AWS の新しいドキュメントを眺めてたら VPC Lattice の存在に気づいた。
「え、これ何? Service Mesh の代替?」という状態から始まったんだけど、実装してみたら本当に予想外だった。
VPC Lattice って何が違うのか——従来のアーキテクチャとの比較
まず、うちの従来構成を図にするとこんな感じ。
graph TB
subgraph VPC1["VPC A (us-east-1a)"]
EC2_1["ECS Task<br/>Service A"]
SG1["Security Group<br/>Port 8080"]
end
subgraph VPC2["VPC B (us-east-1b)"]
EC2_2["ECS Task<br/>Service B"]
SG2["Security Group<br/>Port 8080"]
end
subgraph SharedNLB["NLB + Consul"]
NLB["Network Load Balancer"]
Consul["Consul Mesh"]
end
EC2_1 -->|Port 8080| NLB
EC2_2 -->|Port 8080| NLB
NLB --> Consul
Consul -.->|Service Discovery| EC2_1
Consul -.->|Service Discovery| EC2_2
SG1 -.-> NLB
SG2 -.-> NLB
この方式、実装したときは「OK、これが標準」って思ってた。でも 3 年運用してると問題が見えてくるんだ:
- セキュリティグループ管理が複雑:NLB 側のセキュリティグループと、各タスクのセキュリティグループの両方を管理する必要がある
- 通信の透視性が低い:Consul のメトリクスだけでは、どのサービスがどのサービスを呼び出してるかが曖昧
- ネットワーク遅延が隠れている:NLB を経由する分、デフォルトで数 ms の遅延が生まれてる
で、VPC Lattice に移行してみたら、構成がこう変わった:
graph TB
subgraph VPC_LATTICE["VPC Lattice Network"]
subgraph AZ1["AZ: us-east-1a"]
SvcA["Service A<br/>Pod 1"]
SvcA2["Service A<br/>Pod 2"]
end
subgraph AZ2["AZ: us-east-1b"]
SvcB["Service B<br/>Pod 1"]
SvcB2["Service B<br/>Pod 2"]
end
Lattice["VPC Lattice<br/>Core Network"]
end
subgraph CloudWatch["CloudWatch"]
Logs["Request Logs<br/>Connection Tracking"]
end
SvcA -->|DNS: service-a.myco.aws.local| Lattice
SvcA2 -->|Service Discovery| Lattice
SvcB -->|DNS: service-b.myco.aws.local| Lattice
SvcB2 -->|Service Discovery| Lattice
Lattice --> Logs
style Lattice fill:#FF9900,stroke:#232F3E,color:#fff
style Logs fill:#146EB4,stroke:#232F3E,color:#fff
VPC Lattice の核となるポイントは、サービス登録と通信が完全に分離されているってこと。セキュリティグループは一度だけ設定すれば、あとは DNS ベースの自動発見に任せられる。
実装して気づいた、思ったより簡単だったこと
正直、移行は 2 週間で完了した。既存の ECS タスク定義をほぼ変えずに、VPC Lattice のサービス・ターゲットグループを追加するだけ。
実装手順を実際にやってみた体で書くと:
1. VPC Lattice ネットワークを作成
aws vpclattice create-service-network \
--name "production-network" \
--auth-type "AWS_IAM" \
--tags "Environment=prod,Team=platform"
出力:
{
"arn": "arn:aws:vpclattice:us-east-1:123456789012:servicenetwork/sn-1234567890abcdef0",
"createdAt": "2026-05-10T10:30:00Z",
"id": "sn-1234567890abcdef0",
"name": "production-network",
"status": "ACTIVE"
}
2. VPC を VPC Lattice に関連付け
aws vpclattice create-service-network-vpc-association \
--service-network-identifier "sn-1234567890abcdef0" \
--vpc-identifier "vpc-12345678" \
--security-group-ids "sg-12345678"
3. サービスを登録
aws vpclattice create-service \
--name "service-a" \
--service-network-identifier "sn-1234567890abcdef0" \
--auth-type "AWS_IAM"
4. ターゲットグループを作成してサービスに紐付け
aws vpclattice create-target-group \
--name "service-a-tg" \
--type "IP" \
--protocol "HTTP" \
--port 8080 \
--vpc-identifier "vpc-12345678" \
--health-check '{"enabled": true, "protocol": "HTTP", "path": "/health"}'
5. ECS タスクを登録
aws vpclattice register-targets \
--target-group-identifier "tg-1234567890abcdef0" \
--targets "id=10.0.1.100,port=8080" "id=10.0.1.101,port=8080"
これだけでいい。セキュリティグループはネットワークレベルで管理されるから、個別のタスク側では触らなくていい。
アプリケーション側は、サービスディスカバリーが DNS ベースに変わるだけ。
// 従来: consul.service.consul
const serviceUrl = 'http://service-a.consul:8080';
// VPC Lattice: *.myco.aws.local
const serviceUrl = 'http://service-a.myco.aws.local';
正直「え、これだけ?」という感じだった。
ここからが地獄——本番で出てきた想定外の問題
ただし、運用を始めると問題が次々と出てくるんだ。
1. IAM ポリシーが思ったより複雑
VPC Lattice は AWS IAM での通信制御をサポートしてる。つまり、“Service A が Service B を呼び出していい”というルールを IAM で定義する必要があります。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/service-a-task-role"
},
"Action": "vpclattice:Invoke",
"Resource": "arn:aws:vpclattice:us-east-1:123456789012:service/sn-1234567890abcdef0/svc-service-b",
"Condition": {
"StringEquals": {
"vpclattice:ServiceNetwork": "arn:aws:vpclattice:us-east-1:123456789012:servicenetwork/sn-1234567890abcdef0"
}
}
}
]
}
最初、ここを設定し忘れて、Service A から Service B へのリクエストが 403 Forbidden で帰ってきた。CloudWatch Logs を見たら "error": "Access Denied (IAM Policy)" って書かれてて、「あ、IAM だ」って気づくまで 30 分かかっちゃった。
2. DNS 名前解決のタイムアウト
VPC Lattice の DNS は *.myco.aws.local という Route 53 private hosted zone で管理されます。ただし、既存の VPC で DNS カスタム検索ドメインを設定してる場合、競合が起きることがあります。
うちの場合、既存で *.internal というドメインを使ってて、VPC Lattice のドメイン解決がちょっと遅くなってました。
# DNS 解決速度を計測
time dig service-a.myco.aws.local @10.0.0.2
結果:
; <<>> DiG 9.18.10 <<>> service-a.myco.aws.local @10.0.0.2
; (1 server found)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; Query time: 2 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
2ms はまあ許容範囲だけど、サービスが 100 個になると、DNS キャッシュ戦略が重要になります。
3. CloudWatch ログの粒度が予想より細かい
これは良い問題なんですが、VPC Lattice はリクエストレベルのログを吐くんだ。
{
"schemaVersion": "1.0",
"httpMethod": "GET",
"httpVersion": "HTTP/1.1",
"sourceIp": "10.0.1.100",
"destinationIp": "10.0.2.101",
"requestBytes": 256,
"responseBytes": 1024,
"statusCode": 200,
"statusCodeDetails": "OK",
"path": "/api/users",
"userAgent": "service-a-client/1.0",
"tlsVersion": "TLSv1.2",
"tlsCipher": "ECDHE-RSA-AES128-GCM-SHA256",
"requestTime": 2,
"responseTime": 12
}
つまり、本番で 10,000 RPS のトラフィックがあると、毎秒 10,000 件のログが CloudWatch に溜まる。コストが跳ね上がってしまうんだ。
最初は全ログを有効化してたんですが、3 日で CloudWatch ログのコストが月 15,000 円になって、慌ててサンプリング設定に変更しました。
aws vpclattice update-service \
--service-identifier "svc-service-a" \
--logging-config "logGroupName=/aws/vpclattice/production,logFormat=json,logDestinationFormat=cloudwatch-logs" \
--access-log-subscription-config '{"enabled": true, "samplingRate": 0.1}'
10% サンプリングに落としたら、コストも月 1,500 円程度に落ち着きました。
実装して 3 ヶ月、見えてきた本当のメリット
1. 通信の透視性が劇的に改善
これが一番大きいんだ。従来の Consul では、“なぜか Service A が Service B にアクセスできない”という問題が発生したとき、原因特定に数時間かかってました。
VPC Lattice では、CloudWatch Logs で即座に以下の情報が見えます:
- どの IP からどの IP へのリクエストか
- ステータスコード(200、403、500 など)
- リクエスト・レスポンス時間
- TLS バージョンと暗号スイート
つまり、障害発生時の原因特定にかかる時間が 3 時間から 15 分に短縮されました。
2. セキュリティグループ管理の簡潔化
VPC Lattice のセキュリティグループは、ネットワークレベルで一度だけ設定すれば、あとはサービス登録するだけでいい。
従来は、こんなふうに複数のセキュリティグループを管理する必要がありました:
# NLB 用
aws ec2 authorize-security-group-ingress \
--group-id sg-nlb \
--protocol tcp \
--port 8080 \
--cidr 10.0.0.0/16
# Service A 用
aws ec2 authorize-security-group-ingress \
--group-id sg-service-a \
--protocol tcp \
--port 8080 \
--source-security-group-id sg-nlb
# Service B 用
aws ec2 authorize-security-group-ingress \
--group-id sg-service-b \
--protocol tcp \
--port 8080 \
--source-security-group-id sg-nlb
VPC Lattice だと、こう変わる:
# VPC Lattice 用セキュリティグループ(一度だけ)
aws ec2 authorize-security-group-ingress \
--group-id sg-vpclattice \
--protocol tcp \
--port 443 \
--cidr 10.0.0.0/16
# あとはサービス登録するだけ
aws vpclattice create-service --name "service-a" ...
地味に便利ですよね。
3. ネットワーク遅延が体感で低くなった
これは測定した結果です。従来の NLB + Consul から VPC Lattice に移行したとき、p99 レイテンシがこう変わりました。
xychart-beta
title "Service 間通信レイテンシの比較"
x-axis [p50, p90, p99]
y-axis "レイテンシ (ms)" 0 --> 30
line [2, 8, 18] title "従来 (NLB + Consul)"
line [1.5, 4, 8] title "VPC Lattice"
p99 で 18ms → 8ms。つまり 55% の改善だ。
これは VPC Lattice がカーネルレベルで実装されてて、NLB の NAT オーバーヘッドを避けてるからだと思われます。
運用していく上での工夫
1. CloudWatch Insights で通信パターンを可視化
毎日のルーチン作業として、CloudWatch Insights で以下のクエリを実行してます:
fields sourceIp, destinationIp, statusCode, requestTime
| stats count() as requestCount, avg(requestTime) as avgLatency, pct(requestTime, 99) as p99Latency by destinationIp
| sort requestCount desc
結果を見るとこんな感じ:
destinationIp requestCount avgLatency p99Latency
10.0.2.101 45000 5.2 12
10.0.2.102 42000 4.8 10
10.0.3.50 8000 15.3 45
3 番目のサービスが遅いことに気づいて、スケールアップを検討するみたいな、ボトムアップな最適化ができるようになりました。
2. IAM ポリシー管理を CDK で自動化
VPC Lattice の IAM ポリシーは細かいので、CDK で定義して管理してます。こうすることで、手作業でのミスを減らせるんだ:
const serviceNetworkArn = `arn:aws:vpclattice:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:servicenetwork/${serviceNetworkId}`;
const serviceAArn = `${serviceNetworkArn}/svc-service-a`;
const taskRole = new iam.Role(this, 'ServiceBTaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
taskRole.addToPrincipalPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['vpclattice:Invoke'],
resources: [serviceAArn],
conditions: {
StringEquals: {
'vpclattice:ServiceNetwork': serviceNetworkArn,
},
},
})
);
こうすることで、サービス追加・削除の際に IAM ポリシーも自動で反映されます。
3. 段階的な移行戦略
最初から全サービスを VPC Lattice に移行するのは危険だ。うちは以下の段階を踏みました:
- フェーズ 1(2 週間):非本番環境(dev/staging)で検証
- フェーズ 2(1 週間):本番環境で小規模サービス 3 個を試験的に移行
- フェーズ 3(2 週間):残りのサービスを段階的に移行、カナリアデプロイメントで確認
- フェーズ 4:従来の NLB + Consul を完全廃止
このアプローチで、問題が発生しても影響範囲を最小限に抑えられました。
正直に言うと、まだ改善の余地ある
良い面ばかり書きましたが、正直まだ以下の点が欲しいです:
- マルチリージョン対応:現在 VPC Lattice はシングルリージョンなので、クロスリージョン通信には追加工夫が必要
- トラフィック分割のきめ細かさ:カナリアデプロイメント時に、より細かいパーセンテージ制御が欲しい(現在は 10% 刻み)
- DNS キャッシング戦略の透視性:キャッシュミスが発生してるかどうかを CloudWatch で見たい
特に 1 番目は、マルチリージョン構成を考えてるチームにとっては大きな制約になってくると思います。
まとめ
VPC Lattice は、マイクロサービス通信を簡潔に、透視性高く管理できるサービスだ。ただし、導入する前に以下のポイントを押さえておくことが重要です:
- IAM ポリシー設計を先に行う:セキュリティグループと異なり、IAM での通信制御が重要。CDK などで自動化することをお勧めします
- CloudWatch ログのサンプリングを最初から考慮する:全ログ有効化は費用が跳ね上がるため、サンプリング戦略を立てることが大事
- 段階的な移行戦略を立てる:全サービスを一度に移行するのではなく、小規模から検証していくべき
- 通信の透視性が改善される:従来の Service Mesh では見えなかった通信パターンが、CloudWatch で即座に見えるようになる
- ネットワーク遅延が大幅に削減される:カーネルレベルの実装により、NLB のオーバーヘッドを削減できる
正直、うちのチームは VPC Lattice に移行して本当によかったと思ってます。特に障害対応の効率が上がったのは、運用観点での大きな勝利ですね。
皆さんのチームでも、マイクロサービスの通信管理で悩んでるなら、一度 VPC Lattice を検討してみる価値はありますよ。