Transit GatewayでVPC間通信する構成をTerraformで作成してみた

2019.11.14

以下のエントリで紹介されていたTransit GatewayでVPC間通信を行なう構成を、Terraformを使って構築してみました。

Transit Gatewayを利用してVPC間で通信してみた

構成図

ディレクトリ構造

.
├── .terraform-version
├── _main.tf
├── file
│   └── trgw-sandbox-local-bastion-keypair.pub
├── modules
│   ├── ec2
│   │   └── ec2.tf
│   ├── transit-gateway
│   │   └── vpc-attachment.tf
│   └── vpc
│       └── vpc.tf
├── ec2.tf
├── transit-gateway.tf
└── vpc.tf

※ 以下を参考にディレクトリ移動だけで一時クレデンシャルを設定できるようにしています。

方針

二つVPCを作り、双方にEC2インスタンスを立て、それらをTransit Gatewayに接続するということで、A-VPCとB-VPCで重複する処理が複数存在します。
そういった処理はmodule内に記載し、moduleを二度(A-VPC分とB-VPC分)呼び出すような方針で記述しています。

この方針に至った経緯は以下のエントリに記載しておりますので、よければこちらもご覧ください。

各ファイルの説明

_main.tf

ディレクトリ全体に関わる設定をこちらに記載しています。

  • terraformのrequired_version
  • aws providerのversion
  • s3 buckendの設定
  • 複数のtfファイルで参照する共通変数
    • env 環境。local,stag,prod など
    • basename プロジェクト名。リソースに名前付けする際に接頭辞として必ず使う

VPC

vpc.tf

variable vpc {
  type = map(string)
  default = {
    A = "10.0.0.0/16",
    B = "172.16.0.0/16"
  }
}

module A {
  source = "./modules/vpc"

  env      = var.env
  basename = var.basename
  name     = "A"
  cidr     = var.vpc["A"]
}
module B {
  source = "./modules/vpc"

  env      = var.env
  basename = var.basename
  name     = "B"
  cidr     = var.vpc["B"]
}

modules/vpc/vpc.tf

variable env {}
variable basename {}
variable name {}
variable cidr {}

data aws_availability_zones az {}

resource aws_vpc vpc {
  cidr_block           = var.cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.basename}-${var.env}-vpc-${var.name}"
  }
}

