Google Cloud NAT を利用した Web クローラ用 NAT ゲートウェイ設置

<img src='https://qiita-image-store.s3.amazonaws.com/0/78191/91e88599-3b82-29f1-daf3-b127fcfef95a.png' alt='NAT-Gateway-board'></img>

## この記事について

[Supership 株式会社 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/supership) の 9日目の記事です。
担当は、Supeship HD のグループ会社である Momentum 株式会社開発部の相川です。

Momentum は、インターネット広告の会社です。
広告配信先のWebページが広告主のブランドイメージを毀損するかどうか（ブランドセーフティ）判定や、広告詐欺・不正（Ad Fraud）対策といったサービスを提供しています。

今回は、ブランドセーフティ判定用の Web ページクローリングシステムに、[Google Cloud NAT](https://cloud.google.com/nat/) を利用して NAT ゲートウェイを設置した事例を紹介します。

## Web ページクローリングシステム概要

本システムは、 [Google Cloud Platform (GCP)](https://cloud.google.com) 上に構築しています。
まず、 API サーバがリクエストを受け取り、クローリング対象となる URL をメッセージキューに送ります。クローラは、キューからメッセージを受け取り、指定された URL に対してクローリングを行います。

本システムの概要は、以下の図1のようになります。

**図1: 従来の Web クローリングシステムアーキテクチャ**
![crawler-system-before.png](https://qiita-image-store.s3.amazonaws.com/0/78191/9aa1d014-43e2-f321-817c-3d4d0b08006c.png)

クローラアプリケーションは、[Google Cloud Compute Engine (GCE)](https://cloud.google.com/compute/) の各 VM 上で動作しています。
メッセージキューには、 [Google Cloud PubSub](https://cloud.google.com/pubsub/docs/overview)（以降、PubSub）を使用しています。
また、[VPC](https://cloud.google.com/vpc/docs/vpc) 内の各サブネットごとに、クローラサーバの AutoScaling グループを構築しており、キューに溜まっているメッセージ数や各サーバの CPU 使用率などに合わせてサーバ台数が自動的に伸縮します。

## NAT Gateway 導入経緯

従来のシステムでは、クローラサーバ起動時に自動的に割り当てられた IP アドレスを使用していましたが、特定ドメイン配下の URL をクローリングする際に、アクセス元の クローラサーバ の IP アドレス帯を固定したいというニーズがありました。
ただ、クローラサーバは負荷に応じて AutoScaling し、その台数は十数台から200台以上まで変動します。そのため、予め十分な固定 IP アドレス帯を確保することが困難でした。
そこで、 インターネットアクセス時に特定のゲートウェイ（もしくはプロキシ）を経由させる方法を取ることにしました。

ゲートウェイ設置の大まかな要件は以下の通りです。

- 従来通り、 負荷に応じてクローラサーバの台数が AutoScaling する
- クローラアプリケーションの改修範囲を小さくしたい
- 運用の手間を抑えるため、 できれば Managed サービスを利用したい
- でも、できるだけ低コストで

これらの要件を満たすため、今回は [Google Cloud NAT](https://cloud.google.com/nat/) を利用した NAT ゲートウェイ方式を採用しました。


## NAT ゲートウェイ入りシステムアーキテクチャ

図1 の従来アーキテクチャに Cloud NAT の NAT ゲートウェイを追加した結果が、次の 図2 です。

**図2: NAT ゲートウェイを追加クローリングシステムアーキテクチャ**
![crawler-system-with-nat.png](https://qiita-image-store.s3.amazonaws.com/0/78191/5c5ee08d-ccd1-9958-23e4-c894598c9400.png)

赤枠で囲んだ部分が追加箇所です。

NAT ゲートウェイ配下の新規サブネットを追加し、そこでクローラサーバの AutoScaling グループが動作します。新規サブネット内のクローラサーバには外部 IP アドレスが割り振られず、外部ネットワークとの通信は全て NAT ゲートウェイを経由します。
また、従来システムで使用していたメッセージキューとは別のキュー (PubSub Topic) を新設しました。API サーバなどのクローリング処理の前段で、IP アドレス帯固定が必要なドメインのURLかを判断し、クローリング指示メッセージの送信先を振り分けるようにしています。

## 導入手順

概ね、公式ドキュメントの [CloudNAT 導入例 (GCE)](https://cloud.google.com/nat/docs/gce-example) 通りの手順で導入できました。

### 前準備

#### 1. NAT 設定用の VPC サブネットを新設

```sh
# VPCにネットワーク (custom-nat-network-nat ) をに新設
$ gcloud compute networks create custom-nat-network \
    --subnet-mode custom

# custom-nat-network に NAT ゲートウェイ用のサブネットを追加
$ gcloud compute networks subnets create subnet-for-nat \
    --network custom-nat-network \
    --region "${REGION}" \
    --range 192.168.1.0/22
```

#### 2. NAT IP pool にセットする Static IP の予約

Cloud NAT にセットする固定 IP アドレスを、必要な個数予約しておきます。

```sh
$ gcloud compute addresses create nat-ip-001 --region "${REGION}"
$ gcloud compute addresses create nat-ip-002 --region "${REGION}"
# ...
```

参考: https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address


### Cloud NAT による NAT Gateway 設置

#### 1. Cloud Router の作成

Cloud NAT の設定のため、 同一リージョンに Google [Cloud Router](https://cloud.google.com/router/docs/concepts/overview) を設定する必要があります。

```sh
$ gcloud compute routers create nat-router \
    --network custom-nat-network \
    --region "${REGION}"
```

参考: https://cloud.google.com/nat/docs/gce-example#step_6_create_a_nat_configuration_using
> You must create the Cloud Router in the same region as the instances that use Cloud NAT. Cloud NAT is only used to place NAT information onto the VMs. It is not used as part of the actual NAT gateway.

> **Note:** Cloud NAT uses Cloud Router only to group NAT configuration information (control plane). Cloud NAT does not direct a Cloud Router to use BGP or to add routes. NAT traffic does not pass through a Cloud Router (data plane).


#### 2. Cloud NAT 設定を作成

Cloud NAT 設定を作成します。
今回は、クローリングシステムの要件に合わせて、外部 IP アドレス帯と適用対象サブネットの指定を行っています。

```sh
$ gcloud beta compute routers nats create nat-config \
    --router=nat-router \
    --region="${REGION}" \
    --router-region="${REGION}" \
    --nat-external-ip-pool="${NAT_IP_001},${NAT_IP_002},..." \
    --nat-custom-subnet-ip-ranges=subnet-for-nat
```

ここまでで、 新設した NAT 配下サブネット (`subnet-for-nat`) で稼働するクローラが、 NAT Gateway 経由でのインターネットアクセスが可能になりました。

### 動作確認

NAT 配下サブネットで、外部 IP アドレス割り当てないサーバインスタンスを立ち上げ、インターネットへの疎通確認と、外部 IP アドレス設定状況を確認しました。

まず、サーバには外部 IP が割り当てられていない事を確認しました。

```sh
$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> ...
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
    link/ether ...
    inet 192.168.1.23/32 brd 192.168.1.23 scope global noprefixroute dynamic eth0
    ...
```

続いて、サーバから `curl` コマンドなどを用いて、インターネットへの HTTP リクエスト疎通確認と、外から見える IP アドレスが Cloud NAT に設定したものになっているかを確認しました。

```sh
# 外部へのHTTPリクエスト疎通確認
$ curl -I https://www.example.com
HTTP/1.1 200 OK
Content-Encoding: gzip
Accept-Ranges: bytes
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sat, 08 Dec 2018 15:10:17 GMT
Etag: "1541025663+ident"
Expires: Sat, 15 Dec 2018 15:10:17 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (sec/96ED)
X-Cache: HIT
Content-Length: 606

# 外部IPアドレス確認
$ curl https://ifconfig.io
xx.xx.xx.xx
```

### クローラサーバ AutoScaling 設定

Cloud NAT 利用を選択したことで、当初の要件通りクローラアプリケーションの改修範囲を抑えることができました。そのため、 AutoScaling グループの起動時に指定する VMイメージファイルを共通化することができました。

ただし、ネットワーク設定や PubSub Subscription 設定が分かれてしまったので、[インスタンステンプレート](https://cloud.google.com/compute/docs/instance-templates/) を分けることで対応しました。

#### インスタンステンプレート設定差分例

- `--no-address` : NAT Gateway 経由でアクセスするので、外部 IP アドレスは割り当て不要
- `--metadata` : GCEインスタンスメタデータを設定して、PubSub subscription 設定の切り替えなどに使用


### ハマったところ

#### Firewall rules

VPC [Firewall rules](https://cloud.google.com/vpc/docs/firewalls) の設定更新を忘れたため、VPC 内の他のネットワークで稼働する DB に、新設したサブネット内からアクセスできない状態になってしました。

サブネット追加時に、関連ネットワーク設定更新を忘れないようにお気を付けください。

#### Cloud NAT 制約

Cloud NAT の外部 IP は1個もしくは複数指定可能です。しかし、複数個指定する場合には、個数が偶数である必要があります。

Cloud NAT config 作成時に3個以上の奇数個の IP アドレスを指定すると、以下のようなエラーメッセージが出ます。
> ERROR: (gcloud.beta.compute.routers.nats.update) HTTPError 400: Invalid value for field 'resource.nats[0].natIps': ''. The number of specified NAT IPs must be 1 or an even number.

本番環境に設置する際に、3個の IP アドレスを指定したところ、検証用環境では動いた gcloud コマンドスクリプトがエラーとなり、焦りました。（検証時は、IP アドレスは1個しか指定しなかったのでエラーにならなかった）


## Google Cloud NAT についての補足

### [リリースステータス](https://cloud.google.com/nat/docs/release-notes)

導入当時はまだ Beta でしたが、最近（2018年11月30日）に General Availability になりました。

### 価格

CloudNAT は、稼働時間とトラフィック量に応じた [費用](https://cloud.google.com/nat/pricing) が発生します。

例: Tokyo (asia-northeast1) リージョン※

- 1時間あたり $0.059
- 1GB のトラフィックあたり $0.059

※ 2018年12月上旬確認時点の内容です


## まとめ

Google Cloud NAT を使用して、既存の Web クローリングシステムに NAT ゲートウェイを追加したので、その経緯や導入方法を紹介しました。

AutoScaling 設定したサーバグループに対して、外部 IP アドレス帯を固定する方法は他にも選択肢があると思いますが、 Cloud NAT を利用することで、既存のアプリケーションへの改修範囲を抑えつつ、スムーズに導入することができました。

本番稼働開始後1ヶ月ほど経過しましたが、Cloud NAT 由来と思われるエラーには遭遇していません。
今後、 NAT Gateway 経由のクローリングリクエストが増加する可能性があるので、トラフィックや同時接続数が増えても、今まで通り元気に動いてくれると期待しています。