VPC間通信におけるPrivate Linkとクロスゾーン負荷分散の組み合わせ
Posted On 2025-03-02
クロスゾーン負荷分散が有効なNLBをVPCエンドポイントサービスと連携し、CIDR重複時でもインターネット経由せずに通信できる例をTerraformで構築します。プライベートホストゾーンやセキュリティグループ設定など、Provider・Consumerの両VPCで柔軟なアクセス制御を行う手順を詳細に示します。
目次
はじめに
- ANSの勉強してて出てきた、Private Link+クロスゾーン負荷分散について確かめてみた
- クロスゾーン負荷分散どころかPrivate Linkをちゃんと試したことなかったので作ってみる
- Private LinkはVPCエンドポイント+Network Load Balancer(NLB)の組み合わせで、VPC間をインターネットに出ていかずに通信できる
- 同様のものにVPCピアリングがあるが、これはVPCの間の通信を全て許可してしまうもので、Private Linkのほうがサブネット単位やセキュリティグループでもう少し細かなコントロールが可能。VPCピアリングはCIDRが重複した場合は使えないが、Private LinkはCIDRが重複した場合も使える。
- 実はre:invent 2024でPrivate Linkが改良されており、必ずしもNLBいらなくなっているみたいなのでこれは古いやり方ではある
アーキテクチャー図
- 2つのVPCを作る。Private Linkの優位性を出すためにあえてVPCのCIDRを重複させる(これは本当は非推奨なので、実際使うときはなるべくCIDRを変えるように)
- ConsumerとProviderという2つのVPCを使い、ConsumerはユーザーがSession Managerで接続してcurlやnslookupを叩くもの。Providerは仮想的なWebサーバーを立てるもの
- 実際Webサーバーにアクセスするときは、NLBのアドレスや、Route 53の仮想ホストゾーンに作った適当なドメインに対してNLBのアドレスをAレコードと登録しそのアドレスでアクセスする。今回は後者(Route 53のプライベートホストゾーンを使う)でやってみる
実装のポイント
長いのでTerraformのコードをポイント単位でかいつまんで紹介
VPCを作る
マルチリージョンのフェイルオーバーテストで作ったVPCモジュールをそのまま使う
# Provider VPC
module "provider_vpc" {
source = "./modules/vpc"
vpc_name = "provider-vpc"
vpc_cidr_block = "10.11.0.0/24"
}
# Consumer VPC (with same CIDR)
module "consumer_vpc" {
source = "./modules/vpc"
vpc_name = "consumer-vpc"
vpc_cidr_block = "10.11.0.0/24"
}
このように2つのVPCを定義する
Provider側のNetwork Load Balancer
- Network Load Balancerの作り方。Provider側に作る。Publicに露出させないので、InternalでOK
enable_cross_zone_load_balancing
を切り替えることでクロスゾーン負荷分散の有効化・無効化ができる- クロスゾーン負荷分散に関係なく、NLBはProviderの複数のサブネットに対してまたがらしておく
- 個別のEC2は、NLBのリスナールールに対応するターゲットグループにアタッチしておく(これはALBと同様)
# NLB in Provider VPC
resource "aws_lb" "nlb" {
name = "provider-nlb"
internal = true
load_balancer_type = "network"
subnets = module.provider_vpc.private_subnet_ids
enable_cross_zone_load_balancing = true # クロスゾーン負荷分散を有効・無効化
tags = {
Name = "provider-nlb"
}
}
# Target Group for NLB
resource "aws_lb_target_group" "nlb_tg" {
name = "provider-target-group"
port = 80
protocol = "TCP"
vpc_id = module.provider_vpc.vpc_id
target_type = "instance"
health_check {
protocol = "TCP"
port = 80
interval = 30
healthy_threshold = 3
unhealthy_threshold = 3
}
}
# Register all EC2 instances with the target group
resource "aws_lb_target_group_attachment" "nlb_tg_attachment" {
count = length(aws_instance.provider_service)
target_group_arn = aws_lb_target_group.nlb_tg.arn
target_id = aws_instance.provider_service[count.index].id
port = 80
}
# NLB Listener
resource "aws_lb_listener" "nlb_listener" {
load_balancer_arn = aws_lb.nlb.arn
port = 80
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.nlb_tg.arn
}
}
Provider側:VPCエンドポイントサービス
- Provider側にはVPCエンドポイントサービスを作る
- VPCエンドポイントサービスとはVPCエンドポイントとは異なるもので、外部からアクセスさせるために作るもの。VPCエンドポイントは外部にアクセスさせるために使う。
- つまり、VPCエンドポイントが入口で、VPCエンドポイントサービスが出口。したがってConsumer側にはVPCエンドポイントが必要
# VPC Endpoint Service
resource "aws_vpc_endpoint_service" "endpoint_service" {
acceptance_required = false
network_load_balancer_arns = [aws_lb.nlb.arn]
tags = {
Name = "provider-endpoint-service"
}
}
Consumer側:VPCエンドポイント
- 次はコンシューマー側。VPCエンドポイントにはセキュリティグループを設定可能
- VPCエンドポイントはインターフェイス型とゲートウェイ型があるのが、インターフェイス型を選択
- (自分がやり方よくわからなかっただけだが、)一応VPCエンドポイントにもプライベートDNSは設定できるようである。なぜこれが必要かというと、NLBのドメインをConsumer側で名前解決できないため。
# Security Group for Consumer VPC Endpoint
resource "aws_security_group" "consumer_endpoint_sg" {
name = "consumer-endpoint-sg"
description = "Security group for VPC endpoint in consumer VPC"
vpc_id = module.consumer_vpc.vpc_id
# Consumer VPCのプライベートサブネットからのアクセスのみ許可
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = module.consumer_vpc.private_subnet_cidr_blocks
description = "Allow HTTP from consumer private subnets"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = {
Name = "consumer-endpoint-sg"
}
}
# VPC Endpoint in Consumer VPC
resource "aws_vpc_endpoint" "endpoint" {
vpc_id = module.consumer_vpc.vpc_id
service_name = aws_vpc_endpoint_service.endpoint_service.service_name
vpc_endpoint_type = "Interface"
subnet_ids = [module.consumer_vpc.private_subnet_ids[0]]
security_group_ids = [aws_security_group.consumer_endpoint_sg.id]
private_dns_enabled = false # ここでプライベートDNSを有効化することはできなかったので、別途プライベートホストゾーンを定義する
tags = {
Name = "consumer-endpoint"
}
}
Consumer側:Route 53のプライベートホストゾーン
- Consumer側でNLBのドメインが名前解決できないため、プライベートホストゾーンを定義し、DNSレコードを追加する
- イメージ的にはPublic ALBに対して、ドメインを紐づけるようなイメージ。プライベートホストゾーンとは、そのホストゾーン(VPC内)で通用するドメインのこと
- DNSエントリの追加の仕方はパブリックのときと同じ。ConsumerのEC2からアクセスするときは
provider-nlb.example.local
というURLでアクセスする。
# プライベートホストゾーンの作成(消費者VPCに紐づけ)
resource "aws_route53_zone" "consumer_private_zone" {
name = "example.local" // 内部で利用するドメイン名(例)
vpc {
vpc_id = module.consumer_vpc.vpc_id
}
}
# VPC EndpointのDNSエントリを指すAliasレコードの作成
resource "aws_route53_record" "provider_endpoint_record" {
zone_id = aws_route53_zone.consumer_private_zone.zone_id
name = "provider-nlb" // 結果として "provider-nlb.example.local" というFQDNが作成されます
type = "A"
ttl = 10
alias {
name = aws_vpc_endpoint.endpoint.dns_entry[0].dns_name
zone_id = aws_vpc_endpoint.endpoint.dns_entry[0].hosted_zone_id
evaluate_target_health = false
}
}
結果
- 全てProvider側の3つのAZにEC2を立ち上げている
- Consumer側のEC2にSession Managerで接続し、コマンドを実行
- リソースマップは以下のようになっている。EC2は全て3つのサブネットにまたがっている
クロスゾーン負荷分散なしの場合
Subnet 1のインスタンスのみアクセスされている
クロスゾーン負荷分散ありの場合
Subnet 1~3の全てのインスタンスにアクセスされている。ちなみにNLBの場合はデフォルトだとオフになっている。ALBは常にONでオフにすることはできない
全体コード
VPCモジュールのコードは省略。こちらを参照されたし
provider.tf
# Security Group for NLB target in Provider VPC
resource "aws_security_group" "provider_sg" {
name = "provider-service-sg"
description = "Security group for provider service"
vpc_id = module.provider_vpc.vpc_id
# NLBはセキュリティグループではなくサブネットに関連付けられるため、
# プロバイダVPCのプライベートサブネットCIDRからのアクセスを許可
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = module.provider_vpc.private_subnet_cidr_blocks
description = "Allow HTTP from private subnets"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = {
Name = "provider-service-sg"
}
}
# 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"]
}
}
# EC2 Instance in Provider VPC to be the service
variable "provider_service_instance_count" {
description = "Number of provider service instances to create (up to the number of available private subnets)"
type = number
default = 3
}
resource "aws_instance" "provider_service" {
count = min(var.provider_service_instance_count, length(module.provider_vpc.private_subnet_ids))
ami = data.aws_ami.amazon_linux.id # Amazon Linux 2023 AMI
instance_type = "t3.micro"
subnet_id = module.provider_vpc.private_subnet_ids[count.index]
vpc_security_group_ids = [aws_security_group.provider_sg.id]
user_data = <<-EOF
#!/bin/bash
dnf update -y
dnf install -y httpd
systemctl start httpd
systemctl enable httpd
echo "Hello from Provider VPC on Subnet ${count.index + 1}" > /var/www/html/index.html
EOF
tags = {
Name = "provider-service-${count.index + 1}"
}
}
# NLB in Provider VPC
resource "aws_lb" "nlb" {
name = "provider-nlb"
internal = true
load_balancer_type = "network"
subnets = module.provider_vpc.private_subnet_ids
# subnets = [module.provider_vpc.private_subnet_ids[0]]
enable_cross_zone_load_balancing = true # クロスゾーン分散を無効化
tags = {
Name = "provider-nlb"
}
}
# Target Group for NLB
resource "aws_lb_target_group" "nlb_tg" {
name = "provider-target-group"
port = 80
protocol = "TCP"
vpc_id = module.provider_vpc.vpc_id
target_type = "instance"
health_check {
protocol = "TCP"
port = 80
interval = 30
healthy_threshold = 3
unhealthy_threshold = 3
}
}
# Register all EC2 instances with the target group
resource "aws_lb_target_group_attachment" "nlb_tg_attachment" {
count = length(aws_instance.provider_service)
target_group_arn = aws_lb_target_group.nlb_tg.arn
target_id = aws_instance.provider_service[count.index].id
port = 80
}
# NLB Listener
resource "aws_lb_listener" "nlb_listener" {
load_balancer_arn = aws_lb.nlb.arn
port = 80
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.nlb_tg.arn
}
}
# VPC Endpoint Service
resource "aws_vpc_endpoint_service" "endpoint_service" {
acceptance_required = false
network_load_balancer_arns = [aws_lb.nlb.arn]
tags = {
Name = "provider-endpoint-service"
}
}
consumer.tf
# Security Group for Consumer VPC Endpoint
resource "aws_security_group" "consumer_endpoint_sg" {
name = "consumer-endpoint-sg"
description = "Security group for VPC endpoint in consumer VPC"
vpc_id = module.consumer_vpc.vpc_id
# Consumer VPCのプライベートサブネットからのアクセスのみ許可
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = module.consumer_vpc.private_subnet_cidr_blocks
description = "Allow HTTP from consumer private subnets"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = {
Name = "consumer-endpoint-sg"
}
}
# Security Group for Consumer Instance
resource "aws_security_group" "consumer_instance_sg" {
name = "consumer-instance-sg"
description = "Security group for consumer instance"
vpc_id = module.consumer_vpc.vpc_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = {
Name = "consumer-instance-sg"
}
}
# VPC Endpoint in Consumer VPC
resource "aws_vpc_endpoint" "endpoint" {
vpc_id = module.consumer_vpc.vpc_id
service_name = aws_vpc_endpoint_service.endpoint_service.service_name
vpc_endpoint_type = "Interface"
subnet_ids = [module.consumer_vpc.private_subnet_ids[0]]
security_group_ids = [aws_security_group.consumer_endpoint_sg.id]
private_dns_enabled = false # ここでプライベートDNSを有効化することはできなかったので、別途プライベートホストゾーンを定義する
tags = {
Name = "consumer-endpoint"
}
}
# Session Manager 利用用IAMロールとインスタンスプロファイル
resource "aws_iam_role" "ec2_ssm_role" {
name = "ec2-session-manager-role"
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_attach" {
role = aws_iam_role.ec2_ssm_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ec2_instance_profile" {
name = "ec2-instance-profile"
role = aws_iam_role.ec2_ssm_role.name
}
# EC2 Instance in Consumer VPC to test the connection
resource "aws_instance" "consumer_instance" {
ami = data.aws_ami.amazon_linux.id # Amazon Linux 2023 AMI
instance_type = "t3.micro"
subnet_id = module.consumer_vpc.private_subnet_ids[0]
vpc_security_group_ids = [aws_security_group.consumer_instance_sg.id]
iam_instance_profile = aws_iam_instance_profile.ec2_instance_profile.name
tags = {
Name = "consumer-instance"
}
}
# プライベートホストゾーンの作成(消費者VPCに紐づけ)
resource "aws_route53_zone" "consumer_private_zone" {
name = "example.local" // 内部で利用するドメイン名(例)
vpc {
vpc_id = module.consumer_vpc.vpc_id
}
}
# VPC EndpointのDNSエントリを指すAliasレコードの作成
resource "aws_route53_record" "provider_endpoint_record" {
zone_id = aws_route53_zone.consumer_private_zone.zone_id
name = "provider-nlb" // 結果として "provider-nlb.example.local" というFQDNが作成されます
type = "A"
alias {
name = aws_vpc_endpoint.endpoint.dns_entry[0].dns_name
zone_id = aws_vpc_endpoint.endpoint.dns_entry[0].hosted_zone_id
evaluate_target_health = false
}
}
vpcs.tf
# Provider VPC
module "provider_vpc" {
source = "./modules/vpc"
vpc_name = "provider-vpc"
vpc_cidr_block = "10.11.0.0/24"
}
# Consumer VPC (with same CIDR)
module "consumer_vpc" {
source = "./modules/vpc"
vpc_name = "consumer-vpc"
vpc_cidr_block = "10.11.0.0/24"
}
所感
- ALBっぽい挙動を期待しているのならクロスゾーン負荷分散ありでいいのではないかなと思った
- このへんの理論的な詳しい説明は以下の記事がよいので参考にすると良さそう
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー