エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

AWS・オンプレと GCP を VPN で相互接続する際の勘所

こんにちは。エムスリー CTO の矢崎 @Saiya です。

弊社ではクラウド環境の利用や移行を推奨しており、AWS, GCP のマルチクラウドやオンプレミス環境との相互通信を安全に行うために今回 AWS と GCP を VPN で相互接続しました。

しかし、AWS と GCP を VPN で接続する方法は調べると見つけられるのですが、例えば以下の点についてまとめて説明している資料がないため苦労しました:

  • GCP の HA VPNでのセットアップ方法
    • Classic VPN と HA VPN *1 はかなり仕様が異なるのですが、Classic 前提の情報が多く惑わされやすいです
  • 複数の VPC での推奨構成とその実現方法
  • GCP と AWS の VPN の IPSec のパラメーター調整の仕方

そこで、本稿では AWS と GCP にある複数の VPC を VPN で相互に接続する構成の全体像とハマりどころ・注意点を筆者の知る範囲でまとめました。

なお、本稿では AWS Transit Gateway *2 やオンプレミスとの専用線・VPNはすでに構築済みとし、それらの設計・構築については触れません。 それらの使い方や設計の勘所・難所・ハマり所については 技術書典8 2日目の *3 スペース お-46 にて頒布予定の エムスリーテックブック2 にて詳述いたしますので、ご興味ある方は是非そちらでお求めいただければと存じます。

f:id:Saiya:20200127192910p:plain
エムスリーテックブック2 (技術書典8 の 2 日目)

今回構築するネットワーク・トポロジー

f:id:Saiya:20200127200846p:plain

上図のように GCP <-> AWS <-> オンプレミス を接続することで、3 者間での相互通信を可能とします。 例えば GCP 内の任意の VPC (Network) から AWS の任意の VPC やオンプレミスと通信することが可能になります。

なお図では HA VPN を使って 4 本の VPN を設置していますが、可用性・SLA を犠牲にコストの節約も可能です (後述)。

以下、GCP <-> AWS 間の VPN 敷設と DNS 転送について概略と難所を説明します。

GCP <-> AWS マルチ VPC 構成の VPN を支える技術

本構成では以下の技術が鍵となります:

VPN (IPSec)

今回、拠点間の VPN プロトコルとして一般的である IPSec を利用します。 IPSec は AWS, GCP のマネージドサービスやさまざまな業務用ルーターが対応している一般的なプロトコルです。

IPSec は任意の IP データグラム(TCP, UDP のパケットや ping 等で使う ICMP パケット等)を暗号化した上で転送できます。

それゆえに、これを使うことで VPC の subnet やオンプレミスのネットワークを仮想的に同じネットワークに属しているかのように運用することが可能となり、セキュリティ上の都合で Internet に露出しづらい通信を行いやすくなるといったメリットがあります。

近年では Machine to Machine の認証・暗号化が可能な Service Mesh (Istio/Envoy 等)の技術も台頭してきていますが、とはいえ IP ネットワークとの完全な互換性*4ゆえにアプリケーション層のプロトコルを縛らずに利用可能であり、かつ技術的に枯れてもいる IPSec 技術は依然として有用であると言えるでしょう。

BGP

BGP (Border Gateway Protocol) はルーター間でルーティングのための情報(経路情報)を相互に交換するための標準規格のプロトコルです。 世の中にある BGP の説明を一見するとネットワークの専門家ぐらいしか使わないようにも見えるかもしれないですが、今回のような構成ではほぼ必須です。

例えば AWS 環境のルーター(Transit Gateway)はパケットの転送先をどのように判定すれば良いでしょうか? AWS 内部の複数の VPC, GCP の複数の VPC, オンプレ環境、などパケット転送先として複数の候補が考えられます。 転送先を決定するための一つの手法としては、それぞれのネットワーク機器に対して転送先の一覧表を直接指定するという方法があります(静的ルート)。しかしこの手法では全体で設定すべき転送経路情報が VPC やネットワークの数の 2 乗であるため、設定量が爆発するという問題があります。

