VPCピアリングを試す
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の中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー