こしあん
2025-01-16

VPCピアリングを試す


17{icon} {views}

Terraformを使ってプライベートサブネット同士のVPCピアリングを構築し、承認作業とルート設定を行うだけで相互通信を実現。セッションマネージャを使ったサーバ疎通確認やNATインスタンスを採用する手順など、必要なポイントを簡潔にまとめてみました。

はじめに

  • さんざん試験でいろいろ見てきたVPCピアリングを試してみた
  • VPCピアリングとは、2つのVPC間をインターネットを経由せずに接続するもので、EC2間の接続に使える
  • ピアリング方法は簡単で、VPCを2個作ったらピアリングを確立して、各ルートテーブルを編集するだけ
  • ただピアリングの確立にはマネジメントコンソールからの手動承認がいるので注意

作るもの

割と適当に構成してみた。

  • VPCピアリングの簡単な実装として、2個のVPCのそろぞれのプライベートサブネットに1個EC2を作る。最低限このEC2だけ必要
  • EC2では疎通テスト用にnginxを設置する。これは相手のEC2のプラベートIPをcurlで指定して取得できるかどうかを確認するため
  • EC2へのアクセスはSession Managerを使う。ぶっちゃけEC2をパブリックサブネットにおいてもいいが、キーペアの管理がだるいため、Session Manager前提とした
  • Session Managerの利用には、VPCエンドポイントかNATゲートウェイが必要。わかりやすいようにNATとした。ただコストを抑えるためにfck-nat(NATインスタンス)を使っている。
  • 例によってChatGPTのo1-miniによって書かせたTerraformで実装する

VPCのモジュール化

VPCが再利用しやすいようにモジュール化する。以下のようなソース構成。

/
├── main.tf
├── ec2.tf
├── vpc_cluster.tf
└── modules/
    └── vpc/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

モジュール以下に格納することで、VPCコンポーネントが再利用しやすくなる。以下のようにした。

modules/vpc/main.tf

# Define a VPC
resource "aws_vpc" "main" {
  cidr_block                       = var.vpc_cidr_block
  enable_dns_support               = true
  enable_dns_hostnames             = true
  assign_generated_ipv6_cidr_block = true

  tags = {
    Name = var.vpc_name
  }
}

# Internet gateway
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.vpc_name}-igw"
  }
}

# Public subnet
# (10.0.0.0:20 -> [10.0.0.0:24, 10.0.1.0:24, 10.0.2.0:24])
resource "aws_subnet" "public" {
  count                           = length(var.availability_zones)
  vpc_id                          = aws_vpc.main.id
  cidr_block                      = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index)
  ipv6_cidr_block                 = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, count.index)
  availability_zone               = element(var.availability_zones, count.index)
  map_public_ip_on_launch         = false
  assign_ipv6_address_on_creation = true

  tags = {
    Name = "${var.vpc_name}-public-${count.index + 1}"
  }
}

# Private subnet
# (10.0.0.0:20 -> [10.0.4.0:23, 10.0.6.0:23, 10.0.8.0:23])
resource "aws_subnet" "private" {
  count                           = length(var.availability_zones)
  vpc_id                          = aws_vpc.main.id
  cidr_block                      = cidrsubnet(aws_vpc.main.cidr_block, 3, 2 + count.index)
  ipv6_cidr_block                 = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, 4 + count.index)
  availability_zone               = element(var.availability_zones, count.index)
  map_public_ip_on_launch         = false
  assign_ipv6_address_on_creation = true

  tags = {
    Name = "${var.vpc_name}-private-${count.index + 1}"
  }
}

# Public route table
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  route {
    ipv6_cidr_block = "::/0"
    gateway_id      = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "${var.vpc_name}-public-route-table"
  }
}

resource "aws_route_table_association" "public_assoc" {
  count          = length(aws_subnet.public)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

# Private route table
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.vpc_name}-private-route-table"
  }
}

resource "aws_route_table_association" "private_assoc" {
  count          = length(aws_subnet.private)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private.id
}

# fck-nat for cost savings (NAT instance)
resource "aws_eip" "nat_eip" {
  domain = "vpc"

  tags = {
    Name = "${var.vpc_name}-nat-eip"
  }
}