また、複数経路に冗長化されている場合の経路の切り替えも問題です。静的ルートの場合、転送先(VPN や専用線のインタフェース)を固定的に指定することになってしまうため、せっかく GCP VPN や AWS VPN を冗長化していたとしても人間が対応するまでダウンタイムが発生してしいます。

BGP を利用することで、上記の問題を仕組み的に解決することが出来ます:

  1. ネットワークの IP レンジ情報を自動で交換できる
    • 例えば AWS VPC が使っている IP レンジの情報を GCP に伝えることができます
  2. 冗長経路を自動で切り替えられる
    • BGP は常時 TCP 接続(peering)を貼っており、切断を検知して自動で他の経路に振り返る能力があります

ただし 1. の点については現実には静的な設定の併用が必要になってしまうこともあります。具体的には GCP の VPC Peering と DNS forwarding 機能が Cloud Router (BGPサービス)にうまく対応出来ておらず、静的な経路設定が必要な点があります (後述)。

しかし、部分的な制限事項以外のメリットは得られますし、GCP, AWS のマネージドサービス(Cloud Router, Transit Gateway)であれば BGP の構築は極めて簡単*5なので、出来るだけ BGP に寄せておく意義はあります。

各種の Peering

本構成では peering と名の付く機能を以下の通り複数使いますが、それぞれ別の技術なので混乱しないようにしましょう:

  • BGP の peering: ルーター間で経路情報をやり取りするための接続機能
  • GCP の VPC Network Peering: GCP VPC 同士を直結する機能
    • 今回の構成では VPN 用の VPC と他 VPC を接続するために使います
  • GCP の DNS Peering Zone: GCP VPC から別 VPC に DNS を転送する機能
    • 今回の構成では DNS クエリを VPN 用 VPC に集約するために使います
    • "Peering" と名前に付いていますが*6、双方向ではなく片方向の DNS 転送です
  • GCP DNS Forwarding Zone: GCP VPC から GCP 外に DNS を転送する機能
    • 今回の構成では DNS クエリを GCP から AWS・オンプレへ転送するために使います
    • "Peering" と名前に付いていないですが、Peering Zone とほぼ同じ機能です

必要な terraform リソース一覧

GCP の複数 VPC と AWS Transit Gateway (やその先にあるオンプレミス環境)を相互接続する場合、以下の terraform リソースを記述します:

一見すると数が多く見えますが、これだけの記述*7で複数 VPC の多対多接続にも対応したネットワーク環境を構築できるので、むしろ実現している事に対しては記述量が少ないと筆者は感じています。

また、いきなり terraform 前提で記載してしまいましたが、クラウドベンダーをまたいで依存するリソースをまとめて記述・管理できるメリット*8が大きいため、筆者としては terraform の利用を大いにおすすめいたします。

なお、AWS VPN, GCP VPN は多様な使い方に対応しているため、上記以外にも紛らわしい/類似した/新旧のリソースが多数存在しており惑わされがちです。 今回の構成であれば上記リソースとそれが依存するリソース*9だけに着目したほうが混乱しないものと思います*10

ハマり所

基本的に、上記 terraform resource それぞれを terraform や AWS, GCP のドキュメントを見ながら粛々と書いてゆけば今回の目的は実現できます。AWS, GCP や terraform の基本的な使い方を理解していれば、調べつつ実装することで実現できるのではないでしょうか。

しかし、いくつかの難解な事柄やハマりどころもあったため、それらについて以下で詳述します。

GCP HA VPN と AWS VPN の間の connection と tunnel の本数

GCP VPN のドキュメントの記述 を注意深く読むと VPN の各種リソースの関係が説明されてはいるのですが、AWS 側の概念・リソースとの対応関係が少々難解です。

