本日も乙

ただの自己満足な備忘録。

GCPでNATインスタンスを構築する

(2018/10/17 追記) 先日マネージドNATサービスがリリースされました!

blog.jicoman.info

目次

NATインスタンスを構築する理由

GCPで最初から用意されているデフォルトのネットワークでGCE(VM)インスタンスを作成すると、グローバルIPアドレスが付与されており踏み台サーバなしで直接ログインすることが可能です。AWSで例えるならすべてのEC2インスタンスがパブリックサブネットにいるような状態になっています。すべてパブリックからアクセスができるので不安になりますが、GCPのファイアウォールやCloud Armorでアクセス制限できるため、これらの設定がきちんとされていればこのままでも問題ありません。

しかし、IPアドレス制限している別システムにGCPにあるGCEインスタンス(VMインスタンス)からアクセスしたいときに問題になる場合があります。一つ、二つ程度のVMインスタンスであれば、それぞれのパブリックIPアドレスを許可すれば済むのですが、マネージドインスタンスグループ(MIG)によるオートスケールだとVMインスタンスが頻繁に入れ替わりIPアドレスも都度変更されるため、変更を追従することは困難です。

対策として考えられるのは、VPCネットワーク内部のVMインスタンスから外部へのトラフィックはすべてNATインスタンス経由することです。NATインスタンス経由にすることで、IPアドレス制限をかけている側のシステムはNATインスタンスのグローバルIPアドレスのみ許可すれば済みます。また、NATインスタンスを静的IPアドレス *1 にすることでIPアドレスを固定化できます。

NATインスタンスの構築って面倒?

はい、面倒ですwとくに運用が面倒です。
AWSはNATゲートウェイというマネージドなNATサービスが提供されており、可用性の向上や帯域のスケールを考えなくても済みます。

NATゲートウェイが登場する前は、EC2インスタンスでNATインスタンスを構築するしかありませんでした。その際には、可用性を落とさないようにする工夫を自分たちでとらなければなりませんでした。
例えば、AZ毎に1台ずつのNATインスタンスがあり、それぞれのAZにあるEC2インスタンスはそのAZにあるNATインスタンス経由にしているとします。お互いのNATインスタンスをpingなどで監視し合い、落ちたら生きている方のNATインスタンスが片方のAZのルートテーブルの設定を変更し、生きているNATインスタンスに向けるようにするといった先人の知恵がありました。

GCPではAWSのようなマネージドなNATゲートウェイはサービス提供されていないため、自前でVMインスタンスを構築・設定する必要があります。しかし、GCPならではの高可用性・自動復旧の仕組みが備わっているため、先ほど述べたようなAWSでNATインスタンスを構築するよりかは容易になります。

NATインスタンス経由にする場合のデメリット

AWSと同様に、VPC内部から外部への通信をNATインスタンス経由にした場合、VMインスタンスにグローバルIPアドレスを付与することができず、プライベートIPアドレスのみになります。したがって、ローカルPCから直接VMインスタンスにSSH/RDP接続することができないため、踏み台サーバを用意する必要があります。

bastion
https://cloud.google.com/solutions/connecting-securely#bastion

gcloudコマンドでVMインスタンスにログインしている場合、踏み台サーバとその先のサーバと2回ログインしなければならず不便になります。

AWSとGCPにおけるルーティングの違い

AWSの場合、サブネット毎にルートテーブルが設定されているため、NATインスタンス経由するサブネットにEC2インスタンスすべてがNATインスタンス経由になります。 対してGCPの場合、NATインスタンス経由にするかどうかはタグの付与で決定できるため、VMインスタンス単位で制御が可能です。個別にタグをつけるのが面倒かもしれませんが、実際はインスタンステンプレートでタグ設定しておけばそのインスタンステンプレートにアタッチされているMIGからVMインスタンスを起動するとすべてタグが付与されます。

構築方法

実はGCPのドキュメントでNATインスタンスの構築方法が書かれているので今回はこれを参考にしました。
https://cloud.google.com/vpc/docs/special-configurations#multiple-natgateways