module "fck-nat" {
  source = "RaJiska/fck-nat/aws"

  name               = "${var.vpc_name}-fck-nat"
  vpc_id             = aws_vpc.main.id
  subnet_id          = aws_subnet.public[0].id
  eip_allocation_ids = [aws_eip.nat_eip.id] # Allocation ID of an existing EIP
  instance_type      = "t4g.nano"

  update_route_tables = true
  route_tables_ids = {
    "private-route-table" = aws_route_table.private.id
  }
}
  • fck-natの扱いについては公式参照。基本的にはNATインスタンスで、Terraformのプロバイダーも用意されている
  • 複数(東京リージョンは3つ)のサブネットにまたがるVPCを作る
  • IPv4の数は、パブリックサブネットが256、プライベートサブネットが512
  • ルートテーブルはパブリックはIGWにそのままつけて、プライベートは外部に何も通信できない(アイソレート状態)で定義して、fck-natのモジュール側でルーティング変更する

modules/vpc/variable.tf

variable "vpc_name" {
  type        = string
  description = "Name of VPC"
}

variable "vpc_cidr_block" {
  type        = string
  description = "CIDR block of VPC"
}

variable "availability_zones" {
  type        = list(string)
  description = "List of availability zones"
  default     = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}
  • ユーザーから入力として受け取る部分。VPCの名前やCIDRブロックの指定を行う

modules/vpc/outputs.tf

output "vpc_id" {
  value = aws_vpc.main.id
}