両者の仕様を整理すると、AWS, GCP VPN の各種リソースには以下の関係があります:

  • 1 つの AWS VPN connection (aws_vpn_connection)は 2 つの IPSec tunnel を提供する
  • GCP VPN の接続先情報 (google_compute_external_vpn_gateway)が 1〜4 つの IPSec interface を定義する
    • interface は tunnel と常に 1:1
    • 4 つの tunnel を束ねて冗長化する場合、4 つの tunnel, interface を持つ google_compute_external_vpn_gateway を 1 つだけ定義するのが正解
  • GCP VPN の tunnel (google_compute_vpn_tunnel) は IPSec tunnel/interface と 1:1 対応する
  • GCP HA VPN (google_compute_ha_vpn_gateway) それ自体は connection や tunnel の数を規定しない
    • ただし、VPN tunnel 接続時に使う Public IP アドレスを connection, tunnel 数に関係なく常に 2 つ提供する
  • AWS VPN の接続受付情報(aws_customer_gateway)は、tunnel 接続元の IP アドレス数と 1:1 対応であり、かつ aws_vpn_connection とも 1:1 である必要がある

そして GCP VPN のドキュメントの記述 にある通り、GCP の SLA カバレッジを受けるためには 4 本の tunnel を 2 つの connection に接続することが必須です。

よって、以下のリソースを定義する terraform を書くことになります:

  • 2 つの AWS VPN connection (aws_vpn_connection)
    • これが合計 4 本の tunnel 接続先を提供します
  • 1 つの GCP VPN の接続先情報 (google_compute_external_vpn_gateway)
    • 4 つの interface を持つ設定(redundancy_type = "FOUR_IPS_REDUNDANCY")で定義する
  • 4 つの tunnel (google_compute_vpn_tunnel)
    • aws_vpn_connection の提供する 4 つの接続先に対して、google_compute_external_vpn_gateway の 4 つの interface をそれぞれ接続します
  • 1 つの GCP HA VPN (google_compute_ha_vpn_gateway)
    • 常に 2 つの Public IP が tunnel 接続に使用される (1 IP 当たり 2 tunnel になる)
  • 2 つの aws_customer_gateway
    • tunnel 接続元(GCP HA VPN)の Public IP と 1:1 であるので AWS 仕様と整合
    • aws_vpn_connection とも 1:1 なので AWS 仕様と整合

上記の関係性がわかっていれば、terraform を書いたり運用する上で混乱することを避けられるのではないでしょうか。

Tips: 冗長性の削減によるコスト節約

本番環境では上記のように冗長化すべきですが、テスト環境では過剰なコストとも言えます。企業ビジョンとして "不必要な医療コストを一円でも減らす" ことも掲げている(全文はこちら)弊社としても気になるポイントです。

