こんにちは、SCSKの齋藤です。
本記事では、Google Cloudで固定IPと高可用性を両立する専用線通信基盤のアーキテクチャについてご紹介します。
はじめに
検証環境で外部システムとの専用線接続を構築する機会がありました。要件としては「接続元のIPアドレスをこの1つに固定してほしい」というもので、加えてPartner Interconnect経由での接続が必須、さらに障害時には自動復旧してほしいという高可用性の要件もありました。
最初は「VMに固定IP付けて、MIGで管理すればいいのでは?」と軽く考えていたのですが、実際に設計を進めてみると、固定IPと高可用性を両立させることが想像以上に難しいことがわかりました。Google Cloudのマネージドサービスをいくつか検討しましたが、いずれも制約があって要件を満たせず、最終的にはILB(Internal Passthrough Load Balancer)を活用する構成に辿り着きました。
この記事では、なぜシンプルな構成では実現できないのか、そしてどのようなアーキテクチャで解決したのかを、設計の過程とともに紹介します。構築のためのTerraformの詳細は後続の記事で書いていきますが、まずは全体像と設計判断の部分に焦点を当てます。なお、構成自体は複雑に見えますが、一度構築してしまえば障害時も自動復旧するので運用負荷は意外と低いです。
Internal Passthrough Load Balancer とは
Internal Passthrough Load Balancer(内部パススルーネットワークロードバランサ)は、Google Cloudが提供するL4のロードバランサです。名前の通り、パケットの送信元IPや宛先IPを一切書き換えずに、そのままバックエンドに転送する特徴があります。
一般的なロードバランサ(Application Load BalancerやProxy型)はパケットを終端して新しい接続を張り直しますが、Passthrough型はパケットをそのままバックエンドVMに届けます。このため、VM側でパケットの送信元・宛先を自由に制御でき、NATやプロキシの用途に利用できます。
主なユースケースとしては、内部サービス間のTCP/UDP通信の分散、NATゲートウェイの冗長化、そして今回のようにFloating IPパターン(固定IPをLBに持たせてVMの生死から切り離す)があります。フロントエンドに予約内部IPアドレスを割り当てることで、バックエンドVMが再作成されてもそのIPは不変です。
また、all_ports = true を設定することで全ポートのトラフィックを受け付けることができ、戻りパケット(エフェメラルポート宛て)も含めてすべてをバックエンドに届けられます。これは通常のポート指定型のForwarding Ruleでは実現できない機能です。
resource "google_compute_forwarding_rule" "proxy_ilb" {
load_balancing_scheme = "INTERNAL"
all_ports = true # 戻りパケットも通すために必要
backend_service = google_compute_region_backend_service.proxy.id
}
Regional Managed Instance Group(MIG)とは
Regional MIGは、Google Cloudが提供するVMの自動管理機能です。指定した台数のVMを常に維持し、VMが障害で停止した場合には自動的に再作成します。「Regional」がつくことで、リージョン内の複数ゾーンにまたがってVMを管理できます。
Zonal MIG(単一ゾーン)の場合、ゾーン全体が障害になるとVMが復旧できませんでした。Regional MIGであれば、あるゾーンが落ちても別のゾーンでVMが再作成されるため、ゾーン障害への耐性があります。加えて、Autohealing機能を使うことで、ヘルスチェックに失敗したVMを自動的に削除して再作成する仕組みも持っています。
今回の構成では target_size = 1 で常に1台を維持し、Autohealingで障害検知時に自動再作成する設計にしています。
resource "google_compute_region_instance_group_manager" "proxy" {
name = "proxy-mig"
target_size = 1 # 常に1台を維持
auto_healing_policies {
health_check = google_compute_health_check.autohealing.id
initial_delay_sec = 60
}
}
なぜシンプルな構成ではダメなのか
固定IPで通信したいだけなら、もっとシンプルにできるのではないかと最初は考えました。しかし検討を進めると、どのシンプルな方式も要件を完全には満たせないことがわかりました。
Cloud NAT(Private NAT)を検討した
Cloud NATにはPrivate NATという機能があり、VPC内部の通信にもNATを適用できます。これを使えば送信元IPを固定できるのではないかと考えました。
# Private NATの設定例 — IPはサブネット範囲から動的割当になる
resource "google_compute_router_nat" "private_nat" {
nat_ip_allocate_option = "AUTO_ONLY"
# → 送信元IPを1つに固定する方法がない
}
しかし調べてみると、Private NATの送信元IPはサブネットのCIDR範囲から動的に割り当てられる仕様でした。つまり「このIPアドレス1つだけ」という固定はできませんでした。たとえば/28のサブネットを指定しても、その中のどのIPが使われるかは制御できないのです。相手先から特定の1IPを指定されている以上、この方式は採用できませんでした。
VMに固定IPを割り当ててRegional MIGで管理する
次に考えたのが、VMに直接固定IPを割り当てて、Regional MIGのAutohealingで自動復旧させる方式です。高可用性が要件なのでRegional MIGを使いたいのですが、ここに問題がありました。
Zonal MIG(単一ゾーン)であれば、Stateful MIGの機能でVMの内部IPを引き継ぐことが可能です。しかしRegional MIGの場合、VMが別のゾーンで再作成されることがあります。ゾーンをまたいでの内部IP引き継ぎはサポートされていないため、ゾーン障害が起きてVMが別ゾーンで再作成されると、IPが変わってしまいます。つまり、高可用性(ゾーン障害耐性)と固定IPを同時に満たせないのがこの方式の限界でした。
Zonal MIGで妥協すれば固定IPは実現できますが、それだとゾーン障害時に手動介入が必要になり、高可用性の要件を満たせません。どちらかを犠牲にする必要があり、両立は不可能でした。
解決策: IPの所有者をVMではなくILBに移す
以下に検討した方式の比較をまとめます。
| 方式 | 送信元IP固定 | 高可用性(ゾーン障害耐性) | 採否 |
|---|---|---|---|
| Private NAT | ❌ CIDR範囲で動的割当 | ✅ マネージド | ✗ |
| VM固定IP + Regional MIG | ❌ ゾーン跨ぎでIP引き継ぎ不可 | ✅ Autohealing | ✗ |
| VM固定IP + Zonal MIG | ✅ Stateful MIGで引き継ぎ可 | ❌ ゾーン障害に弱い | ✗ |
| **ILB + Regional MIG + SNAT** | ✅ ILBがIP保持 | ✅ Autohealing | ✓ |
上記の検討を経て辿り着いたのが、「IPアドレスの所有者をVMではなくILBに持たせる」というアイデアです。
問題の本質は「IPをVMに持たせると、VMの再作成に引きずられてIPが変わる」ことでした。であれば、IPをVMから切り離して、VMの生死に影響されない場所に置けばいい。Internal Passthrough LBのフロントエンドIPは予約アドレスなので、バックエンドのVMが何回再作成されても変わりません。
VMにはそのIPの代わりに動的IPが割り当たりますが、VM内部のiptablesでSNAT(Source NAT)を行い、外部への通信時に送信元IPをILBのフロントエンドIP(固定IP)に書き換えます。
# startup-scriptでのSNAT設定イメージ
ip addr add ${FLOATING_IP}/32 dev lo
iptables -t nat -A POSTROUTING -d ${DEST_IP}/32 -j SNAT --to-source ${FLOATING_IP}
この構成であれば、Regional MIGでVMが別ゾーンに再作成されてもILBのIPは不変で、startup-scriptでSNATが自動設定されるため、手動介入なしで復旧できます。固定IPと高可用性の両立が実現しました。
パケットの流れ
この構成でパケットがどのように処理されるかを説明します。Internal Passthrough LBはパケットを書き換えないため、VM側でいくつかの設定が必要になります。
まず、Cloud RunからILBのフロントエンドIPに向けてパケットが送信されます。ILBはPassthrough型なのでパケットの宛先IPを書き換えずにそのままVMに届けます。VMのカーネルがこのパケットを受け取るためには、「このIPは自分宛てだ」と認識させる必要があるので、loopbackインターフェースにフロントエンドIPを追加しておきます。
VMがパケットを受け取ると、Nginx Stream等で外部サーバーに転送します。この際、iptablesのPOSTROUTINGチェーンでSNATが行われ、送信元IPがVMの動的IPからフロントエンドIP(固定IP)に書き換わります。GCEのデフォルトではNIC IPと異なる送信元のパケットはドロップされるため、Instance Templateで can_ip_forward = true を設定しておく必要もあります。
ILBのForwarding Ruleでは all_ports = true を設定します。外部サーバーからの戻りパケットはエフェメラルポート宛てに届くため、ポートを制限するとTCP接続が成立しませんでした。
# NG: ポート指定だと戻りパケットが通らない
# ports = ["12345"]
# OK: 全ポート許可
all_ports = true
高可用性の実現
VMレイヤーの高可用性はRegional MIG + Autohealingで実現しています。HTTPヘルスチェックでProxy VMの正常性を監視し、異常を検知すると自動的にVMを削除して再作成します。Regional MIGなのでゾーン障害時には別ゾーンで再作成されます。再作成後はstartup-scriptでloopback IPの追加とiptables SNATの設定が自動的に行われるため、手動介入は不要です。
ネットワークレイヤーの高可用性はPartner Interconnectの冗長化で実現しています。VLAN Attachmentを2本、異なるAvailability Domainに配置し、BGP + BFDで障害を高速検知してフェイルオーバーします。
resource "google_compute_interconnect_attachment" "vlan_a" {
edge_availability_domain = "AVAILABILITY_DOMAIN_1"
# ...
}
resource "google_compute_interconnect_attachment" "vlan_b" {
edge_availability_domain = "AVAILABILITY_DOMAIN_2"
# ...
}
工夫したところ・ハマったところ
can_ip_forward を忘れてパケットが消えた
構築中に最もデバッグに時間がかかったのがこの問題です。iptables SNATの設定は正しく、VM内部ではパケットが正しく書き換わっているのに、外部に一切届かない状態に陥りました。tcpdumpで確認してもVM内では正常に見えるのに、VM外に出ていきません。GCEのデフォルトでは自分のNIC IP以外を送信元としたパケットはVPCレベルでドロップされるという仕様があり、can_ip_forward = true をInstance Templateに設定して初めてこの制限が解除されます。
resource "google_compute_instance_template" "proxy" {
can_ip_forward = true # これがないとSNATパケットがドロップされる
# ...
}
ドキュメントにはちゃんと書いてあるのですが、問題が起きてから探し当てるまでに時間がかかりました。
シリーズ構成
本シリーズは全5回で構成しています:
1. 全体アーキテクチャ編(本記事)— なぜこの構成なのか
2. Partner Interconnect編 — 専用線接続の構築
3. Cloud Router + BGP + BFD編 — ルーティングと冗長化
4. ILB + Floating IP編 — 固定IPパターンの実装
5. Proxy VM + MIG + iptables SNAT編 — VMとSNATの構築
まとめ
– Private NATではサブネットCIDR範囲での動的割当になるため、IP1つに固定できなかった
– VM固定IP + Regional MIGでは、ゾーンをまたぐとIPを引き継げないため、高可用性と固定IPを両立できなかった
– ILBのフロントエンドにIPを持たせることで、VMの生死からIPを切り離すことに成功した
– 構成は複雑に見えるが、一度構築すれば障害時もAutohealingで自動復旧するため運用負荷は低い