今回はデフォルトネットワークではなく、事前にtestというネットワークを作成しておき、2つのNATインスタンスを東京リージョンのサブネットに構築しました。
本来ならば、東京リージョンのゾーン *2 が3つあるため、NATインスタンスも3つあった方が可用性の観点から適切なのですが、コスト的観点から2つにしました。逆に1つだけだと落ちた場合に外部への通信ができなくなってしまうため、丁度良い2つにしました。

2つの静的 IP アドレスを予約して保存

静的IPアドレスを2個取得します。それぞれ nat-tokyo-1, nat-tokyo-2 という名前をつけました。

$ gcloud compute addresses create nat-tokyo-1 --region asia-northeast1
# 一つ目のNATインスタンスのグローバルIPアドレスを取得する
$ nat_1_ip=$(gcloud compute addresses describe nat-tokyo-1 \
    --region asia-northeast1 --format='value(address)')

$ gcloud compute addresses create nat-tokyo-2 --region asia-northeast1
# 二つ目のNATインスタンスのグローバルIPアドレスを取得する
$ nat_2_ip=$(gcloud compute addresses describe nat-tokyo-2 \
    --region asia-northeast1 --format='value(address)')

2つのインスタンス テンプレートを作成し、予約済みIPアドレスを割り当てる

起動スクリプトが公開されているので、ローカルに一度持ってきます。

$ gsutil cp gs://nat-gw-template/startup.sh .

startup.sh の中身は以下の通りです。URLパス /health-check にGETリクエストすると、www.google.com にpingするようになっています。

#!/usr/bin/python
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
import subprocess

PORT_NUMBER = 80
PING_HOST = "www.google.com"

def connectivityCheck():
  try:
    subprocess.check_call(["ping", "-c", "1", PING_HOST])
    return True
  except subprocess.CalledProcessError as e:
    return False

#This class will handle any incoming request
class myHandler(BaseHTTPRequestHandler):
  def do_GET(self):
    if self.path == '/health-check':
      if connectivityCheck():
        self.send_response(200)
      else:
        self.send_response(503)
    else:
      self.send_response(404)

try:
  server = HTTPServer(("", PORT_NUMBER), myHandler)
  print "Started httpserver on port " , PORT_NUMBER
  #Wait forever for incoming http requests
  server.serve_forever()

except KeyboardInterrupt:
  print "^C received, shutting down the web server"
  server.socket.close()
EOF

nohup python /usr/local/sbin/health-check-server.py >/dev/null 2>&1 &

インスタンステンプレート *3 を作成します。subnet のパラメータは各自の環境に読み替えてください。

$ gcloud compute instance-templates create nat-tokyo-1 \
    --machine-type n1-standard-1 --can-ip-forward --tags natgw \
    --metadata-from-file=startup-script=startup.sh --address $nat_1_ip \
    --subnet=projects/nat-sample/regions/asia-northeast1/subnetworks/dmz

$ gcloud compute instance-templates create nat-tokyo-2 \
    --machine-type n1-standard-1 --can-ip-forward --tags natgw \
    --metadata-from-file=startup-script=startup.sh --address $nat_2_ip \
    --subnet=projects/nat-sample/regions/asia-northeast1/subnetworks/dmz

マシンタイプですが、GCPのドキュメントには vCPUあたりピーク時に下り(GCPから外部への通信)で2Gbpsが上限となっていますので、見込まれる通信量に応じて適宜変更してください。

下りスループット上限は、仮想マシン インスタンスにある vCPU の数に依存します。各 vCPU にはピーク時のパフォーマンスのために 2 Gbps の下り下限があります。vCPU 数を追加することでネットワークの容量が増加し、理論的には仮想マシンごとに最大で 16 Gbps となります。たとえば、4 つの vCPU を備えた仮想マシン インスタンスのゾーン内ネットワーク スループット容量は 2 Gbps × 4 = 8 Gbps となります。8 つの vCPU を備えた仮想マシン インスタンスゾーン内ネットワーク スループット容量は 2 Gbps × 8 = 16 Gbps となります。 https://cloud.google.com/compute/docs/networks-and-firewalls#egress_throughput_caps

