
DMM GAMESプラットフォームにおけるインフラ事例として、限定公開クラスタ設定が無効のGKEでのCloud NATを使う方法についてご紹介です。
はじめに
EXNOA プラットフォームインフラ部の角です。
私達の組織では、PCやスマートフォンなど複数デバイスでオンラインゲームやダウンロードゲームを遊べる、登録ユーザー3,100万人超のプラットフォーム「DMM GAMES」 で利用しているサーバの構築と運用を行っています。
このプラットフォームは10年以上運営されており、一部のアプリケーションはGoogle Cloud PlatformのGKE上で動いています。
私たちがGKEを運用し始めたころは、限定公開クラスタが登場する前だったので、一般公開クラスタ(限定公開クラスタ設定が無効)のGKEを運用しています。
今回、GKE上で動作するPod(アプリケーション)からIP制限が実施されているAPIへの通信が発生することになりました。
しかし、一般公開クラスタ上で動作するPodは、デフォルトルートへの通信にワーカーノードが持つエフェメラルなExternal IPを利用するので、対象のAPIへ通信できないことが分かりました。
そのため、エフェメラルではないExternal IPをGKE上のPodから使えるように、一般公開クラスタでCloud NAT を使うように構成を変更しました。
一般公開クラスタのGKEで、Cloud NATを利用するケースのブログ記事があまりなかったので紹介したいと思います。
目次は以下の通りです。
- はじめに
- 一般公開クラスタと限定公開クラスタの違い
- Cloud NATについて
- 一般公開クラスタのGKEでCloud NATを使う方法
- 一般公開クラスタでCloud NATを利用するメリット
- 最後に
一般公開クラスタと限定公開クラスタの違い
まず、一般公開クラスタと限定公開クラスタの違いについて説明します。
GKEは構築時にネットワーク設定で、一般公開クラスタと限定公開クラスタのどちらかを選択することができます。
一般公開クラスタでGKEを作成すると、下の画像のように限定公開クラスタの欄が無効 の状態になります。
ちなみに、クラスタの作成後に設定を変更することはできません。
一般公開クラスタと限定公開クラスタの大きな違いは、ワーカーノードがExternal IPを持つか持たないか です。
一般公開クラスタでkubectl get nodeを実行した結果が以下の通りで、External IPがワーカーノードに付与されているのが分かります。
| NAME | EXTERNAL-IP | INTERNAL-IP |
|---|---|---|
| gke-public-1-default-pool-250e11b5 | 35.224.139.12 | 10.128.0.12 |
一方、限定公開クラスタでkubectl get nodeを実行した結果は以下の通りで、External IPが付与されていません。
| NAME | EXTERNAL-IP | INTERNAL-IP |
|---|---|---|
| gke-private-1-default-pool-2d3f6871 | < none > | 10.128.0.14 |
一般公開クラスタの場合は、ワーカーノードにエフェメラルなExternal IPが付与されており、Podからインターネットへの通信では、このExternal IPが使われます。
限定公開クラスタは、ワーカーノードにExternal IPが付与されていないので、Cloud NATやNATインスタンスを構築して利用する必要があります。
その他の違いとして、限定公開クラスタの場合にワーカーノードからマスターノードへの通信を VPC Network Peering経由にすることが可能という点も挙げられます。
限定公開クラスタは、クラスタのワークロードがパブリックなネットワークから分離されるので、一般公開クラスタと比べてセキュリティのレベルが高くなります。
限定公開クラスタは2019年ごろから使えるようになったので、それ以前からGKEを運用している組織で限定公開クラスタへのリプレイスを行なっていない場合は、一般公開クラスタを使っているかと思います。
Cloud NATについて
本題に入る前に、前提知識として必要なCloud NATについて説明したいと思います。
Cloud NATはGCPが提供するフルマネージドなNATのサービスです。
NATといってもマネージドなNATインスタンス(NATプロキシ)のようなもの をGCPが提供してくれるものではありません。
GCPのドキュメントによると以下の通りです。
Public NAT を使用すると、パブリック IP アドレスを持たない Google Cloud リソースがインターネットと通信できます。こうした VM は、一連の共有パブリック IP アドレスを使用してインターネットに接続します。Public NAT はプロキシ VM に依存しません。代わりに、Public NAT ゲートウェイは、ゲートウェイを使用してインターネットへのアウトバウンド接続を行う各 VM に、外部 IP アドレスと送信元ポートのセットを割り当てます。
参考: https://cloud.google.com/nat/docs/overview?hl=ja
既存のNAT(NAPT、 IPマスカレード)と比較して、私たちは次のように解釈しています。
「Cloud NATは、NATインスタンスではなくNATのコントローラー的な役割を持つ」
違いは以下の通りで、
- NAT インスタンス = IPとポートの変換を行い宛先のIPへパケットを転送する(パケットを直接処理する)
- Cloud NAT = サーバに利用可能なIPとポートを割り当てる(パケットを直接処理しない)
NATインスタンスはNAPT(IPマスカレード)を実行するサーバやルーターのことで、クライアントからのパケットをIPとポートの変換を行い宛先のIPへ転送します。
対して、Cloud NATは、個々のGCE(GKEであれば、ワーカーノード)に対して、事前にNATのために取得したグローバルIPと特定のポートのセットの割り当てのみ を行います。
そうすることで、個々のGCE(GKEであれば、ワーカーノードやPod)は、デフォルトルートへの通信でNATコントローラーから割り当てられたグローバルIPとポートを使って通信を行うようになります。(裏ではGCPの仮想化ネットワークも関わってきますが、省略してます)
Cloud NATの良さは、
- 直接通信を処理しないので、単一障害点とならない
- NATの負荷を個々のGCEに分散させることができる
だと考えています。
私たちの組織ではTerraformで構成管理を行なっているので、以下のようなコードでCloud NATの設定をしています。(パラメータは実際の設定と異なります)
resource "google_compute_address" "nat_ips" {
count = var.nat_counts
name = "${var.name}-nat-ip-address-${count.index}"
region = var.region
address_type = "EXTERNAL"
}
resource "google_compute_router" "this" {
name = "${var.name}-router"
region = var.region
network = var.self_link
}
resource "google_compute_router_nat" "this" {
name = "${var.name}-nat"
region = var.region
router = google_compute_router.this.name
nat_ip_allocate_option = "MANUAL_ONLY"
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
nat_ips = google_compute_address.this[*].self_link
# portが足りなくなった場合に動的にportを割り当てる機能を有効化
enable_dynamic_port_allocation = true
# ポートの最小割り当て数を1024に設定
min_ports_per_vm = 1024
# 外部へ通信が多いpodが特定のノードに偏る場合があるのでデフォルトの65536に設定しておく
max_ports_per_vm = 65536
# Endpoint-independent Mappingは無効にしておく
enable_endpoint_independent_mapping = false
# ポートの枯渇を緩和するためtcpのwait timeoutを短めに設定
tcp_time_wait_timeout_sec = 30
# Cloud NATのエラーログの有効化
log_config {
enable = true
filter = "ERRORS_ONLY"
}
}
利用するresourceやパラメータの数は少ないので設定は簡単です。
しかし、min_ports_per_vm(VMあたりに割り当てる最小ポート数)はデフォルトで64だったりと本番環境で利用するには低い値が設定されているため、変更しておいたほうが良いです。
ほかにも、Cloud NATを利用してパケットがドロップされる現象に遭遇したというケースが多く散見され、運用するサービスに合わせたチューニングを行うためにもCloud NATの仕組みの理解が必要になります。
一般公開クラスタのGKEでCloud NATを使う方法
本題の一般公開クラスタでCloud NATを利用する方法を説明していきます。
GCPのドキュメントには、次のリソースからCloud NATを利用してインターネットへのアウトバンドの接続が可能になると記載されています。
- Compute Engine 仮想マシン(VM)インスタンス
- 限定公開の Google Kubernetes Engine(GKE)クラスタ
- サーバーレス VPC アクセスを使用する Cloud Run インスタンス
- サーバーレス VPC アクセスを使用する Cloud Functions インスタンス
- サーバーレス VPC アクセスを使用する App Engine スタンダード環境インスタンス
ここで注目してほしいのが、限定公開クラスタのGKEしか記載されていないと言うことです。
ドキュメントを読んだ当初、一般公開クラスタはワーカーノードがExternal IPを持っているのでCloud NATが利用できないのではと考えていました。
しかし、ドキュメントをよく見ると「以下の条件がどちらも当てはまる場合、限定公開以外のクラスタ内のPodから送信されたパケットは、Public NAT ゲートウェイで処理できる」 との記載があります。
- VPCネイティブ クラスタの場合、Public NAT ゲートウェイがクラスタの Pod のセカンダリ IP アドレス範囲に適用されるように構成されている。
- クラスタの IP マスカレード構成は、Pod からインターネットに送信されるパケットのクラスタ内で SNAT を実行するように構成されていません。
この2つの項目を満たすことで一般公開クラスタでも、Podからの通信でCloud NATを利用することが可能になるので、それぞれ説明します。
1つ目はGKEがVPCネイティブクラスタに設定されて、PodがVPC上のセカンダリIPアドレスの範囲を利用するようになっていることです。
以下の画像のように設定されていれば、問題ありません。
2つ目は、Podのデフォルトルートへの通信がワーカーノードのExternal IPでSNATされないようにすることです。
これには、ip-masq-agent というDaemonSetを利用します。
https://github.com/kubernetes-sigs/ip-masq-agent
ip-masq-agentは、ワーカーノードのiptablesを変更して、MASQUERADE(SNAT)の設定を変更する機能を持っています。
例えば、以下のようなconfigをComfigMapとしてcreateしておくと、ip-masq-agentがconfigを読み取って、ワーカーノードから0.0.0.0/0へ送信されるパケットをSNATしないようにiptableの設定を変更してくれます。
nonMasqueradeCIDRs:
- 0.0.0.0/0
masqLinkLocal: true
resyncInterval: 60s
具体的に、iptablesがどのように変更されるかを確認したいと思います。
ip-masq-agentをデプロイしていない状態(ワーカーノードが起動した時のデフォルト)では、以下のようなiptablesのルールがワーカーノードで設定されています(一部抜粋)
ルールの内容は、RFCに定められている範囲以外のパケットをMASQUERADEするものです。
先ほどのconfigをcreate後にip-masq-agentをapplyすると、以下のようにiptablesが変更されます(一部抜粋)
上記のように、MASQUERADEの設定よりも前にRETURNされるようなルール が IP-MASQチェインに追加されるので、Podが0.0.0.0/0宛の通信でワーカーノードのIPでMASQUERADE(SNAT)されるのを無効にすることができます。
事前にCloud NATを構築して、GKEが動作しているVPCにマッピングされていれば、ip-masq-agentがiptablesを変更したタイミングからPodからの0.0.0.0/0宛の通信はCloud NATのIPが利用されるようになります。
ちなみに、iptablesの変更でCloud NATのIPを使うようになるので、構成変更によるダウンタイムは発生しません。
実際に私たちが運用している本番環境でも、サービスに影響なく設定変更することが可能でした。
一般公開クラスタでCloud NATを利用するメリット
限定公開クラスタへ移行するよりも、一般公開クラスタでCloud NATを使うようにしたのは、単純に作業コストが小さく、構成変更によるダウンタイムが発生しなかったからです。
新規でGKEを構築・運用する場合は、特別な要件がない限り限定公開クラスタとCloud NATを利用するのがセキュリティの観点でベストだと思います。
しかし、せっかくなので一般公開クラスタでCloud NATを使うメリット を考えてみたいと思います。
考えられるメリットとして、Cloud NATのIPとワーカーノードのExternal IPをルート別に使い分けることができる という点が挙げられます。
一般公開クラスタでCloud NATを利用する場合、ip-masq-agentの設定次第で特定のIPアドレスへ通信する時のみCloud NAT経由で通信することができます。
例えば、以下のようなconfigをapplyすると3.230.23.0/32と34.198.0.55/32(httpbin.org)への通信のみCloud NATのIPが利用されます。
nonMasqueradeCIDRs:
- ノードのIP範囲(例: 10.128.0.0/20)
- PodのIP範囲(例: 192.168.0.0/16)
- 3.230.23.0/32
- 34.198.0.55/32
masqLinkLocal: false
resyncInterval: 60s
configに設定したIPは、httpbin.orgのIPでPodからcurlでリクエストを送ってみて、IPが使い分けられているか確認してみます。
# ubuntu 22.04のPodを起動して実行
# curl https://httpbin.org/ip
{
"origin": "34.42.204.6"
}
# curl https://inet-ip.info
34.69.83.130
上記のように、configに設定されているIP(httpbin.org)にはCloud NATのIPが利用され、それ以外のIP(inet-ip.info)にはワーカーノードのエフェメラルなExternal IPが利用されていることが分かります。
このように、特定のグローバルIPで送信元IPを固定しなくてもいい宛先にはワーカーノードに付与されたExternal IPを使い、送信元IPを固定しなければならない宛先にはCloud NATを使うことが可能になります。
これのなにが嬉しいかというと、特定のケースでCloud NATの動的割り当ての機能に起因するパケットのドロップを防ぐ ことに役立ちます。
Cloud NATで動的割り当てを利用している場合は、新しくポートが割り当てられるときに短い一定期間のパケットドロップが発生します。
そのため、アクセスの集中などでアプリケーションから外部へ大量に通信が発生した場合は、動的割り当てが発動して一時的にスループットが上がらずにうまく捌ききれないということが発生します。
利用されるポート数の見積もりが完璧にできるのであれば、静的割り当てを使えばパケットのドロップを防ぐことができるでしょう。
しかし、複数種類のアプリケーションがデプロイされるGKEでは、ワーカーノードごとに利用されるポート数のばらつきがあるので静的割り当てを利用するのは難しいです。
そのため、下記のどちらともに当てはまるケースで一般公開クラスタとCloud NATの組み合わせにより、動的割り当てに起因するパケットのドロップを防ぐことができるかもしれません。
- 頻繁に外部のAPIなどに通信が発生するようなアプリケーションが存在する
- 送信元IPを固定しなければならない通信がある
検証はしていませんが、このようなケースで一般公開クラスタとCloud NATの組み合わせの構成だと、
- 送信元IPを固定しなくてもよくて頻繁にアクセスする宛先には、ワーカーノードのエフェメラルなExternal IPを利用する
- 送信元IPを固定しなければならない宛先には、Cloud NATのIPを利用する
という形で、うまく分散させる ことが可能になるためです。
最後に
今回、運用中の一般公開クラスタのGKEでCloud NATを使うように構成の変更を行いました。
Cloud NATは私がこれまで知っていたNATの仕組みとは異なるものであったため、理解するのに少し苦労しました。
また、単一のサーバやネットワークリソースに依存せずに、NATを分散的に実行させるという可用性とパフォーマンスを両立させたGoogleらしいマネージドサービスだなと思いました。
EXNOAでは、一緒に働く仲間を募集しています!