AWS FargateでNATを利用する時の注意事項

# 概要
AWS FargateでNATを利用しているときにぶつかった、NAT通信過課金問題とその回避策について
簡単にまとめます。

# NAT利用の背景
Fargate上に配置するコンテナから、外部サービスにアクセスする必要がありましたが、
セキュリティの都合上で外部サービス側にIPアクセス制限が設けられていました。
これを解決するため、FargateからのアクセスをNAT経由にすることでアクセスIPを固定化する方法を
選択しました。

# NAT設定から問題発覚までの流れ
## Subnet、NAT作成およびルーティング設定
まずはNAT越しにインターネットアクセスできるようにSubnetやルーティング設定を組みます。
ざっくりと手順をまとめると、

- Public用とPrivate用Subnetそれぞれを作成。
- EIPを作成。さらにNAT Gatewayを作成し、作成したEIPを割り当てる
- 作成されたEIPのアドレスを接続先サービスに許可してもらう
- Internet Gateway(IGW)を作成
- 作成したNAT GatewayをPublic用Subnetに割り当て。Internet GatewayもPublic側のSubnetに割り当てる。
- Private側Subnetのルーティングに対して、外部アクセスはNATを踏みように設定。

という流れで設定しました。
AWS Console上の設定方法については割愛します。イメージ的には以下のように設定しました。
<img width="176" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/296824/b027073f-ee1e-dda8-9f5c-9134c52f9c0b.png">
（補足：マルチゾーンに対応するには、これをゾーン毎に組み上げます。）

## Fargate設定
ネットワーク周りの準備ができたので、次にFargateの設定です。
今回は１０分間隔程度で外部サービスにアクセスし、処理するものがなければそのままコンテナを終了する、
という仕様のため、ECS上にクラスタを作成し、タスクスケジュールを組む方式にしました。
その際の設定にて、コンテナをPrivateA Subnetに配置する設定を行なっています。

<img width="238" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/296824/7e4fe166-c02e-b50a-64fe-ccdb304cdb96.png">


この設定により、無事にFargate上に配置されたコンテナから外部サービスにアクセスする際の接続元IPを
NATに割り当てたIPアドレスに固定化することに成功しました。

## 問題発覚
Fargate設定の数日後、日別請求を見てみるとNAT通信にかかる請求が異常に跳ね上がっている問題に
気づきました。とはいえ、外部サービスからのレスポンスデータのサイズは数KB程度。
どう計算しても過剰な利用量となっていました。

# 原因特定とその対策について
## 原因特定
NAT通信量が異常に跳ね上がった原因を特定するため、一からFargateの仕様を整理しました。
そもそも、Fargateは

- タスク起動時、タスク定義に従ってdockerイメージをECRからpullする
- pullしたdockerイメージをベースにコンテナを作成・実行
- コンテナ終了後、コンテナを破棄

というライフサイクルで動作しています。コンテナ内のアプリケーションの通信を除くと、
dockerイメージのpullくらいです。

この仕様から、
「Fargateコンテナ上からECRのアクセスもプライベートSubnetからは全てNATを通過するのでは」
という仮説を持ちました。

そこで、似たような課題がレポートされていないか探してみると、
「[VPC endpoint support needed to start using ECR](https://forums.aws.amazon.com/thread.jspa?threadID=222124) 」
というタイトルでの投稿が見つかりました。

この投稿の中でも、ECRサービスとの通信はインターネット上で行われている、と触れられています。

つまり、FargateコンテナとECR間の通信経路は特別に設けられているわけではなく、
Internet Gateway越しに行われている、という結論にたどり着きました。
プライベートSubnetにFargateコンテナを置くと、必然的にdockerイメージのpullは
NAT越しに行われ、そのダウンロードサイズ分、課金され続ける、結果となっていたようです。

実際に、Public側のSubnetにFargateコンテナを配置すると、Fargate -> 外部サービスアクセスの外部IPは
当然ながら固定化できませんでしたが、NAT課金が大幅に改善された結果から見ても上記の仮説の通りの事象と
結論づけました。

<img width="422" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/296824/5511ed5c-c858-1bec-b520-703711ca5593.png">


## 対策
今回はFargateコンテナ上に配置するサービスは外部サービスへのアクセスのみで、特に内部向けにポートを
オープンするサービスではなかったため、割り切って以下のように対策しました。

- FargateコンテナをIGWが配置されているPublicサブネット上に配置する
- 外部サービスへのアクセスだけNATを経由するようルーティングする
- それ以外（ECRからのdocker pull含む）のアクセスはIGWを経由するようルーティングする

という形にしました。

ただし、この構成で１つ注意が必要なのが、NATの
「別subnetからのパケットをIGWにルーティングする」
という性質です。

同一Subunet内でNATへのルーティングを行なったとしても、それは期待通りルーティングされません。

よって、以下のように、IGWをそれぞれ持った２つのパブリックSubnetを用意し、その片方にNATを割り当てる、
という構成にしました。

<img width="602" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/296824/d57d73c0-18bc-7ffa-beaa-f8dbaade0d5a.png">
(イメージ上は二つのIGWを書いていますが、実際には同じIGWを二つのSubnetで共有しています。)

これにより、晴れてECRからのDockerイメージpullに必要な転送コストを抑え、外部サービスへのアクセスのみ、
NAT経由でIP固定化するという動きを再現しました。

# 留意事項
上記で紹介した方法では、外部アクセスを遮断したいようなポートを持っているようなサービスには向きません。
ただし、ポートをオープンしているようなサービスでは頻繁にECRからdockerイメージをpullする
必要はないので、そもそも大きな問題にはならないかと思います。
ただし、頻繁にスケール調整が必要なケースにおいては、dockerイメージのサイズを抑えるなどの努力が
必要となります。

# 参考記事
https://forums.aws.amazon.com/thread.jspa?threadID=222124