resource aws_internet_gateway igw {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name = "${var.basename}-${var.env}-vpc-${var.name}-igw"
  }
}
resource aws_subnet public {
  count                   = 2
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = cidrsubnet(aws_vpc.vpc.cidr_block, 8, count.index)
  availability_zone       = data.aws_availability_zones.az.names[count.index]
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.basename}-${var.env}-vpc-${var.name}-public-subnet-${count.index + 1}"
  }
}
resource aws_route_table public {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.basename}-${var.env}-vpc-${var.name}-public-subnet-rtb"
  }
}
resource aws_route public {
  route_table_id         = aws_route_table.public.id
  gateway_id             = aws_internet_gateway.igw.id
  destination_cidr_block = "0.0.0.0/0"
}
resource aws_route_table_association public {
  count          = 2
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource aws_network_acl acl {
  vpc_id = aws_vpc.vpc.id

  subnet_ids = aws_subnet.public.*.id

  ingress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  egress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  tags = {
    Name = "${var.basename}-${var.env}-vpc-${var.name}-acl"
  }
}

output vpc_id {
  value = aws_vpc.vpc.id
}
output cidr {
  value = aws_vpc.vpc.cidr_block
}
output public_subnet_ids {
  value = aws_subnet.public.*.id
}
output public_route_table_id {
  value = aws_route_table.public.id
}

EC2

ec2.tf

事前にSSHキーペアを作成して、公開鍵を file/trgw-sandbox-local-bastion-keypair.pub に配置します。

locals {
  ssh_allowed_cidr = "xxx.xxx.xxx.xxx/32"
}

module ec2-in-A {
  source = "./modules/ec2"

  env      = var.env
  basename = "${var.basename}-A"

  vpc_id            = module.A.vpc_id
  subnet_id         = module.A.public_subnet_ids[0]
  ping_allowed_cidr = module.B.cidr
  ssh_allowed_cidr  = local.ssh_allowed_cidr
  public_key_path   = "./file/${var.basename}-${var.env}-bastion-keypair.pub"
  private_ip        = "10.0.0.161"
}
module ec2-in-B {
  source = "./modules/ec2"

  env      = var.env
  basename = "${var.basename}-B"

  vpc_id            = module.B.vpc_id
  subnet_id         = module.B.public_subnet_ids[0]
  ping_allowed_cidr = module.A.cidr
  ssh_allowed_cidr  = local.ssh_allowed_cidr
  public_key_path   = "./file/${var.basename}-${var.env}-bastion-keypair.pub"
  private_ip        = "172.16.0.107"
}

modules/ec2/ec2.tf

元のブログエントリの内容と合わせるために、private IPを指定できるようにしました。こんなことできるなんて初めて知りました。

ただし、通常private IPを指定する必要がある場合は少ないと思います。ですのでdefault値をnullにし、指定が無い場合は自動採番されるようにしました。

variable env {}
variable basename {}

variable vpc_id {}
variable subnet_id {}
variable ssh_allowed_cidr {}
variable ping_allowed_cidr {}

variable public_key_path {}
variable private_ip {
  default = null
}

locals {
  ingress_rules = {
    ssh = {
      protocol  = "tcp",
      from_port = 22,
      to_port   = 22,
      cidr      = var.ssh_allowed_cidr
    },
    ping = {
      protocol  = "icmp",
      from_port = 8,
      to_port   = 0,
      cidr      = var.ping_allowed_cidr
    }
  }
}

resource "aws_security_group" ec2 {
  name        = "ec2-sg"
  description = "ec2-sg"
  vpc_id      = var.vpc_id
}

resource aws_security_group_rule egress {
  type              = "egress"
  security_group_id = aws_security_group.ec2.id
  protocol          = -1
  from_port         = 0
  to_port           = 0
  cidr_blocks       = ["0.0.0.0/0"]
}

resource aws_security_group_rule ingress {
  for_each          = local.ingress_rules
  type              = "ingress"
  security_group_id = aws_security_group.ec2.id
  protocol          = each.value.protocol
  from_port         = each.value.from_port
  to_port           = each.value.to_port
  cidr_blocks       = [each.value.cidr]
}

resource aws_key_pair bastion_keypair {
  key_name   = "${var.basename}-${var.env}-bastion-keypair"
  public_key = file(var.public_key_path)
}

data aws_subnet subnet {
  id = var.subnet_id
}

resource aws_instance ec2 {
  ami                         = "ami-0ff21806645c5e492"
  instance_type               = "t2.micro"
  availability_zone           = data.aws_subnet.subnet.availability_zone
  monitoring                  = false
  associate_public_ip_address = true
  key_name                    = aws_key_pair.bastion_keypair.key_name
  tags = {
    Name = "${var.basename}-${var.env}-bastion"
  }
  vpc_security_group_ids = [aws_security_group.ec2.id]
  subnet_id              = var.subnet_id

  private_ip = var.private_ip
}

Transit Gateway

transit-gateway.tf

resource aws_ec2_transit_gateway example {
  vpn_ecmp_support                = "disable"
  default_route_table_association = "disable"
  default_route_table_propagation = "disable"
  auto_accept_shared_attachments  = "disable"
}

resource aws_ec2_transit_gateway_route_table example {
  transit_gateway_id = aws_ec2_transit_gateway.example.id
}

module A-attachment {
  source = "./modules/transit-gateway"

  vpc_id                         = module.A.vpc_id
  subnet_ids                     = module.A.public_subnet_ids
  route_table_id                 = module.A.public_route_table_id
  transit_gateway_id             = aws_ec2_transit_gateway.example.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.example.id
  destination_vpc_cidr           = module.B.cidr
}

module B-attachment {
  source = "./modules/transit-gateway"

  vpc_id                         = module.B.vpc_id
  subnet_ids                     = module.B.public_subnet_ids
  route_table_id                 = module.B.public_route_table_id
  transit_gateway_id             = aws_ec2_transit_gateway.example.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.example.id
  destination_vpc_cidr           = module.A.cidr
}

modules/transit-gateway/vpc-attachment.tf

variable vpc_id {}
variable subnet_ids {}
variable route_table_id {}
variable transit_gateway_id {}
variable transit_gateway_route_table_id {}
variable destination_vpc_cidr {}

resource aws_ec2_transit_gateway_vpc_attachment vpc_attachment {
  subnet_ids                                      = var.subnet_ids
  transit_gateway_id                              = var.transit_gateway_id
  vpc_id                                          = var.vpc_id
  transit_gateway_default_route_table_association = false
  transit_gateway_default_route_table_propagation = false
}
resource aws_ec2_transit_gateway_route_table_association association {
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.vpc_attachment.id
  transit_gateway_route_table_id = var.transit_gateway_route_table_id
}
resource aws_ec2_transit_gateway_route_table_propagation propagation {
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.vpc_attachment.id
  transit_gateway_route_table_id = var.transit_gateway_route_table_id
}
resource aws_route to-trgw {
  route_table_id         = var.route_table_id
  transit_gateway_id     = var.transit_gateway_id
  destination_cidr_block = var.destination_vpc_cidr
}

アクセス確認

Transit Gatewayにアタッチメントしたリソース間で通信を確認してみます。

A-Server → B-Server

[ec2-user@ip-10-0-0-161 ~]$ ping -c 3 172.16.0.107
PING 172.16.0.107 (172.16.0.107) 56(84) bytes of data.
64 bytes from 172.16.0.107: icmp_seq=1 ttl=254 time=0.853 ms
64 bytes from 172.16.0.107: icmp_seq=2 ttl=254 time=0.526 ms
64 bytes from 172.16.0.107: icmp_seq=3 ttl=254 time=0.510 ms

--- 172.16.0.107 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2031ms
rtt min/avg/max/mdev = 0.510/0.629/0.853/0.160 ms

B-Server → A-Server

[ec2-user@ip-172-16-0-107 ~]$ ping -c 3 10.0.0.161
PING 10.0.0.161 (10.0.0.161) 56(84) bytes of data.
64 bytes from 10.0.0.161: icmp_seq=1 ttl=254 time=0.878 ms
64 bytes from 10.0.0.161: icmp_seq=2 ttl=254 time=0.547 ms
64 bytes from 10.0.0.161: icmp_seq=3 ttl=254 time=0.539 ms

--- 10.0.0.161 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2039ms
rtt min/avg/max/mdev = 0.539/0.654/0.878/0.160 ms

通信できました!

まとめ

月並みの感想で恐縮ですが、やはり手を動かすのは大事ですね。どんなリソースが必要か理解が深まりました。