記事の内容
terraformでマルチアカウント環境のtransitgatewayを構築する方法を紹介します。
記事の長さ
約5分程度で読めます。
全体図
サンプルAWS環境を作成(SSM接続可能EC2)
# localの値は各アカウントで変えてください
terraform {
required_version = ">= 1.9.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.14"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
locals {
region = "ap-northeast-1"
cidr_block = "10.10.0.0/16"
private_subnet = "10.10.1.0/24"
az = "ap-northeast-1a"
name_prefix = "network-test"
tags = {
Environment = "dev"
Component = "network-test"
}
endpoint_service_map = {
ssm = "com.amazonaws.${local.region}.ssm"
ssmmessages = "com.amazonaws.${local.region}.ssmmessages"
ec2messages = "com.amazonaws.${local.region}.ec2messages"
}
}
data "aws_ssm_parameter" "al2023_x86_64" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64"
}
resource "aws_vpc" "this" {
cidr_block = local.cidr_block
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(local.tags, { Name = "${local.name_prefix}-vpc" })
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.this.id
cidr_block = local.private_subnet
availability_zone = local.az
map_public_ip_on_launch = false
tags = merge(local.tags, { Name = "${local.name_prefix}-private-${local.az}" })
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.this.id
tags = merge(local.tags, { Name = "${local.name_prefix}-private-rt" })
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
resource "aws_security_group" "instance" {
name = "${local.name_prefix}-instance"
description = "Allow instance egress"
vpc_id = aws_vpc.this.id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.tags, { Name = "${local.name_prefix}-instance-sg" })
}
resource "aws_security_group" "endpoint" {
name = "${local.name_prefix}-endpoint"
description = "Allow HTTPS access to interface endpoints"
vpc_id = aws_vpc.this.id
ingress {
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.this.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.tags, { Name = "${local.name_prefix}-endpoint-sg" })
}
resource "aws_vpc_endpoint" "interface" {
for_each = local.endpoint_service_map
vpc_id = aws_vpc.this.id
service_name = each.value
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.private.id]
security_group_ids = [aws_security_group.endpoint.id]
private_dns_enabled = true
tags = merge(local.tags, { Name = "${local.name_prefix}-${each.key}-endpoint" })
}
resource "aws_iam_role" "ssm" {
name = "${local.name_prefix}-ssm-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
tags = local.tags
}
resource "aws_iam_role_policy_attachment" "ssm_core" {
role = aws_iam_role.ssm.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ssm" {
name = "${local.name_prefix}-ssm-profile"
role = aws_iam_role.ssm.name
}
resource "aws_instance" "private" {
ami = data.aws_ssm_parameter.al2023_x86_64.value
instance_type = "t3.micro"
subnet_id = aws_subnet.private.id
vpc_security_group_ids = [aws_security_group.instance.id]
iam_instance_profile = aws_iam_instance_profile.ssm.name
associate_public_ip_address = false
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
}
tags = merge(local.tags, { Name = "${local.name_prefix}-ec2" })
}
output "vpc_id" {
value = aws_vpc.this.id
}
output "private_subnet_id" {
value = aws_subnet.private.id
}
output "instance_id" {
value = aws_instance.private.id
}
output "endpoint_ids" {
value = { for key, ep in aws_vpc_endpoint.interface : key => ep.id }
}
これで VPC,PrivateSubnet,EC2,endpointの最小構成が作成できます。
endpointは高価のため、テスト終了後は必ずリソースを削除してください。
TGW本体、RAMを作成
tgw本体と、tgwを承認されたアカウントに見えるようにするため、ramを作成します。
ここでは、デモとしてAuto_accept_shared_attachments = "enable" により共有されたアタッチメントを自動受け入れ可能にしています。
自動受け入れしない場合は、別途マネジメントコンソールで手動承認するか、アタッチメントを作成したアカウントで、承認用リソースを、別途定義する必要があります。
# TGWアタッチメント追加を承認(TGW作成アカウント側で)
resource "aws_ec2_transit_gateway_vpc_attachment_accepter" "workload" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.this.id
tags = {
Side = "Accepter"
}
}
Organization配下なら、以下定義で、同組織なら自動承認にすることが可能です。
# 既存のTGW
resource "aws_ec2_transit_gateway" "this" {
description = "example-tgw"
# VPCアタッチメントの共有を自動承認する
auto_accept_shared_attachments = "enable"
var.share_principal_account_id にワークロード側アカウント ID を指定することで RAM 共有先を紐付けます。
以下定義例
aws_ec2_transit_gateway "this" {
description = "tgw"
auto_accept_shared_attachments = "enable"
default_route_table_association = "disable"
default_route_table_propagation = "disable"
}
aws_ram_resource_share "tgw" {
name = "tgw-share"
allow_external_principals = true
}
aws_ram_principal_association "share_target" {
resource_share_arn = aws_ram_resource_share.tgw.arn
principal = var.share_principal_account_id
}
Apply画面例
アカウントBのマネジメントコンソールで、きちんとram共有がなっているか確認
※RAM周りは初見だと依存関係エラーが多発しやすいです。
アカウントAにTGWアタッチメントを作成
TGW作成後、RAM共有が有効になっていることを確認し、構築します。
aws_ec2_transit_gateway_vpc_attachment "this" {
subnet_ids = [aws_subnet.private.id] # アタッチメントを置くsubnetを指定
transit_gateway_id = aws_ec2_transit_gateway.this.id # TGWのIDを指定
vpc_id = aws_vpc.this.id # VPCIDを指定
}
相手アカウントに、tgwアタッチメントを追加
# まじで定義例 普通にTGWIDを手打ちしてもいいです
# stateからTGWIDを抜き出しています(バケットポリシー設定必須)
locals {
network_tgw_id = data.terraform_remote_state.network.outputs.transit_gateway_id
}
# 同様に設定
aws_ec2_transit_gateway_vpc_attachment "this" {
subnet_ids = [aws_subnet.private.id]
transit_gateway_id = local.network_tgw_id
vpc_id = aws_vpc.this.id
}
TGWルートテーブル作成
TGW構築は「TGWルートテーブル、VPCルートテーブル、セキュリティグループ」の三段階の設定が肝です。
この中で、TGWルートテーブルは独自の概念で、ミスが起きやすいです。
ざっくりと、三つのリソースを定義します。
aws_ec2_transit_gateway_route_table
→terraformあるあるな書き方です。ルートテーブルの箱を作ります。ルート情報は後で入れます。
aws_ec2_transit_gateway_route_table_association
→ざっくりアタッチメントとTGWルートテーブルを紐づけるという認識でいい。
VPCが、どのtgwルートテーブルのルート情報を見るか。
aws_ec2_transit_gateway_route_table_propagation
→伝播。アタッチメントが持っている情報(自VPCのIPなど)をルートテーブルに自動反映
# TGW用のルートテーブルを作成
resource "aws_ec2_transit_gateway_route_table" "example" {
transit_gateway_id = aws_ec2_transit_gateway.example.id
}
# VPCアタッチメントをルートテーブルに関連付ける
resource "aws_ec2_transit_gateway_route_table_association" "example" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.example.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.example.id
}
# VPCのルート情報をルートテーブルに伝播させる
resource "aws_ec2_transit_gateway_route_table_propagation" "example" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.example.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.example.id
}
これにより、TGW ルートテーブルへ両 VPC のルートが伝播し、相互通信の基盤が整います。
VPCルートテーブルに TGW 経由のルートを追加する
Transit Gateway を使って VPC 間を接続した場合、各VPCのルートテーブルに「相手VPCへのルート」を明示的に追加する必要があります。これを忘れると、TGW自体は正しく動いていても VPC 間で通信できません。
# 例
resource "aws_route" "to_peer_vpc" {
route_table_id = aws_route_table.private.id # 通信させたいサブネットが関連付けられているルートテーブルを指定する。
destination_cidr_block = "10.20.0.0/16" # 相手先VPCのCIDRブロックを指定する。
transit_gateway_id = aws_ec2_transit_gateway.example.id # 宛先までの経路を Transit Gateway 経由にする。
}
セキュリティグループの設定ポイント
VPC間の接続を確認する際は、まず ICMP(ping)を通す設定があれば十分です。
この場合、双方のVPCで以下が必要です。
インスタンス側のセキュリティグループ
相手VPCのCIDRからの ICMP (type: all) を許可するルールを追加
相手先のインスタンスのセキュリティグループ
同様に、こちらのVPC CIDR からの ICMP を許可するルールを追加
👉 これで ping を使ったシンプルな疎通確認が可能になります。
※ アプリケーション通信では、任意のポートを開ける必要あり
通信テスト
transit Gateway の設定が完了したら、最後に実際に通信できるかどうかを確認します。
今回はシンプルに ICMP (ping) を使って、相手VPCのEC2インスタンスに疎通できるかテストしました。
以下のスクリーンショットの通り、片方のVPCのEC2から相手VPCのEC2 (10.20.1.103) へ ping を実行すると、応答が返ってきています。
これにより、VPC間の接続が正しく確立していることが確認できました。