iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

複数VPCのインターネット通信を1つのVPCに集約する構成をTerraformで書いてみた

はじめに

こんにちは、iimonでエンジニアをしているhogeです。 現在、弊社のワークロードで使っているVPCの数は多くありませんが、将来的にVPC数が増えた場合、NAT GatewayVPC 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)にのみ設置した場合の固定費は以下の通りです。

  • NAT Gateway固定費: 89.28 USD(Shared VPCにのみ設置)
  • Transit Gateway Attachment費用 (10 VPC分): 50.4×10=504USD

固定費の合計は、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 GatewayVPC Endpointを配置する場合より割高になるケースもあります。通信量が多いVPCについては、それぞれのVPC内にNAT GatewayVPC Endpointを置いたほうが費用を抑えられる可能性があります。すべてのワークロードが大量の通信を行うわけではないと思うので、基本は集約構成にしつつ、通信量の多いVPCだけは個別にNAT GatewayVPC Endpointを設置してTransit Gatewayを経由しないようにする、というハイブリッドな対応がよさそうです。

集約構成を構築してみる

まず、下図のような構成を想定します。VPCが2つあり、それぞれにNAT Gatewayが存在しています。Private SubnetにあるEC2インスタンスは、NAT Gatewayを経由してインターネットへアクセスします。

この構成を、Transit Gatewayを使って各サービスVPCのPrivate SubnetからShared VPC(共通VPC)のNAT Gatewayへとアウトバウンド通信を集約するように変更すると、下図のようになります。サービスのVPC同士は相互に通信できないように制御しています。 Terraformのソースコードは以下のリポジトリに配置しています。

github.com

  • 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