チームの基盤にCloud NATを導入しました

CETチームやむやむです。

この記事は、リクルートライフスタイル Advent Calendar 2019 の24日目の記事です!メリークリスマス!
昨日の記事は龍野さんによるWorkload Identityの実基盤への導入でした。

私たちのチームではGKEを使ったAPI基盤やCloud Composerを使ったJob基盤を提供しており、社内のエンジニアがこの基盤上で本番サービス向けの機能を開発しています。

この基盤のNATはGoogle Compute EngineのVMで稼働していましたが、自分たちでNATの管理をするのは運用コストが高く、またスケーラビリティも考慮できていないといった問題がありました。

そんな時、GCPからクラウドマネージドなNATであるCloud NATの提供が開始されたためこれを機にCloud NATへ移行しようということになりました。

しかし私たちの基盤では本番稼働しているAPIもあり、また稼働しているAPIやJobの数も膨大なため「基盤としてダウンタイムを発生させることなく」「出来るだけ移行のコストを下げつつ」マイグレーションを行うこととなりました。

この記事ではマイグレーションの詳細とCloud NATを扱う上でのポイントを紹介していきたいと思います。

既存の基盤とCloud NATの調査

既存の基盤

既存の基盤ではGoogle Compute EngineのVMとしてNATを構築しており、ネットワーク内のno-ipタグが付与されたVMのトラフィックをNATに流すように設定しています。また外部IPを持っているVMは自身のIPで外部と通信します。

img/old-nat-infra.png

このno-ipタグを付与されているVMはGCEのVMやGKEのNodeなど多岐に渡り、これらを漏れなくCloud NATを経由しての通信となるように移行していく必要があります。

これらの現状の仕様を考慮しつつスムーズにCloud NATに移行する手段として方針に上がったのがCloud NATのSubnetworkによる管理でした。

Subnetworkについて

Cloud NATではNATを使用するVMをSubnet単位で指定することができます。

例えばCloud NATにSubnet AとSubnet Bを紐づけた場合、Subnet AとSubnet Bに所属するVMはCloud NATを使って通信を行うように設定できます。
またno-ipタグによるルーティングのように特別なルールがある場合にはそちらが優先され、優先順位としては ルーティングルール > 外部IP > Subnet となっています。

上記のことから、GCEのVMが属するSubnetやGKEクラスタのSubnetを洗い出してCloud NATと紐づけたあとにタグの削除を行えば、既存のVM群に影響を与えずにNAT VMからCloud NATへと移行できることがわかりました。

幸い、GCEのVMの多くは1~2個のSubnetに集まっていたのでSubnetを洗い出す作業にそれほど手間はかかりませんでした。

ports/vmについて

また、Cloud NATを扱う上で注意したい点としてports/vmの振る舞いがあります。

Cloud NATはCloud NATに使用する静的IPひとつにつき、65536からwell-knownポートの1024個を差し引いた64512個のポートをCloud NATを使うVMのTCP通信用に用意しています。

ひとつのVMがどれくらいの量のポートを使えるかを指定するのがmin_ports_per_vmというパラメータです。このパラメータで使用可能なポート数64512を割った値がCloud NATのひとつの静的IPで通信可能なVMの数になります。

このmin_ports_per_vmはひとつのIP:Portの組に対して適用される値なので、1500のコネクションをひとつのIP:Portの組に送るにはmin_ports_per_vmの値は1500に設定する必要がありますが、1500のコネクションをIPは同じでも別々のPortに送るのであればmin_ports_per_vmは1で済みます。

img/nat-port-per-vm.png

もしひとつのVMが同時に通信できる量を多くしておきたい(=min_ports_per_vmを高い値に設定したい)が、Cloud NATを使いたいVMの数が許容数を超えてしまうといった場合にはCloud NATに複数の静的IPを設定しておく必要があります。
GKEを使用している場合、min_ports_per_vmはpodではなくNodeに対する値となるため、注意が必要です。

またチームでports/vmの妥当な値を調べていたところ、「設定したports/vmの値以下のコネクションしか張ることができない」という事象に遭遇しました。

こちらはGCPのサポートチームに問い合わせたところ、Cloud NATのバグとのことでした。
今後GCP側で対応していくのでそれまではports/vmの値は想定の値より多め(2倍程度というアドバイスを受けました)を設定していくことになりました。

Cloud NATの設定

上記の調査を受けて、私たちのチームでは次のようなリソース定義をTerraformで行いました。

GCEの多くのVMが所属しているSubnetとGKEクラスタのSubnetをCloud NATの管理Subnetとして設定、そしてports/vmは512で設定しVMやpodの数も考慮してCloud NATの静的IPは複数取得して設定しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
resource "google_compute_address" "nat-external-address" {
  count  = 2
  name   = "nat-nw-${count.index}"
}
resource "google_compute_router" "router-nat-nw" {
  name    = "router-nat-nw"
  network = var.nw_a
}
data "google_compute_subnetwork" "subnet-c" {
  name   = "subnet-c"
}
resource "google_compute_router_nat" "cloud-nat" {
  name                               = "cloud-nat"
  router                             = google_compute_router.router-nat-nw.name
  min_ports_per_vm                   = 512
  nat_ip_allocate_option             = "MANUAL_ONLY"
  nat_ips                            = google_compute_address.nat-external-address.*.self_link
  source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS"
  subnetwork {
    name                    = data.google_compute_subnetwork.cluster-a.self_link
    source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
  }
  subnetwork {
    name                    = data.google_compute_subnetwork.cluster-b.self_link
    source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
  }
  subnetwork {
    name                    = data.google_compute_subnetwork.subnet-c.self_link
    source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
  }
  log_config {
    # filterをALLにすると全てのログを出力してしまうのでコストに注意
    # Errorのログのみ出力する設定も可能
    filter = "ALL"
    enable = true
  }
}

Terraformでの注意点

Terraformを使ってCloud NATのリソースを管理する場合、ひとつだけ注意点があります。

Terraformで使用しているGCPのproviderのバージョンがv2.12.0以下だとCloud NATのリソースのフィールドの一部に「ForceNew: true」の設定がされており、これによりCloud NATのリソースに手を加えた場合にCloud NATの再作成処理が走りダウンタイムが発生してしまいます。(v2.13.0で改善されました)

もし諸般の事情でGCPのproviderのバージョンをv2.12.0より上げられない際には、GCPのコンソール上から修正を行えばCloud NATにダウンタイムは発生しないので、(ちょっとハック的なやり方になってしまいますが)コンソール上から変更を加えてその状態にTerraformのリソースを合わせてapplyすることでダウンタイムを発生させずにCloud NATに変更を加えることが可能です。

Cloud NATの運用

Cloud NATの導入により、チームの基盤の構成は次のようになりました。

img/cloud-nat-infra.png

まだNAT VMの削除が済んでいないため、NAT VMとCloud NATが並行稼働している状態ですが、タグを削除することでCloud NATへトラフィックを移せる段階まで来ているので、NAT VMは近い将来に利用を停止しCloud NAT一本になる予定です。

最後に

本記事では、私たちのチームで行なったCloud NAT導入に向けた検証と設定のポイントを紹介しました。

NATはネットワーク内の全てのVMにとって単一障害点になりうる部分なので、プロジェクト内の通信数やVM数を確認しながら余裕のある設定を行うのが良いと思います。

リクルートライフスタイルでは、一緒にサービス・プロダクト改善をしてくれる仲間を募集しています! 興味のある方はぜひご連絡をお待ちしております (Twitter: @ymym3412)!

それではよいクリスマスを!

参考文献