ヘルスチェックの作成

応答性をモニタリングするためのヘルスチェックを作成します。
/health-check に定期的に問い合わせることで、ヘルスチェックが正常(= www.google.com へのpingに成功)かどうかをチェックします。

$ gcloud compute health-checks create http nat-health-check --check-interval 30 \
    --healthy-threshold 1 --unhealthy-threshold 5 --request-path /health-check

# ヘルスチェックをチェックするソースIPアドレスを許可するファイアウォールルールを設定
# netgwタグをつけたGCEインスタンスに対してルールが適用される
$ gcloud compute firewall-rules create "nat-firewall" \
    --allow tcp:80 --target-tags natgw \
    --source-ranges "130.211.0.0/22","35.191.0.0/16" \
    --network=nat-sample

ドキュメントでは 209.85.152.0/22209.85.204.0/2235.191.0.0/16 となっていましたが、これはネットワーク負荷分散によるヘルスチェックを行い場合のIPアドレスです。
今回はHTTPによるヘルスチェックなので、130.211.0.0/22, 35.191.0.0/16 を許可します。

Network Load Balancing

When a health check is used with Network Load Balancing, the health check probes come from addresses in the ranges 209.85.152.0/22, 209.85.204.0/22, and 35.191.0.0/16. You need to create firewall rules that allow these connections to all your load balanced instances.

HTTP(S), SSL Proxy, TCP Proxy, and Internal Load Balancing

When a health check is used with HTTP(S), SSL Proxy, TCP Proxy, or Internal Load Balancing, the health check probes come from addresses in the ranges 130.211.0.0/22 and 35.191.0.0/16. You need to create firewall rules that allow these connections to all your load balanced instances. https://cloud.google.com/load-balancing/docs/health-checks#source-ip-firewall-rules

NAT ゲートウェイのインスタンスグループを作成

マネージドインスタンスグループ(Managed Instance Group: MIG) *4 を作成することでGCEインスタンスが落ちたとしても指定した台数を確保してくれます。今回の場合は、各ゾーン毎に一台固定となっているため、NATインスタンスが削除されたとしても自動的に新しいインスタンスが作成されます。
AWS でも可用性向上のため、1台のみのAuto Scalingを設定することもありますが、GCPではこのように設定することで同様なことができます。
参考: 1台のサーバですら Auto Scaling でケチる - HDE BLOG

$ gcloud compute instance-groups managed create nat-tokyo-1 \
    --size=1 --template=nat-tokyo-1 --zone=asia-northeast1-a
$ gcloud compute instance-groups managed create nat-tokyo-2 \
    --size=1 --template=nat-tokyo-2 --zone=asia-northeast1-b

上記のコマンドまで実行すると、インスタンステンプレートに沿ったGCEインスタンス(NATインスタンス)が起動されます。

自動修復の設定

先ほど作成したヘルスチェックを組み込むことで、応答しないNATインスタンスを自動的に再起動するようになります。

$ gcloud beta compute instance-groups managed set-autohealing nat-tokyo-1 \
    --health-check nat-health-check --initial-delay 120 --zone asia-northeast1-a
# 一つ目のNATインスタンス名を取得する
$ nat_1_instance=$(gcloud compute instances list |awk '$1 ~ /^nat-tokyo-1/ { print $1 }')

$ gcloud beta compute instance-groups managed set-autohealing nat-tokyo-2 \
    --health-check nat-health-check --initial-delay 120 --zone asia-northeast1-b
# 二つ目のNATインスタンス名を取得する
$ nat_2_instance=$(gcloud compute instances list |awk '$1 ~ /^nat-tokyo-2/ { print $1 }')

デフォルトルートの追加

