ALBとPrivate Linkで複数のVPCにルーティング
ALBにVPCエンドポイント(ENI)をターゲットとして設定し、パスごとにNLB→EC2へトラフィックを振り分ける仕組みを構築しています。特にセキュリティグループの設定やTerraformの依存関係が要となり、正しい構成が確認できればログ一元化などのメリットを得られます。
目次
はじめに
- ANSで出てきたネタ。1個目のVPCにALBをおいて、VPCエンドポイント(Elastic Network Interface:ENI)をALBのターゲットグループに指定
- パスの振り分けは最初のALBで行う
- 各ENIに対してPrivate Linkを張って異なるVPCにルーティング。その後、NLBからEC2にルーティング
というのを実装してみた。
アーキテクチャー図
結果
/vpc2
というパスにすると、VPC2の1か2のインスタンスに振り分けられる
/vpc3
というパスにすると、VPC3の1か2のインスタンスに振り分けられる
ポイント:セキュリティグループの設定
リソースをデプロイすることはそこまで難しくないが、「デプロイしたはいいが、インスタンスに繋がらない」となるだろう。結構セキュリティグループを指定する箇所が多い。
- ALBのセキュリティグループ(VPC1)
- VPCエンドポイントのセキュリティグループ(VPC1)
- EC2のセキュリティグループ(VPC2,3)
NLBはセキュリティグループつけられないので無視して構わない。
ALBのセキュリティグループ(VPC1)
今までと同じで普通にTCP80を許可する設定で良い。今回は自分のIPに絞っている
resource "aws_security_group" "alb_sg" {
name = "alb-security-group"
description = "Security group for ALB"
vpc_id = module.vpc_1.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.my_ip] # 特定のIPアドレスからのみアクセスを許可
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "alb-sg"
}
}
VPCエンドポイントのセキュリティグループ(VPC1)
ALBのセキュリティグループからのインバウンドを許可する。ポートやプロトコルは何でも良さそう。「ALBからのヘルスチェックが走ってどのポートにくんだっけ?」となって全許可にしてみたが、ここを絞ってもいいと思う。
resource "aws_security_group" "vpc_endpoint_sg" {
name = "vpc-endpoint-security-group"
description = "Security group for VPC endpoints in VPC1"
vpc_id = module.vpc_1.vpc_id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
security_groups = [aws_security_group.alb_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "vpc-endpoint-sg"
}
}
EC2のセキュリティグループ(VPC2,3)
WebサーバーのEC2のある各自のVPCのCIDRからの通信を許可する。HTTPなのでTCP80に絞っている。ここがNLBやPrivate Linkの癖の強いところで、VPC1からの通信の許可ではない。
resource "aws_security_group" "web_sg_vpc2" {
name = "web-security-group-vpc2"
description = "Security group for web servers in VPC2"
vpc_id = module.vpc_2.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [module.vpc_2.vpc_cidr_block] # VPC2のCIDRブロックを使って許可
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-sg-vpc2"
}
}
ポイント:ALBの設定
VPC1のALBのリスナールールとターゲットグループを見てみる。VPC2を見てみよう。
# ALBターゲットグループ(VPC2とVPC3へのプライベートリンク用)
resource "aws_lb_target_group" "vpc2_tg" {
name = "vpc2-target-group"
port = 80
protocol = "HTTP"
vpc_id = module.vpc_1.vpc_id
target_type = "ip"
health_check {
enabled = true
interval = 30
path = "/"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 5
protocol = "HTTP"
matcher = "200"
}
tags = {
Name = "vpc2-target-group"
}
}
# ALBリスナーとルール設定
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "Default route"
status_code = "200"
}
}
}
resource "aws_lb_listener_rule" "vpc2_rule" {
listener_arn = aws_lb_listener.http.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.vpc2_tg.arn
}
condition {
path_pattern {
values = ["/vpc2/*"]
}
}
}
ローカルIPに対してヘルスチェックを行い、パスは/
、ポートはtraffic_port
にする。これだけではVPCエンドポイント(ENI)がALBにアタッチされていないので、次のようにしてアタッチする
# VPC2のENIからプライベートIPを取得
data "aws_network_interface" "nlb_endpoint_enis_vpc2" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[0].network_interface_ids))
id = tolist(aws_vpc_endpoint.nlb_endpoint[0].network_interface_ids)[count.index]
}
# ALBターゲットグループにVPCエンドポイントのプライベートIPを登録
resource "aws_lb_target_group_attachment" "vpc_endpoint_attachment_vpc2" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[0].network_interface_ids))
target_group_arn = aws_lb_target_group.vpc2_tg.arn
target_id = data.aws_network_interface.nlb_endpoint_enis_vpc2[count.index].private_ip
port = 80
}
アタッチの順序
以下のコードは初回のterraform apply
では失敗してしまう。VPCエンドポイントのIPはapplyしないとわかないから、設定しようがないというものだ。ローカルIPを指定してVPCエンドポイントをデプロイできればこんな手間はないかもしれないが、このへんのいい方法はわからなかった。
そのため、今回はここを初回はコメントアウトして、applyが終わったら戻してapplyした。Terraformのdynamic
とdepends_on
でもできそうな気がする…?
# VPC2のENIからプライベートIPを取得
data "aws_network_interface" "nlb_endpoint_enis_vpc2" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[0].network_interface_ids))
id = tolist(aws_vpc_endpoint.nlb_endpoint[0].network_interface_ids)[count.index]
}
# VPC3のENIからプライベートIPを取得
data "aws_network_interface" "nlb_endpoint_enis_vpc3" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[1].network_interface_ids))
id = tolist(aws_vpc_endpoint.nlb_endpoint[1].network_interface_ids)[count.index]
}
# ALBターゲットグループにVPCエンドポイントのプライベートIPを登録
resource "aws_lb_target_group_attachment" "vpc_endpoint_attachment_vpc2" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[0].network_interface_ids))
target_group_arn = aws_lb_target_group.vpc2_tg.arn
target_id = data.aws_network_interface.nlb_endpoint_enis_vpc2[count.index].private_ip
port = 80
}
resource "aws_lb_target_group_attachment" "vpc_endpoint_attachment_vpc3" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[1].network_interface_ids))
target_group_arn = aws_lb_target_group.vpc3_tg.arn
target_id = data.aws_network_interface.nlb_endpoint_enis_vpc3[count.index].private_ip
port = 80
}
うまくいったか確認
わかりやすいのはALBのターゲットグループ。このようにVPCエンドポイントのローカルIPに対して、「Healthy」になっている必要がある。つながっていない場合、ここが「Unhealthy」になっていることが多い
同様に、NLBに紐づいているEC2のターゲットグループに対しても「Healthy」になっている必要がある。これは普通にALBのターゲットグループにEC2をアタッチする場合と同じ。
うまくいった場合、Network ManagerのVPC Reachbility Analyzerでも成功になっている。ただ、パスは長め
全体コード
VPCモジュールは以前作成したこちらものを使用している
ec2.tf
# 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インスタンス(VPC2とVPC3に2つずつ)
resource "aws_instance" "web_vpc2" {
count = 2
ami = data.aws_ami.amazon_linux.id # Amazon Linux 2023
instance_type = "t3.micro"
subnet_id = module.vpc_2.private_subnet_ids[count.index % length(module.vpc_2.private_subnet_ids)]
vpc_security_group_ids = [aws_security_group.web_sg_vpc2.id]
user_data = <<-EOF
#!/bin/bash
dnf update -y
dnf install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>This is a web server in VPC2 - Instance ${count.index + 1}</h1>" > /var/www/html/index.html
mkdir -p /var/www/html/vpc2
echo "<h1>VPC2 Path - Instance ${count.index + 1}</h1>" > /var/www/html/vpc2/index.html
EOF
tags = {
Name = "web-vpc2-${count.index + 1}"
}
}
resource "aws_instance" "web_vpc3" {
count = 2
ami = data.aws_ami.amazon_linux.id # Amazon Linux 2023
instance_type = "t3.micro"
subnet_id = module.vpc_3.private_subnet_ids[count.index % length(module.vpc_3.private_subnet_ids)]
vpc_security_group_ids = [aws_security_group.web_sg_vpc3.id]
user_data = <<-EOF
#!/bin/bash
dnf update -y
dnf install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>This is a web server in VPC3 - Instance ${count.index + 1}</h1>" > /var/www/html/index.html
mkdir -p /var/www/html/vpc3
echo "<h1>VPC3 Path - Instance ${count.index + 1}</h1>" > /var/www/html/vpc3/index.html
EOF
tags = {
Name = "web-vpc3-${count.index + 1}"
}
}
# NLBターゲットグループにEC2インスタンスを登録
resource "aws_lb_target_group_attachment" "web_vpc2" {
count = 2
target_group_arn = aws_lb_target_group.web_tg[0].arn
target_id = aws_instance.web_vpc2[count.index].id
port = 80
}
resource "aws_lb_target_group_attachment" "web_vpc3" {
count = 2
target_group_arn = aws_lb_target_group.web_tg[1].arn
target_id = aws_instance.web_vpc3[count.index].id
port = 80
}
load_balancers.tf
# ALB(VPC1)の設定
resource "aws_lb" "alb" {
name = "main-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb_sg.id]
subnets = module.vpc_1.public_subnet_ids
tags = {
Name = "main-alb"
}
}
# ALBターゲットグループ(VPC2とVPC3へのプライベートリンク用)
resource "aws_lb_target_group" "vpc2_tg" {
name = "vpc2-target-group"
port = 80
protocol = "HTTP"
vpc_id = module.vpc_1.vpc_id
target_type = "ip"
health_check {
enabled = true
interval = 30
path = "/"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 5
protocol = "HTTP"
matcher = "200"
}
tags = {
Name = "vpc2-target-group"
}
}
resource "aws_lb_target_group" "vpc3_tg" {
name = "vpc3-target-group"
port = 80
protocol = "HTTP"
vpc_id = module.vpc_1.vpc_id
target_type = "ip"
health_check {
enabled = true
interval = 30
path = "/"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 5
protocol = "HTTP"
matcher = "200"
}
tags = {
Name = "vpc3-target-group"
}
}
# ALBリスナーとルール設定
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "Default route"
status_code = "200"
}
}
}
resource "aws_lb_listener_rule" "vpc2_rule" {
listener_arn = aws_lb_listener.http.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.vpc2_tg.arn
}
condition {
path_pattern {
values = ["/vpc2/*"]
}
}
}
resource "aws_lb_listener_rule" "vpc3_rule" {
listener_arn = aws_lb_listener.http.arn
priority = 200
action {
type = "forward"
target_group_arn = aws_lb_target_group.vpc3_tg.arn
}
condition {
path_pattern {
values = ["/vpc3/*"]
}
}
}
# NLB(VPC2とVPC3)の設定
resource "aws_lb" "nlb" {
count = 2
name = "nlb-vpc-${count.index + 2}"
internal = true
load_balancer_type = "network"
subnets = count.index == 0 ? module.vpc_2.private_subnet_ids : module.vpc_3.private_subnet_ids
enable_cross_zone_load_balancing = true
tags = {
Name = "nlb-vpc-${count.index + 2}"
}
}
# NLBターゲットグループ
resource "aws_lb_target_group" "web_tg" {
count = 2
name = "web-tg-vpc-${count.index + 2}"
port = 80
protocol = "TCP"
vpc_id = count.index == 0 ? module.vpc_2.vpc_id : module.vpc_3.vpc_id
target_type = "instance"
health_check {
protocol = "TCP"
port = 80
healthy_threshold = 3
unhealthy_threshold = 3
interval = 30
}
tags = {
Name = "web-tg-vpc-${count.index + 2}"
}
}
# NLBリスナー
resource "aws_lb_listener" "nlb_listener" {
count = 2
load_balancer_arn = aws_lb.nlb[count.index].arn
port = "80"
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web_tg[count.index].arn
}
}
privatelink.tf
# VPC Endpointサービス(PrivateLink)
resource "aws_vpc_endpoint_service" "nlb_endpoint_service" {
count = 2
acceptance_required = false
network_load_balancer_arns = [aws_lb.nlb[count.index].arn]
tags = {
Name = "nlb-endpoint-service-vpc${count.index + 2}"
}
}
# VPC1からVPC2/VPC3のNLBへのVPCエンドポイント
resource "aws_vpc_endpoint" "nlb_endpoint" {
count = 2
vpc_id = module.vpc_1.vpc_id
service_name = aws_vpc_endpoint_service.nlb_endpoint_service[count.index].service_name
vpc_endpoint_type = "Interface"
subnet_ids = module.vpc_1.private_subnet_ids
security_group_ids = [aws_security_group.vpc_endpoint_sg.id]
private_dns_enabled = false
tags = {
Name = "nlb-endpoint-to-vpc${count.index + 2}"
}
}
# VPC2のENIからプライベートIPを取得
data "aws_network_interface" "nlb_endpoint_enis_vpc2" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[0].network_interface_ids))
id = tolist(aws_vpc_endpoint.nlb_endpoint[0].network_interface_ids)[count.index]
}
# VPC3のENIからプライベートIPを取得
data "aws_network_interface" "nlb_endpoint_enis_vpc3" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[1].network_interface_ids))
id = tolist(aws_vpc_endpoint.nlb_endpoint[1].network_interface_ids)[count.index]
}
# ALBターゲットグループにVPCエンドポイントのプライベートIPを登録
resource "aws_lb_target_group_attachment" "vpc_endpoint_attachment_vpc2" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[0].network_interface_ids))
target_group_arn = aws_lb_target_group.vpc2_tg.arn
target_id = data.aws_network_interface.nlb_endpoint_enis_vpc2[count.index].private_ip
port = 80
}
resource "aws_lb_target_group_attachment" "vpc_endpoint_attachment_vpc3" {
count = length(tolist(aws_vpc_endpoint.nlb_endpoint[1].network_interface_ids))
target_group_arn = aws_lb_target_group.vpc3_tg.arn
target_id = data.aws_network_interface.nlb_endpoint_enis_vpc3[count.index].private_ip
port = 80
}
outputs.tf
# 出力
output "alb_dns_name" {
description = "The DNS name of the ALB"
value = aws_lb.alb.dns_name
}
output "vpc1_id" {
description = "VPC1 ID"
value = module.vpc_1.vpc_id
}
output "vpc2_id" {
description = "VPC2 ID"
value = module.vpc_2.vpc_id
}
output "vpc3_id" {
description = "VPC3 ID"
value = module.vpc_3.vpc_id
}
output "nlb1_dns_name" {
description = "The DNS name of the NLB in VPC2"
value = aws_lb.nlb[0].dns_name
}
output "nlb2_dns_name" {
description = "The DNS name of the NLB in VPC3"
value = aws_lb.nlb[1].dns_name
}
output "vpc_endpoint_service1_name" {
description = "VPC Endpoint Service Name for VPC2"
value = aws_vpc_endpoint_service.nlb_endpoint_service[0].service_name
}
output "vpc_endpoint_service2_name" {
description = "VPC Endpoint Service Name for VPC3"
value = aws_vpc_endpoint_service.nlb_endpoint_service[1].service_name
}
security_groups.tf
# セキュリティグループ
resource "aws_security_group" "alb_sg" {
name = "alb-security-group"
description = "Security group for ALB"
vpc_id = module.vpc_1.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.my_ip] # 特定のIPアドレスからのみアクセスを許可
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "alb-sg"
}
}
resource "aws_security_group" "web_sg_vpc2" {
name = "web-security-group-vpc2"
description = "Security group for web servers in VPC2"
vpc_id = module.vpc_2.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [module.vpc_2.vpc_cidr_block] # VPC2のCIDRブロックを使って許可
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-sg-vpc2"
}
}
resource "aws_security_group" "web_sg_vpc3" {
name = "web-security-group-vpc3"
description = "Security group for web servers in VPC3"
vpc_id = module.vpc_3.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [module.vpc_3.vpc_cidr_block] # VPC3のCIDRブロックを使って許可
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-sg-vpc3"
}
}
resource "aws_security_group" "vpc_endpoint_sg" {
name = "vpc-endpoint-security-group"
description = "Security group for VPC endpoints in VPC1"
vpc_id = module.vpc_1.vpc_id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
security_groups = [aws_security_group.alb_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "vpc-endpoint-sg"
}
}
vpc.tf
# 3つのVPCを作成
module "vpc_1" {
source = "./modules/vpc"
vpc_name = "vpc-1-alb"
vpc_cidr_block = "10.1.0.0/16"
availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
}
module "vpc_2" {
source = "./modules/vpc"
vpc_name = "vpc-2-nlb"
vpc_cidr_block = "10.2.0.0/16"
availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
}
module "vpc_3" {
source = "./modules/vpc"
vpc_name = "vpc-3-nlb"
vpc_cidr_block = "10.3.0.0/16"
availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
}
所感
- 大規模になったらこういうことするんだろうね。ドメインでルーティングするかは悩ましいが、ALBでログ類一元化できるのは良さそう
- 今回初めてCline使ってみたが、思ったよりClaude3.7がアホで、口調をずんだもんに変えないとヘイト貯まる度が高かった。これにo3-mini-highをあわせてデバッグしていたが特にセキュリティグループの部分で1~2時間ハマって、自分のブログ見たら一瞬で解決したという芸があったので、まだまだ人工無能だな感はある。あとClaudeの問題なのかもしれないけど、セキュリティグループを0.0.0.0にした結構危なっかしいコードを書くので結構調教がいる
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー