メディアドゥでリードエンジニアをしております、沓名です。
今回はAWS FargateでNATを利用しているときにぶつかった、NAT通信過課金問題*1とその回避策について 簡単にまとめます。
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上の設定方法については割愛します。イメージ的には以下の通りです。
(補足:マルチゾーンに対応するには、これをゾーン毎に組み上げます。)
Fargate設定
ネットワーク周りの準備ができたので、次にFargateの設定です。
今回は10分間隔程度で外部サービスにアクセスし、処理するものがなければそのままコンテナを終了する、という仕様のため、ECS上にクラスタを作成し、タスクスケジュールを組む方式にしました。
その際の設定にて、コンテナをPrivateA Subnetに配置する設定を行なっています。
この設定により、無事にFargate上に配置されたコンテナから外部サービスにアクセスする際の接続元IPをNATに割り当てたIPアドレスに固定化することに成功しました。
問題発覚!!
Fargate設定の数日後、日別請求を見てみるとNAT通信にかかる請求が異常に跳ね上がっている問題に気づきました。
とはいえ、外部サービスからのレスポンスデータのサイズは数KB程度。どう計算しても過剰な利用量となっていました。
原因特定とその対策について
原因特定
NAT通信量が異常に跳ね上がった原因を特定するため、一からFargateの仕様を整理しました。
そもそも、Fargateは
- タスク起動時、タスク定義に従ってdockerイメージをECRからpullする
- pullしたdockerイメージをベースにコンテナを作成・実行
- コンテナ終了後、コンテナを破棄
というライフサイクルで動作しています。コンテナ内のアプリケーションの通信を除くと、dockerイメージのpullくらいです。
この仕様から、
「Fargateコンテナ上からECRのアクセスもプライベートSubnetからは全てNATを通過する」
という仮説を立て、似たような課題がレポートされていないか探してみると以下の投稿がヒットしました。
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課金が大幅に改善された結果から見ても上記の仮説の通りの事象と結論づけました。
(理想的にはdockerコンテナが配置されるサブネットからECRへのアクセスがPrivateLinkなどを用いて結ばれる経路があるべきですが、現在サポートを進めている最中のようです。https://github.com/aws/containers-roadmap/issues/1)
対策
今回はFargateコンテナ上に配置するサービスは外部サービスへのアクセスのみで、特に内部向けにポートをオープンするサービスではなかったため、割り切って以下のように対策しました。
- FargateコンテナをIGWが配置されているPublicサブネット上に配置する
- 外部サービスへのアクセスだけNATを経由するようルーティングする
- それ以外(ECRからのdocker pull含む)のアクセスはIGWを経由するようルーティングする
という形にしました。
ただし、この構成で1つ注意が必要なのが、NATの
「別subnetからのパケットをIGWにルーティングする」
という性質です。
同一Subunet内でNATへのルーティングを行なったとしても、それは期待通りルーティングされません。
よって、以下のように、IGWをそれぞれ持った2つのパブリックSubnetを用意し、その片方にNATを割り当てる、という構成にしました。
(イメージ上は二つのIGWを書いていますが、実際には同じIGWを二つのSubnetで共有しています。)
これにより、晴れてECRからのDockerイメージpullに必要な転送コストを抑え、外部サービスへのアクセスのみ、NAT経由でIP固定化するという動きを再現しました。
まとめ
上記で紹介した方法では、外部アクセスを遮断したいようなポートを持っているようなサービスには向きません。
現時点ではプライベートなdocker registryを立てるなどの対策が必要となります。
参考サイト
https://github.com/aws/containers-roadmap/projects/1
https://github.com/aws/containers-roadmap/issues/1
https://forums.aws.amazon.com/thread.jspa?threadID=222124
*1:本記事で扱っている課題は以下の機能がリリースされれば解決することが期待されます。2019/01/09時点でComming soonのステータスになっているので、同様の問題を抱えている方は以下Issueのステータスも合わせてご覧ください。github.com