NATインスタンスへのデフォルトルートを追加します。no-ipタグをつけたGCEインスタンスに対して適用されます。
なお、デフォルトルートを指定する先はNATインスタンスのインスタンス名(nat-tokyo-xxxx)なので、NATインスタンスを作り変えた場合は、デフォルトルートも変更(つまり再作成)する必要がある点にご注意ください。

$ gcloud compute routes create nat-tokyo-route1 --destination-range 0.0.0.0/0 \
    --tags no-ip --priority 800 --next-hop-instance-zone asia-northeast1-a \
    --next-hop-instance $nat_1_instance \
    --network=nat-sample
$ gcloud compute routes create nat-tokyo-route2 --destination-range 0.0.0.0/0 \
    --tags no-ip --priority 800 --next-hop-instance-zone asia-northeast1-b \
    --next-hop-instance $nat_2_instance \
    --network=nat-sample

GCEインスタンスにタグを付ける

すでにGCEインスタンスが作成されている場合は、一度停止しグローバルIPアドレスを外したうえで no-ipタグをつけて起動します。

$ gcloud compute instances add-tags test-instance001 --tags no-ip

ネットワーク疎通の確認

NATインスタンスができたら内部のGCEインスタンスからpingを打って外部ネットワークにでていけることを確認します。
内部のGCEインスタンスへは踏み台サーバ経由でログインしてください(別途記事を書く予定です)。

$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=45 time=38.1 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=45 time=36.5 ms
...

内部VMインスタンスからGCP外部のサーバにアクセスすると2つのNATインスタンスからトラフィックがでていることがわかります。
GCPでは、等価コスト マルチパス(ECMP)ルーティングでトラフィックが分散されます。

以下は例として、203.0.113.10 をNATインスタンスA、203.0.113.11をNATインスタンスBとしています。

203.0.113.10 - - [02/Aug/2018:13:55:25 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:13:55:26 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:13:55:27 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:13:55:28 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:13:55:29 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:13:55:30 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
...

NATインスタンスの可用性の検証

1つ目のNATインスタンス(nat-tokyo-1-xxx)を停止し一時的に疎通できないようにします。
MIG配下のGCEインスタンスが停止された場合、自動的に起動するようになっていますので、停止しても約40秒程度で自動復旧されることが確認できました。

203.0.113.10 - - [02/Aug/2018:14:30:55 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:14:30:56 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:14:30:57 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:30:58 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:14:30:59 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:00 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:14:31:02 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:03 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"   # このあたりでNATインスタンスが停止
203.0.113.10 - - [02/Aug/2018:14:31:04 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:05 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:06 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:13 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:20 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:21 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:22 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:23 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:25 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:26 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:28 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:29 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:30 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:31 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:32 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:33 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:34 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:35 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:36 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:37 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:38 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:39 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:40 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:41 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:42 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:43 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:44 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:45 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:14:31:46 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"   # NATインスタンスが自動復旧
203.0.113.10 - - [02/Aug/2018:14:31:47 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:14:31:48 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:49 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.10 - - [02/Aug/2018:14:31:50 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"
203.0.113.11 - - [02/Aug/2018:14:31:51 +0900] "GET / HTTP/1.1" 200 30 "-" "curl/7.35.0" "-"

最後に

GCPでNATインスタンスを構築する方法をドキュメントに沿って紹介しました。また、NATインスタンスが落ちた場合の可用性の検証も行いました。
一度作ってしまえば便利なんですが、GCEインスタンスの帯域の制限があるので、帯域を大きくしたい場合にGCEインスタンスを入れ替える必要があります。その場合、インスタンステンプレートを再作成し、MIGの設定変更も必要になりますし、デフォルトルートの再設定も必要になります。
やはりNATインスタンスを自前で運用すると面倒なので、マネージドのNATサービスがGCPからも出てくれることを期待したいですね。

*1:AWSのElastic IPアドレスに相当

*2:AWSのアベイラビリティゾーン(AZ)に相当

*3:AWSのLaunch Configuration(起動設定)に相当

*4:AWSのAuto Scaling Groupに相当