0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Transit gatewayをterraformで管理したい

Last updated at Posted at 2025-09-27

記事の内容

terraformでマルチアカウント環境のtransitgatewayを構築する方法を紹介します。

記事の長さ

約5分程度で読めます。

全体図

drawio (1).png

サンプル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は高価のため、テスト終了後は必ずリソースを削除してください。
{E0651D2D-BFF5-47EB-9314-A65DAB3F4F9D}.png

{2DFD3D80-F038-4D49-9505-E0CC2BF62130}.png

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画面例
{4C1B9DDF-0C7B-46E6-9785-EA20FE9AFD82}.png
アカウントBのマネジメントコンソールで、きちんとram共有がなっているか確認
※RAM周りは初見だと依存関係エラーが多発しやすいです。
タイトルなし.png

アカウント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
}

{1428245C-9140-47D9-A9C5-99CC1FA48523}.png

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 を実行すると、応答が返ってきています。
{261D36C6-C123-4010-B26F-C2F21ED0A0A7}.png
これにより、VPC間の接続が正しく確立していることが確認できました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?