Route 53 Resolverで本番火災を経験した話|マルチVPC運用の落とし穴と解決策
マイクロサービス環境でRoute 53 Resolver導入後に直面した名前解決の遅延問題。半年の運用で学んだ設定ミスと実装コードを本音で共有します。
先日チームでRoute 53 Resolverの本格導入を始めたんですが、正直ハマることが多くてね
僕たちのチームは元々、VPC内のDNS解決を各VPCごとに独立させていたんですよ。でも、マイクロサービスのサイロ化が進むにつれて「別VPCのサービスが見えない」「オンプレのリソースが呼べない」という問題が増えてきた。だから、Route 53 Resolverを導入してハイブリッドDNS環境を統一しようってことになったわけです。
結論から言うと、2026年時点でもRoute 53 Resolverは「組み込み」の概念が複雑で、設計を誤ると本番で火が出ます。個人的には、マルチアカウント環境でのResolver共有ルールとVPCエンドポイント戦略の組み合わせが重要だと感じた。正直まだ検証中だけど、ここまでの知見をシェアしたい。
Route 53 Resolverの基本設定で最初につまずいた話
Route 53 Resolverって、VPC内で自動的にAWS管理のリソース(例:ECS、RDS、ALB)のDNS解決を行うシステムなんですが、オンプレミスや他VPCへのアウトバウンドクエリをコントロールするには「Resolver ルール」を設定する必要があります。
うちのチームが最初に犯した失敗は「Resolver ルールを全部Central Accountに集約しようとした」こと。確かに管理は楽だけど、各VPCから見えやすいエンドポイント設定がないと、名前解決の遅延が10倍になりました。実際のコードを見たら、こんな感じだったんです。
# ❌ 最初の実装:Central Accountに全集約(失敗パターン)
resource "aws_route53_resolver_endpoint" "central" {
name = "central-resolver"
direction = "OUTBOUND"
security_group_ids = [aws_security_group.resolver_sg.id]
ip_address_specification {
subnet_id = aws_subnet.central_az1.id
}
ip_address_specification {
subnet_id = aws_subnet.central_az2.id
}
}
# オンプレミスへのルール
resource "aws_route53_resolver_rule" "onprem" {
name = "onprem.internal"
domain_name = "onprem.internal"
rule_type = "FORWARD"
resolver_endpoint_id = aws_route53_resolver_endpoint.central.id
target_ip {
ip = "10.100.0.1" # オンプレのDNSサーバ
}
}
# 全VPCに関連付け(これが非効率)
resource "aws_route53_resolver_rule_association" "all_vpcs" {
for_each = var.all_vpcs
resolver_rule_id = aws_route53_resolver_rule.onprem.id
vpc_id = each.value.id
}
この構成で何が起きたかというと、**「各VPCのResolver ルールクエリが全部Central VPCを経由する」**ことになって、Central VPCがボトルネックになったんです。さらに悪いことに、Central VPCのエンドポイントサブネットに障害があると、全社的にDNS解決が止まる。監視も追跡も大変。
そこで、僕たちが改善したのが「各VPC内にローカルResolverエンドポイントを配置して、組織内共有ルール経由でCentralの設定を参照する」という階層構造ですね。
# ✅ 改善版:各VPCにローカルエンドポイントを配置
# VPC A(Dev環境)
resource "aws_route53_resolver_endpoint" "vpc_a_outbound" {
name = "vpc-a-resolver"
direction = "OUTBOUND"
security_group_ids = [aws_security_group.resolver_sg.id]
ip_address_specification {
subnet_id = aws_subnet.vpc_a_az1.id
}
ip_address_specification {
subnet_id = aws_subnet.vpc_a_az2.id
}
}
# VPC B(Prod環境)
resource "aws_route53_resolver_endpoint" "vpc_b_outbound" {
name = "vpc-b-resolver"
direction = "OUTBOUND"
security_group_ids = [aws_security_group.resolver_sg.id]
ip_address_specification {
subnet_id = aws_subnet.vpc_b_az1.id
}
ip_address_specification {
subnet_id = aws_subnet.vpc_b_az2.id
}
}
# Resolver共有ルール(Central Accountで管理)
resource "aws_route53_resolver_rule" "shared_onprem" {
name = "shared-onprem.internal"
domain_name = "onprem.internal"
rule_type = "FORWARD"
resolver_endpoint_id = aws_route53_resolver_endpoint.central.id
target_ip {
ip = "10.100.0.1"
}
}
# Organizations ポリシーで全VPCに自動適用
resource "aws_route53_resolver_rule_association" "org_shared" {
for_each = var.org_member_vpcs
resolver_rule_id = aws_route53_resolver_rule.shared_onprem.id
vpc_id = each.value.id
}
これでも足りなくて、後から「VPC間通信のDNS遅延」も見えてきました。理由は簡単で、Route 53 Resolverのインバウンドエンドポイント設定がなかったからなんです。別VPCから「vpc-b.internal」を解決したいときに、Route 53 Private Hosted Zoneを参照できる状態が作られてなかったんですよ。
インバウンドエンドポイント設定で「見えない資源問題」が解決した
Route 53 Resolverには、実は2種類のエンドポイントがあるんです。それぞれの役割は全然違う。
| エンドポイント種別 | 方向 | 用途 | 例 |
|---|---|---|---|
| Outbound | VPC → 外部 | VPC内から外部(オンプレ、他VPC)へのクエリ送信 | VPC AがオンプレのDNSを参照、別VPCのPrivate Zoneを参照 |
| Inbound | 外部 → VPC | 外部(オンプレ、他VPC)からのクエリを受信してPrivate Hosted Zoneを参照 | VPC Aが「vpc-b.internal」というクエリをVPC Bへ送信し、VPC Bが応答 |
うちが最初に見落としてた部分は「Inbound Endpoint」でした。各VPCのResolverが他VPCのリソースを解決するには、対象VPCでインバウンドエンドポイントを立てて、そこに別VPCからクエリを送信させる必要があります。これがないと、マジで見えない資源が生まれるんですよ。
# VPC B(Prod)のインバウンドエンドポイント
resource "aws_route53_resolver_endpoint" "vpc_b_inbound" {
name = "vpc-b-inbound"
direction = "INBOUND"
security_group_ids = [aws_security_group.resolver_inbound_sg.id]
ip_address_specification {
subnet_id = aws_subnet.vpc_b_az1.id
}
ip_address_specification {
subnet_id = aws_subnet.vpc_b_az2.id
}
}
# Private Hosted Zone(VPC Bのリソース用)
resource "aws_route53_zone" "vpc_b_internal" {
name = "vpc-b.internal"
vpc {
vpc_id = aws_vpc.vpc_b.id
}
}
# VPC A から VPC B の内部リソースを見るためのOutboundルール
resource "aws_route53_resolver_rule" "vpc_a_to_vpc_b" {
name = "vpc-b.internal-forward"
domain_name = "vpc-b.internal"
rule_type = "FORWARD"
resolver_endpoint_id = aws_route53_resolver_endpoint.vpc_a_outbound.id
# VPC B のインバウンドエンドポイントのIPを指定
target_ip {
ip = aws_route53_resolver_endpoint.vpc_b_inbound.ip_addresses[0]
}
target_ip {
ip = aws_route53_resolver_endpoint.vpc_b_inbound.ip_addresses[1]
}
}
# VPC A に関連付け
resource "aws_route53_resolver_rule_association" "vpc_a_assoc" {
resolver_rule_id = aws_route53_resolver_rule.vpc_a_to_vpc_b.id
vpc_id = aws_vpc.vpc_a.id
}
この構成を入れたら、VPC A からservice.vpc-b.internalへのクエリが平均20msで解決されるようになりました。それまでは中央集約構成で50-100ms掛かってた。正直、この改善だけで導入の半分成功したような気分です。レイテンシ削減って、アプリケーションのパフォーマンスに直結するから、本当に大事ですね。
AWS構成図:マルチVPC・マルチアカウントのRoute 53 Resolver設計
graph TB
subgraph CentralAccount["Central Account (Route 53管理)"]
CentralZone["Private Hosted Zone<br/>(org.internal)"]
CentralOutbound["Resolver Endpoint<br/>(Outbound)"]
OnPremDNS["OnPrem DNS<br/>10.100.0.1"]
CentralOutbound -->|Forward| OnPremDNS
CentralOutbound -->|Shared Rule| CentralZone
end
subgraph VPC_A["VPC A (Dev Account)"]
LocalOutboundA["Resolver Endpoint<br/>(Outbound)"]
PrivateZoneA["Private Hosted Zone<br/>(vpc-a.internal)"]
ResolutionA["Local Resolution<br/>EC2/ECS"]
LocalOutboundA -->|Query| PrivateZoneA
PrivateZoneA --> ResolutionA
LocalOutboundA -->|Shared Rule<br/>from Org| CentralOutbound
end
subgraph VPC_B["VPC B (Prod Account)"]
LocalOutboundB["Resolver Endpoint<br/>(Outbound)"]
LocalInboundB["Resolver Endpoint<br/>(Inbound)"]
PrivateZoneB["Private Hosted Zone<br/>(vpc-b.internal)"]
ResolutionB["Service Resource<br/>RDS/ALB"]
LocalOutboundB -->|Query| PrivateZoneB
PrivateZoneB --> ResolutionB
LocalInboundB -->|Receive| PrivateZoneB
LocalOutboundB -->|Shared Rule| CentralOutbound
end
VPC_A_Query["VPC A Query<br/>service.vpc-b.internal"]
VPC_A_Query -->|Outbound Endpoint| LocalOutboundB
LocalOutboundB -->|Inbound Endpoint| LocalInboundB
LocalInboundB -->|Resolve| PrivateZoneB
PrivateZoneB --> ResolutionB
style CentralAccount fill:#fff4e6
style VPC_A fill:#e6f3ff
style VPC_B fill:#e6ffe6
セキュリティグループ設定で半日ハマった話
Route 53 Resolverってね、セキュリティグループの設定ミスで本当に簡単にハマります。むしろ、ここが最難関かもしれません。特にInbound Endpointのセキュリティグループで、別VPCのResolver EIPからのUDP 53トラフィックを許可するのを忘れると、クエリが黙って落ちるんですよ。ログにも証跡が残らないから、本当に厄介。
# ❌ 最初の設定:インバウンドルールが不足
resource "aws_security_group" "resolver_inbound_sg_bad" {
name = "resolver-inbound-bad"
vpc_id = aws_vpc.vpc_b.id
# DNS(UDP 53)を許可するのを忘れた!
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# ✅ 正しい設定
resource "aws_security_group" "resolver_inbound_sg" {
name = "resolver-inbound"
vpc_id = aws_vpc.vpc_b.id
# VPC A の Resolver から DNS クエリ受信
ingress {
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = [aws_vpc.vpc_a.cidr_block] # VPC A の CIDR
description = "DNS from VPC A"
}
# TCP 53 も許可(大きなレスポンスの場合)
ingress {
from_port = 53
to_port = 53
protocol = "tcp"
cidr_blocks = [aws_vpc.vpc_a.cidr_block]
description = "DNS TCP from VPC A"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
このセキュリティグループ問題って、CloudWatch Logsに証拠が出ないんですよ。だから「なぜか解決できない」という謎の状態が続いて、うちのチームは3時間無駄にしました。結局VPC Flowログを見たら「RejectをConnection.reset」の形で検出できたんだけど、もっと早く気付きたかった。皆さんはどうしてます?
デバッグのコツとしては、疑わしい時点でVPC Flowログを確認する癖をつけるべきですね。Route 53 Resolverのトラブルシューティングで、これが一番確実な方法です。
マルチアカウント環境での組織内ルール共有が複雑だった
AWS Organizations構成では、「複数アカウントのVPCに統一的にResolver ルールを適用したい」ケースが多いです。でも、これが結構複雑で、うちのチームは最初失敗してます。
2026年時点では、Resolver ルール共有がOrganizations Sharingで自動化できるようになってますが、それ以前の手動関連付けとの混在で設定がカオス化するんです。正直、この部分は設計段階で決めておかないと、後から改修するのが地獄。
# Central Account で Resolver ルール を作成
resource "aws_route53_resolver_rule" "org_shared_forward" {
name = "org-forward-rule"
domain_name = "onprem.internal"
rule_type = "FORWARD"
resolver_endpoint_id = aws_route53_resolver_endpoint.central.id
target_ip {
ip = "10.100.0.1"
}
}
# Organizations ResourceShare を使用して共有
resource "aws_ram_resource_share" "resolver_rule_share" {
name = "resolver-rule-share"
allow_external_principals = false
}
resource "aws_ram_resource_association" "resolver_rule" {
resource_arn = aws_route53_resolver_rule.org_shared_forward.arn
resource_share_arn = aws_ram_resource_share.resolver_rule_share.arn
}
resource "aws_ram_principal_association" "org" {
principal = data.aws_organizations_organization.current.arn
resource_share_arn = aws_ram_resource_share.resolver_rule_share.arn
}
この組み込みで良かったのは、手動で各メンバーアカウントのVPCに関連付ける手間が消えること。Organizations DelegatedAdminを設定すれば、あとは自動で各VPCに適用されます。これは本当に便利ですね。
ただし、うちが痛感したのは「既存の手動ルール関連付けと共有ルールが混在すると、トラブル時に追跡が地獄」ってことですね。デバッグしてる時に「あ、このVPCは共有ルール、あのVPCは手動か」みたいに混在してると、本当に面倒くさい。だから、新しくRoute 53 Resolverを導入するなら、最初から共有ルール構成で統一するべき。後からの修正は人的コストがめちゃくちゃ高いですよ。
DNS キャッシング戦略で本番トラフィックが30%減った
Route 53 Resolverのもう1つの工夫が、Resolver Queryログ分析に基づくキャッシュ戦略です。これは地味だけど、かなり有効な施策なんですよ。
我々が実施したのは、Resolver Queryログを CloudWatch Logs に出して、頻繁に解決されるドメインの TTL を調べるってやつです。
# Query ログ設定
resource "aws_route53_query_logging_config" "vpc_a" {
depends_on = [aws_cloudwatch_log_group.resolver_logs]
zone_id = aws_route53_zone.vpc_a_internal.zone_id
cloudwatch_log_group_name = aws_cloudwatch_log_group.resolver_logs.name
}
resource "aws_cloudwatch_log_group" "resolver_logs" {
name = "/aws/route53/resolver"
retention_in_days = 7
}
このログから出てきたデータをもとに、アプリケーション側の Private Hosted Zone レコード TTL を調整しました。特に、頻繁にアクセスされるRDS エンドポイントやALB のDNS名は、TTL を 60 秒から 3600 秒(1時間)に延ばした。これが想外に効果的だったんです。
結果として、Resolver へのクエリトラフィックが30%低下し、VPC 間の通信遅延も改善されました。CloudWatch Insights で以下みたいなクエリを走らせれば、どのドメインが頻繁に解決されてるか一目瞭然ですよ。
fields @timestamp, queryDomain, queryType
| stats count() as query_count by queryDomain
| sort query_count desc
| limit 20
実際に走らせてみると、データベースのエンドポイントとロードバランサーが全体の60%以上を占めてることに驚きました。こういう高頻度ドメインを長めのTTLで設定するだけで、Resolverの負荷が大幅に下がるんですよ。
まとめ
Route 53 Resolver の実装で、半年運用してわかったこと、整理しておきます。
1. セントラル集約構成は避けるべし 各VPC にローカルエンドポイントを配置して、オンプレ向けだけ中央経由にすることでボトルネック解消できた。中央集約は管理が楽に見えるけど、本番で火が出ます。
2. インバウンドエンドポイントは必須 VPC間通信を実現するには、対象VPCのインバウンド設定が不可欠。これがないと「見えない」問題に陥る。Inbound と Outbound の役割をしっかり理解しておくべき。
3. セキュリティグループは UDP/TCP 53 両方確認 ログに証跡が出ないので、VPC Flowログで確認する必要あり。疑わしきは Flowログを見る、これが鉄則です。
4. Organizations 共有ルール で統一 手動関連付けとの混在は最悪。新規導入なら共有ルール構成一択。設計段階で決めておかないと、後から修正するのが地獄。
5. キャッシュ戦略で本番トラフィック削減 Query ログを分析して TTL 調整すれば、Resolver負荷が30%近く下げられる。高頻度ドメインを特定して、そこのTTLを長めに設定するだけでも効果でかいです。
正直、2026年時点でもまだ「複雑さ」は残ってますが、オンプレとのハイブリッド環境では Route 53 Resolver がほぼ必須です。設計段階で VPC 間通信とオンプレ向けを分離して、各VPC にローカルエンドポイントを配置するのが、うちのチームが着地した最適解ですね。
次はマルチリージョン対応を検討してるんですが、これはまた別の地雷があるんじゃないかと覚悟してます。何か同じような経験してる方いたら、コメントもらえると嬉しいです。