こしあん
2025-03-20

ALBとPrivate Linkで複数のVPCにルーティング


74{icon} {views}


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のdynamicdepends_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の中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

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