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種類のエンドポイントがあるんです。それぞれの役割は全然違う。

エンドポイント種別方向用途
OutboundVPC → 外部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 にローカルエンドポイントを配置するのが、うちのチームが着地した最適解ですね。

次はマルチリージョン対応を検討してるんですが、これはまた別の地雷があるんじゃないかと覚悟してます。何か同じような経験してる方いたら、コメントもらえると嬉しいです。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事