output "vpc_cidr_block" {
  value = aws_vpc.main.cidr_block
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

output "public_subnet_cidr_blocks" {
  value = aws_subnet.public[*].cidr_block
}

output "private_subnet_cidr_blocks" {
  value = aws_subnet.private[*].cidr_block
}

output "public_route_table_id" {
  value = aws_route_table.public.id
}

output "private_route_table_id" {
  value = aws_route_table.private.id
}

外部から参照するために必要。もうちょっとうまいやり方があるかもしれない

VPCピアリングの構築(vpc_cluster.tf)

モジュールの外のvpc_cluster.tfで、作成したモジュールを活用してVPCを2個作成し、ピアリングを構築する。ルートテーブルの更新が必要。

## ============
## VPC 2個
## ============
module "vpc1" {
  source             = "./modules/vpc"
  vpc_name           = "vpc-1"
  vpc_cidr_block     = "10.10.0.0/20"
  availability_zones = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}

module "vpc2" {
  source             = "./modules/vpc"
  vpc_name           = "vpc-2"
  vpc_cidr_block     = "10.10.16.0/20"
  availability_zones = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}

## ============
## VPC ピアリング
## ============
# VPCピアリングのリクエスト
resource "aws_vpc_peering_connection" "peer" {
  vpc_id        = module.vpc1.vpc_id
  peer_vpc_id   = module.vpc2.vpc_id
  peer_region   = "ap-northeast-1"  # 両者が同じリージョンの場合

  tags = {
    Name = "vpc1-to-vpc2"
  }
}

# VPC1 のルートテーブルに VPC2 へのルートを追加
resource "aws_route" "vpc1_to_vpc2" {
  route_table_id = module.vpc1.private_route_table_id # VPC1 のプライベートルートテーブルID
  destination_cidr_block    = module.vpc2.vpc_cidr_block
  vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
}

# VPC2 のルートテーブルに VPC1 へのルートを追加
resource "aws_route" "vpc2_to_vpc1" {
  route_table_id            = module.vpc2.private_route_table_id # VPC2 のプライベートルートテーブルID
  destination_cidr_block    = module.vpc1.vpc_cidr_block
  vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
}

terraform applyするとピアリングがすぐに構築されるのではなく、VPC側で承認が必要。マネジメントコンソールの、VPCのダッシュボードから「ピアリング接続」→「アクション」から承認

承認終わるとルートテーブルに飛べる。ステータスがすぐに「アクティブ」になるはず。承認をしないとピアリングの送信先のステータスは「ブラックホール」になっている。

EC2のnginxの作成(ec2.tf)

  • Amazon Linux 2023のインスタンスを各VPCに1個ずつ作り、ユーザースクリプトでnginxを起動すればいい
  • ピアリングの接続を許可するために、対象のEC2がいるサブネットのCIDRからのポート80のInboundを許可している
  • またSession Managerのために、Outbandは全許可にしている
  • Session Manager用のインスタンスプロファイル(ロール)も用意している
## ============
## 各VPCのサブネットにEC2を1個ずつ作成
## ============
# Amazon Linux 2023
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["137112412989"] # AmazonのAMI所有者ID

  filter {
    name = "name"
    # Amazon Linux 2023 AMIの名前パターン。minimumを除外する
    values = ["al2023-ami-2023*-kernel-*-x86_64"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Security Groups for ec2
resource "aws_security_group" "ec2_sgs" {
  for_each = {
    vpc1 = {
      vpc_id = module.vpc1.vpc_id
      source = module.vpc2.private_subnet_cidr_blocks[0]
    }
    vpc2 = {
      vpc_id = module.vpc2.vpc_id
      source = module.vpc1.private_subnet_cidr_blocks[0]
    }
  }

  name        = "${each.key}-ec2_sg"
  description = "A security group for EC2 instances"
  vpc_id      = each.value.vpc_id

  ingress {
    description = "Allow HTTP traffic from vpc"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = [each.value.source]
  }

  egress {
    description      = "Allow all outbound traffic"
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}

# IAM role for Session Manager
resource "aws_iam_role" "ssm_role" {
  name = "ec2_ssm_role_nginx"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ssm_policy_attachment" {
  role       = aws_iam_role.ssm_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

# Creating IAM instance profile
resource "aws_iam_instance_profile" "ssm_instance_profile" {
  name = "ec2_ssm_instance_profile_nginx"
  role = aws_iam_role.ssm_role.name
}

# nginx EC2 instances
resource "aws_instance" "nginx" {
  for_each = {
    vpc1 = module.vpc1.private_subnet_ids[0] # 最初のプライベートサブネットを使用
    vpc2 = module.vpc2.private_subnet_ids[0]
  }

  ami                         = data.aws_ami.amazon_linux.id
  instance_type               = "t3.micro"
  subnet_id                   = each.value
  associate_public_ip_address = false
  vpc_security_group_ids      = [aws_security_group.ec2_sgs[each.key].id]
  iam_instance_profile        = aws_iam_instance_profile.ssm_instance_profile.name

  user_data = <<-EOF
              #!/bin/bash
              dnf update -y
              dnf install nginx -y
              systemctl start nginx
              systemctl enable nginx
              EOF

  tags = {
    Name = "${each.key}-nginx"
  }
}

# private ip of EC2s
output "nginx_ips" {
  value = {for k, v in aws_instance.nginx : k => v.private_ip}
}

接続確認

erraform applyすると、以下のようにnginxのローカルIPが出てくるはず。

Outputs:

nginx_ips = {
  "vpc1" = "10.10.4.236"
  "vpc2" = "10.10.20.161"
}

VPC1->VPC2への接続

マネジメントコンソールからSession Managerに接続し、VPC2のインスタンスにcurlを飛ばしてみる。nginxのデフォルトのメッセージが出てくるはず。(rootへの切り替えは必須ではなく、コンソールにIPが表示されてわかりやすいのでそうしている。sudo su -で切り替えられる)

[root@ip-10-10-4-236 ~]# curl http://10.10.20.161
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

VPC2→VPC1への接続

同様にVPC2のEC2にもSession Managerで接続し、VPC1のEC2にもcurlを飛ばす

[root@ip-10-10-20-161 ~]# curl http://10.10.4.236
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

いずれの場合もcurlが想定された結果となった。

ポイント

  • これらのEC2はプライベートサブネットに配置されており、インターネットから直接アクセスできない。また、セキュリティグループの設定もインターネットからのアクセス(パブリックIPからのアクセス)を許可していない
  • したがって、curlはVPCピアリングの接続から飛んできたものである

所感

  • 思ったより簡単にVPCピアリングできた
  • 承認がいるってのがちょっと面倒くさいところかもしれない


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です