こしあん
2025-03-02

VPC間通信におけるPrivate Linkとクロスゾーン負荷分散の組み合わせ


14{icon} {views}


クロスゾーン負荷分散が有効な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"
}

所感



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

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

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