そこで、弊社では倹約するべき環境は以下のように構築しています:

  • 1 つの AWS VPN connection (aws_vpn_connection)
  • 1 つの GCP VPN の接続先情報 (google_compute_external_vpn_gateway)
    • 1 つの interface を持つ設定(redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT")で定義する
  • 1 つの tunnel (google_compute_vpn_tunnel)
    • 1 つの aws_vpn_connection は 2 つの tunnel を受け付けますが、両方に繋がなくても動作します *11
  • 1 つの GCP HA VPN (google_compute_ha_vpn_gateway)
  • 1 つの aws_customer_gateway
    • HA VPN の 2 つの IP のうち定義順が先のほうが tunnel に使われるため、 aws_customer_gateway はその IP に対して定義する

両社の課金モデルは aws_vpn_connection, google_compute_vpn_tunnel に対する時間従量課金であるため、上記でコスト最小化が可能です。

なお、terraform を利用している場合、 count 機能や四則演算, 3 項演算子, lookup, dynamic block を活用することで、本番構成と倹約構成どちらも同じ terraform ソースコードで記述できます。例えば筆者の書いた google_compute_external_vpn_gateway は以下のようになっています:

resource "google_compute_external_vpn_gateway" "aws_vpn" {
  provider = google-beta

  name = "aws-vpn"
 
  redundancy_type = lookup({
    1: "SINGLE_IP_INTERNALLY_REDUNDANT",
    2: "TWO_IPS_REDUNDANCY",
    4: "FOUR_IPS_REDUNDANCY",
  }, var.connections * var.tunnels_per_connection)
  
  dynamic "interface" {
    for_each = range(var.connections * var.tunnels_per_connection)
    content {
      id = interface.value
      ip_address = (interface.value % var.tunnels_per_connection) == 0 ? 
aws_vpn_connection.vpn[floor(interface.value / var.tunnels_per_connection)].tunnel1_address : 
aws_vpn_connection.vpn[floor(interface.value / var.tunnels_per_connection)].tunnel2_address
    }
  }
}

terraform のソースがコピペによる別記述になってしまうと、依存するリソースの記述も引きずられて冗長になりがちなので、こういったケースに関してはできるだけ 1 つの terraform のソースで汎用的に記述する方が良いです*12

VPN に関わる IP アドレス

VPN 接続を構築する際に IP アドレスの設定項目が多数現れるため、混乱しないようにする必要があります。 今回の構成で取得・定義する必要がある IP アドレスは以下の 3 種だけであることを理解した上で構築すると混乱しないで済むでしょう:

  1. GCP の VPN 装置(google_compute_ha_vpn_gateway)の Global IP
    • google_compute_ha_vpn_gateway の出力属性 vpn_interfaces[0〜3].ip_address として得られます
    • AWS 側の aws_customer_gateway を構築する際にこの値が必要になります
  2. AWS の VPN 装置(aws_vpn_connection)の Global IP
    • aws_vpn_connection の出力属性 tunnel1_address, tunnel2_address として得られます
    • GCP 側の google_compute_external_vpn_gateway を構築する際にこの値が必要になります
  3. VPN 内部で使う Link-local IP *13 レンジ (tunnel inside CIDR)
    • VPN の仮想ネットワーク内部で、VPN の両側(AWS, GCP)の機材それぞれに付与する IP アドレスの範囲です
    • aws_vpn_connection を構築する際にこの値が必要になります
    • 2 つの aws_vpn_connection 間で重複しない*14よう、手動で明示的に設定する必要があります
    • AWS VPN の仕様 で指定可能なレンジが限られているため注意が必要です *15

なお、 GoogleCloudPlatform/autonetdeploy-multicloudvpn の terraform 等で google_compute_external_vpn_gatewayinterfaceip_address も決め打ちで指定している事例が見受けられますが、ここに指定すべき情報 *16aws_vpn_connection の出力属性 tunnel1_address, tunnel2_address から得ることが出来るため、ハードコードや自力で推定する必要はありません。

VPN (IPSec) のパラメーター

デフォルトのまま構築するだけでも AWS <-> GCP の VPN 接続は簡単に構築できますし、動作もします。しかし GCP の 公式ドキュメント を注意深く読むと、以下のような気になる記述があります:

f:id:Saiya:20200127230928p:plain
Cloud VPN "Known issue"

VPN の暗号化等のオプション設定を最小限にしないと rekey (鍵の更新)時にパケットが分割されることがあり、GCP VPN がパケット分割に対応できていないので失敗する"可能性がある" (can fail)と書かれています。

試したところデフォルトのままでも rekey 出来ているようにも見えたのですが、とはいえリスクがある以上はドキュメント推奨の通り暗号化設定を最小限にする方が安全であると筆者は考えました。 暗号関係の設定は AWS 側で制御できる(GCP 側では後述の DH group number を含めて全く制御できない)ので、 GCP の対応している設定 を見つつ AWS 側で設定を絞っていくことになるのですが、表現の違いや DH group の選択の仕方*17等いくつかのハマりどころがあります。

筆者が試した所、以下の設定であれば最小限のオプションで GCP/AWS の相互接続が可能でした (利用は自己責任で):

f:id:Saiya:20200127231627p:plain

残念ながらも terraform は現時点でこの設定に対応していないため、AWS の Site-to-Site VPN Connection 画面で Actions メニューから Modify VPN Tunnel Options を選択し、tunnel (計4つ)に対して上記の設定を手動で適用することになります (計 80 回ほどのマウスクリックでひたすらチェックを外します)。

DNS forward のソース IP 35.199.192.0/19

DNS forward を用いることで、VPN に接続している VPC から VPN 先へ DNS クエリを転送することが出来ます。

しかし、GCP が DNS クエリを転送する際は必ず 35.199.192.0/19 の IP から DNS が転送されます。転送先の AWS・オンプレミスの DNS はこの Public IP から来た DNS クエリを受付け、かつ応答のパケットを DNS クエリがやってきたのと同じ VPN に 返す必要があります (Global IP なのに!)。

したがって、転送先の DNS サーバーとそこまでの経路上のルーター全てには 35.199.192.0/19 宛のパケットを GCP の VPN へ戻す経路を必ず登録する必要があります。

ところで、1 つのネットワークに対して 2 つの相異なる GCP VPC, VPN から GCP DNS forward で DNS クエリが送られてきた場合はどうすればよいのでしょうか?

例えばオンプレミスの 1 つのネットワークに development 環境 DNS と staging 環境 DNS があり、GCP の異なる VPC からの DNS クエリをそれぞれ development DNS, staging DNS に転送するようなケースです。 このようなケースでは、転送先 DNS サーバーから見ると 35.199.192.0/19 への DNS レスポンスのパケットをどちらの VPN, VPC に返すべきかが不明になってしまいます。

筆者の知りうる限り、このような状況では static NAT や DNS 中継サーバー(dnsmasq 等)を立てて 1 つのネットワークに直接 DNS forward クエリが流入する状況を避ける以外の解決方法は存在しないように思われます。

VPC Peering, DNS forwarding の必要性

Development DNS, staging DNS のようなケースだけでなく、同一環境の中に複数の GCP VPC が存在するケースも当然存在します。 このような場合にも、個別の VPC から DNS forward してしまうと先述の 35.199.192.0/19 のルーティングの問題が発生してしまいます。

したがって、GCP 側で複数の VPC を建てる予定がある場合は、少なくとも DNS クエリを 1 つの VPC, VPN に集約する必要があります。 本稿の構成で、VPN に接続している GCP VPC を 1 つにし、各 GCP VPC はその VPC と VPC Peering および DNS Peering する構成にしているのはこの点が理由です。

なお、原理的には DNS クエリのみを 1 つの VPC, VPN に集約し、通信自体は各 VPC から個別の VPN で外に出る構成も可能と思われます。 筆者のケースでは VPN 料金のコストパフォーマンスや管理コスト観点でそのような構成にはしていませんが、もし VPN で大量データを転送したいのであれば検討してみる余地もあるでしょう。

VPC Peering と Cloud Router の BGP

VPC Peering を行う際に、VPN 側の GCP VPC で Export custom route し個別 VPC 側で Import custom route することで、VPN 経由で BGP で受け取っている経路情報を個別 VPC に伝搬させることが出来ます *18。 すなわち、AWS やオンプレミスの IP アドレスレンジの情報を機械的・自動的に GCP へ伝搬し、GCP から AWS・オンプレミスへ向かうパケットを適切にルーティングできます。

しかし、GCP 側の個別 VPC の経路情報を AWS・オンプレミスへ BGP で自動的に広告する手段は...ありません。 この点は公式ドキュメントを大変に注意深く読むと明記されていることに気づきます:

VPC ネットワーク ピアリングを使用して、VPC ネットワークに接続された別の VPC ネットワークからルート アドバタイズする場合。この場合、ピア ネットワーク内のサブネット ルートとインポートされたカスタムルートは、VPC ネットワーク内の Cloud Router によって自動的にアドバタイズされません。それらをアドバタイズするには、VPC ネットワークの Cloud Router でカスタムルート アドバタイズを作成し、VPC Network Peering 接続がカスタムルートを交換するように設定する必要があります。 https://cloud.google.com/router/docs/concepts/overview#route-advertisement-default

上記にある通り、VPC Peering を追加した場合は、Cloud Router の「カスタムルート」に明示的に当該 VPC の IP レンジを追加する必要があります。そうすれば BGP によって AWS・オンプレミスに当該経路が伝わり、AWS・オンプレミスから GCP へのパケットをルーティングできるようになります。

おわりに

本稿では GCP と AWS・オンプレミスを VPN で相互疎通するための技術要素・terraform リソースの全体像と、注意点や難解な点の詳細を述べました。 本稿では全ての詳細まで述べておりませんが、ここまでに示した内容が AWS, GCP のマルチクラウドのネットワーク環境を構築する上での道標になれば幸いです。

ここまでに述べた通り難しさが 0 ではないですが、しかしマネージドサービスであるため信頼性・作業効率・保守効率が大変向上していることも確かです。 大規模なインフラ組織などを持たない組織であっても複数環境をまたぐネットワークの構築が現実的になってきているのではないかと筆者は考えております。

また、繰り返しの手前味噌で恐縮ですが、AWS - オンプレミス のマルチクラウド環境や専用線の構築を通して、そういったネットワークのグランドデザインや構築のより包括的なノウハウ・実体験を 技術書典8 2日目の スペース お-46 にて頒布予定の エムスリーテックブック2 にて詳述いたしますので、ご興味ある方は是非そちらでお求めいただければと存じます。

f:id:Saiya:20200127192910p:plain
エムスリーテックブック2 (技術書典8 の 2 日目)

We are hiring!

エムスリーでは、本稿のようなインフラ技術のみならず、技術や頭脳で日本の医療を前進させることのできる方(エンジニア, QA, プロダクトマネージャー などなど)を常に募集しております。

本稿のような技術に限らず、何かしらの腕に覚えがある方はぜひ以下よりカジュアル面談や雑談でもお気軽にお申し込みください:

jobs.m3.com

*1:HA という名前ですが、後述のように冗長性を犠牲に 1 VPN 接続分だけのコストで運用することも可能です

*2:AWSで多数のVPCを扱う際には必須と言っても良いサービスです。

*3:今回から2日に分けて開催になりました

*4:一点だけ、MTU を超えたときの fragmentation に GCP VPN が対応していない点は注意が必要ですが...。

*5:インターネットの根幹を支えている標準規格だけあって規格やノウハウが成熟していることもあってか、マネージドサービスとしてのラッピングが安定している印象です

*6:正直 Peering というより DNS Forwarding というべき機能です...BGP, VPC などの Peering は双方向ですが DNS Peering は片方向の転送です

*7:VPC 本体や Firewall ルールなどの自明なリソースを多少省略してはいます

*8:GCP 側で生成される IP アドレスに依存する AWS リソースを作る、といった記述が簡潔かつ安全にできるメリットが大きいです

*9:例えば GCP の VPC Network や Subnet は当然必要です

*10:GCP Classic VPN とは異なり static IP や fowarding IP の定義が不要になっていたりもしています

*11:AWS 画面で見ると未接続の tunnel を示す赤字の "DOWN" という表示が見えてしまうので少しドキッとしますが

*12:異なるケースに於いては無理せずに別の module に分けたり local 変数と symbolic link を介したりして別の terraform ソースに分けたほうが良い場合もあります

*13:Link-local というのは、ネットワーク全体で一意である必要はなく隣接する機材同士の間で一意でさえあれば良い用途で用いられる IP アドレスです(RFC 3927)。VPN のようにルーターとルーターを接続する際によく用いられます。

*14:全てを単一の google_compute_external_vpn_gateway に接続するため、GCP 側の一つの機材から見て一意でないとなりません

*15:といっても、あくまで Link local なのでレンジの確保は容易ですが

*16:VPN の仮想ネットワーク内部での AWS 側機材の IP アドレスと inside CIDR のネットマスク幅を表す文字列です

*17:GCP が第一候補に提示するものを AWS が受け入れるようにしておかないとハンドシェイクに失敗する...そして GCP が第一候補に提示するものは固定されている

*18: "Custom route" という表現がミスリーディングですが、GCP の VPC 以外への経路情報全般を指すニュアンスのようです