はじめに
こんにちは、iimonでエンジニアをしているhogeです。 現在、弊社のワークロードで使っているVPCの数は多くありませんが、将来的にVPC数が増えた場合、NAT GatewayやVPC Endpointの費用が大きくなる可能性があります。そこで、Transit Gatewayを利用してインターネット通信を1つのVPCに集約する構成を検討し始めました。実際に導入するとなったときに素早く構築できるように、Terraformを使ってインターネット通信を集約する構成を一通り書いてみたので、今回はそれを紹介したいと思います。
インターネット通信を集約するとどれくらいコストを節約できるか
まず、東京リージョンにNAT Gatewayを2つ(2AZ分)配置する構成でシミュレーションしてみます。
- NAT Gatewayの固定料金
- 0.062(USD/h)×2(AZ)×24(h/日)×30(日)=89.28USD
- NAT Gatewayのデータ処理料金(利用料によって変動)
- 0.062(USD/GB)
仮に10個のVPCすべてにNAT Gatewayを配置すると、固定料金だけで89.28×10=892.8USDかかります。
次に、Transit GatewayでNAT Gatewayを集約した場合を考えます。
- NAT Gatewayの固定料金
- 0.062(USD/h)×2(AZ)×24(h/日)×30(日)=89.28USD
- NAT Gatewayのデータ処理料金(利用料によって変動)
- 0.062(USD/GB)
- Transit GatewayのAttachment料金 (1 VPCあたり1つアタッチが必要)
- 0.07(USD/h)×24(h/日)×30(日)=50.4USD (1 VPCあたり)
- Transit Gatewayのデータ処理料金
- 0.02(USD/GB)
10個のVPCをTransit Gatewayで集約し、NAT GatewayはShared VPC(集約用VPC)にのみ設置した場合の固定費は以下の通りです。
固定費の合計は、89.28 + 504 = 593.28$となります。
VPCごとにNAT Gatewayを設置する(892.8USD)構成と比べると、約300 USDほど安くなります!
以下はVPC数を1から10まで増やしたときの料金をまとめた表です。VPC数が少ない場合、集約構成のコストメリットは小さいですが、VPC数が多くなるほどコスト削減効果が大きくなることが分かります。
VPC数 | Transit Gatewayの料金 | NATの料金(2az) | VPCごとにNATを設置する場合の料金 | NAT集約構成の料金 |
---|---|---|---|---|
1 | 50.4 | 89.28 | 89.28 | 139.68 |
2 | 50.4 | 89.28 | 178.56 | 190.08 |
3 | 50.4 | 89.28 | 267.84 | 240.48 |
4 | 50.4 | 89.28 | 357.12 | 290.88 |
5 | 50.4 | 89.28 | 446.4 | 341.28 |
6 | 50.4 | 89.28 | 535.68 | 391.68 |
7 | 50.4 | 89.28 | 624.96 | 442.08 |
8 | 50.4 | 89.28 | 714.24 | 492.48 |
9 | 50.4 | 89.28 | 803.52 | 542.88 |
10 | 50.4 | 89.28 | 892.8 | 593.28 |
ただし、固定料金以外にも考慮すべき点として、Transit Gatewayのデータ処理費用(0.02 USD/GB)がVPC内にNAT GatewayやVPC Endpointを配置する場合より割高になるケースもあります。通信量が多いVPCについては、それぞれのVPC内にNAT GatewayやVPC Endpointを置いたほうが費用を抑えられる可能性があります。すべてのワークロードが大量の通信を行うわけではないと思うので、基本は集約構成にしつつ、通信量の多いVPCだけは個別にNAT GatewayやVPC Endpointを設置してTransit Gatewayを経由しないようにする、というハイブリッドな対応がよさそうです。
集約構成を構築してみる
まず、下図のような構成を想定します。VPCが2つあり、それぞれにNAT Gatewayが存在しています。Private SubnetにあるEC2インスタンスは、NAT Gatewayを経由してインターネットへアクセスします。
この構成を、Transit Gatewayを使って各サービスVPCのPrivate SubnetからShared VPC(共通VPC)のNAT Gatewayへとアウトバウンド通信を集約するように変更すると、下図のようになります。サービスのVPC同士は相互に通信できないように制御しています。
Terraformのソースコードは以下のリポジトリに配置しています。
- service_network.tf : service1とservice2のネットワーク関連定義
- shared_network.tf : 共通VPCのネットワーク関連定義
- ec2.tf : 疎通確認用EC2インスタンス
- network_insights.tf : Reachability Analyzer関連
ここからは、この構成でどのようにルーティングされるのか、Terraformの実装を例に解説していきます。
Serviceからインターネットに出るパターン
(赤い線が行きの通信、青い線が戻りの通信を示しています)
上図は、Service1のPrivate SubnetにあるEC2インスタンスがインターネットにアクセスする際の流れを表しています。
1.Service1のPrivate SubnetではデフォルトルートがTransit Gatewayに向くように設定されているため、インターネット向けの通信はまずTransit Gatewayへ向かいます。
# private subnetのアウトバウンドトラフィックをtgwに流す resource "aws_route" "service1_to_tgw" { count = length(module.service1.private_route_table_ids) route_table_id = module.service1.private_route_table_ids[count.index] destination_cidr_block = "0.0.0.0/0" transit_gateway_id = aws_ec2_transit_gateway.main.id }
2.3.各Service VPCのTransit Gateway Attachmentは、Service用のTransit Gateway Route Tableを参照します。ここで0.0.0.0/0宛てのルートをShared VPCのTransit Gateway Attachmentへとルーティングしています。
resource "aws_ec2_transit_gateway_route_table" "service" { transit_gateway_id = aws_ec2_transit_gateway.main.id } # アウトバウンドトラフィックをshared-vpcに流す resource "aws_ec2_transit_gateway_route" "service_last_resort" { transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.service.id destination_cidr_block = "0.0.0.0/0" transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.shared.id }
4.5. その後は、おなじみのNAT Gatewayの処理フローを通り、インターネットに出ていきます。
6.7.戻りの通信(インターネット→NAT Gateway→EC2)も通常のNAT Gatewayの動きですが、Shared VPC側でService1への戻りルートがないと疎通ができません。そこで、Shared VPCのpublic subnetのルートテーブルに、Service1 CIDRへのルートとしてTransit Gatewayを設定しています。NAT Gatewayからの戻りトラフィックはTransit Gateway Attachmentへ送られます。
# shared-vpcのpublic subnetのルートテーブルにservice1へのルートを追加 # (NAT Gatewayを含むpublic subnetからservice1へのトラフィックをTransit Gatewayに流す) resource "aws_route" "shared_vpc_public_to_service1" { count = length(module.shared_vpc.public_route_table_ids) route_table_id = module.shared_vpc.public_route_table_ids[count.index] destination_cidr_block = module.service1.vpc_cidr_block transit_gateway_id = aws_ec2_transit_gateway.main.id }
8.9.10. Shared VPCのTransit Gateway AttachmentはShared VPC用のルートテーブルを見て、Service1のCIDR宛て(Service1側のTransit Gateway Attachment)へルーティングし、最終的にEC2インスタンスに到達します。
resource "aws_ec2_transit_gateway_route_table" "shared_route_table" { transit_gateway_id = aws_ec2_transit_gateway.main.id } # shared-vpcのTransit Gatewayルートテーブルからservice1への戻りルート resource "aws_ec2_transit_gateway_route" "shared_vpc_to_service1_return" { transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.shared_route_table.id destination_cidr_block = module.service1.vpc_cidr_block transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.service1.id }
Service間の通信がブロックされるパターン
上図は、Service1からService2への通信を試みた場合のフローです。
1.2.まではインターネット通信と同じで、Transit Gateway Attachmentへ向かいます。
3.Service用のTransit Gateway Route Tableでは、各ServiceのCIDR(Service1やService2宛て)をblackholeに設定することで、Service間の通信を破棄しています。
# service間のルートをブロックする resource "aws_ec2_transit_gateway_route" "block_inter_services" { for_each = toset([module.service1.vpc_cidr_block, module.service2.vpc_cidr_block]) transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.service.id destination_cidr_block = each.key blackhole = true }
これにより、Service間通信を行えないように制御しています。
検証
Reachability Analyzerを利用して疎通確認を行います。network_insights.tfで以下のように定義しています。
# service1のインスタンスからservice2の接続テスト resource "aws_ec2_network_insights_path" "service1_to_service2_path" { source = module.service1_instance.id destination = module.service2_instance.primary_network_interface_id protocol = "tcp" tags = { Name = "service1-to-service2-path" } } # service1のインスタンスからshared-vpcのigwへの接続テスト resource "aws_ec2_network_insights_path" "service1_to_shared_vpc_internetgateway_path" { source = module.service1_instance.id destination = module.shared_vpc.igw_id protocol = "tcp" tags = { Name = "service1-to-shared-vpc-internetgateway-path" } }
1つ目: Service1のインスタンスからService2へ接続するパス
2つ目: Service1のインスタンスからShared VPCのIGWへ接続するパス
この2つのパスを定義してAWSコンソールのReachability Analyzerから分析すると、Service1→Service2は失敗し、Service1→Shared VPCは成功していることがわかります。
2つ目(Service1→Shared VPC)を詳しく見ると、途中でNAT Gatewayを経由していることが確認できます。
また、Service1のインスタンスにログインし、実際にインターネットにアクセスできるかを確認します。問題なく接続できました。
sh-5.2$ curl www.google.com --head HTTP/1.1 200 OK
念のため、tracerouteでNAT Gatewayを経由しているかどうかもチェックしてみます。
sh-5.2$ traceroute 8.8.8.8 traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets 1 * * * 2 ip-10-107-3-21.ap-northeast-1.compute.internal (10.107.3.21) 0.833 ms 0.816 ms 1.275 ms
2ホップ目でNat Gatewayのeniのipを経由していることが分かります。
まとめ
実際にアウトバウンド通信を1つのVPCに集約する構成を検証したところ、想定以上にTransit Gateway Attachmentのコストがかかったり、ルーティングの設定で気をつける点(Service間で通信させたくない場合はblackholeを設定する、Shared VPC側に戻りのルートを設定しないとNAT Gatewayからの復路が通らない、など)が多々あり、とても勉強になりました。
また、Terraformで構築してみると、VPCごとに設定すべきリソースやルートが多く、運用時にはルート設定ミスを防ぐためにも、モジュール化してシンプルに追加できるようにする仕組みが重要だと感じました。私自身Transit Gatewayを触るのは初めてだったので、もし設定やルートテーブルの管理方法などでより良いアプローチがあれば、ぜひ教えていただけると嬉しいです。
iimon採用サイト / Wantedly / Green