TerraformでVPCピアリングを実装してみた

最近、特定のタスクを達成するために、VPC1内のプライベートサブネットに設置したLambdaアプリケーションを、同一VPC内のパブリックサブネットに存在するNAT Gatewayを介して、VPC2内のパブリックサブネットにあるALB(接続先はプライベートサブネットのECS)のパブリックURLを利用するように設定しました。

しかし、ALBへのトラフィックはLambdaからのものだけで、以下の非機能要求を満たすために、VPCピアリングを介したプライベートな接続に変更することにしました。

- コスト: NAT Gatewayを利用するよりもデータ転送量が少なくて済み、コスト削減につながる。
- パフォーマンス: VPC間のネットワーク遅延を最小化でき、大量のデータ転送や低レイテンシを要求するアプリケーションに有効。
- セキュリティ: 通信がAWS内のプライベートなネットワーク接続で行われるため、通信がインターネットに出るリスクを排除できる。

この記事では、これらの要求を満たすためにTerraformを用いてVPCピアリングを実装する方法を記載します。

# 実装
今回は以下のようなディレクトリ構成を採用しています。
各環境（dev, stg, prod)に対応するディレクトリと、再利用可能なモジュール(route_table, security_group, vpc_peering)を用意しています。

```lua
-- terraform-project/
   -- environments/
      -- dev/
         -- backend.tf
         -- main.tf
      -- stg/
         -- backend.tf
         -- main.tf
      -- prod/
         -- backend.tf
         -- main.tf
   -- modules/
      -- route_table/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README.md
      -- security_group/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README.md
      -- vpc_peering/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README.md
```

## VPCピアリング
VPCピアリングはAWS内の2つのVPC間で直接的なネットワーク接続を作る機能です。
以下の`vpc_peering/main.tf`では、SSMパラメータストアからVPCのIDを取得し、それらを用いてVPCピアリングを設定しています。

```tf:modules/vpc_peering/main.tf
data "aws_ssm_parameter" "lambda_vpc_id" {
  name = "/vpc/id/lambda"
}

data "aws_ssm_parameter" "ecs_vpc_id" {
  name = "/vpc/id/ecs"
}

// 同一アカウント内でのVPCピアリングのため、接続と承認を一度に行う
resource "aws_vpc_peering_connection" "vpc_peering" {
  vpc_id      = data.aws_ssm_parameter.lambda_vpc_id.value
  peer_vpc_id = data.aws_ssm_parameter.ecs_vpc_id.value
  auto_accept = true
}
```
さらに、VPCピアリングのIDを他のモジュールで利用できるように`outputs.tf`で出力しています。

```tf:modules/vpc_peering/outputs.tf
output "vpc_peering_connection_id" {
  description = "Connection Id of VPC Peering"
  value       = aws_vpc_peering_connection.vpc_peering.id
}
```

## ルートテーブル
次に、VPC内のネットワークトラフィックのルートを設定するルートテーブルを作成します。
ここでは、各VPCのCIDRブロックとVPCピアリングのIDを用いて、ピアリング接続を経由するルートを設定しています。
Lambdaが配置されているサブネットのルートテーブルを`lambda_local_nat`、ALBが配置されているサブネットのルートテーブルを`ecs_local`としています。

```tf
data "aws_ssm_parameter" "lambda_vpc_cidr" {
  name = "/vpc/cidr/lambda"
}

data "aws_ssm_parameter" "ecs_vpc_cidr" {
  name = "/vpc/cidr/ecs"
}

resource "aws_route_table" "lambda_local_nat" {
  vpc_id = data.aws_ssm_parameter.lambda_vpc_id.value
  route {
    cidr_block = "0.0.0.0/0"
    nat_gateway_id = data.aws_ssm_parameter.lambda_nat_gw_id.value
  }
  tags = {
    Name = "lambda-vpc-private-local-nat-rtb"
  }
}

// 既存ルートへの影響を考慮して分割
resource "aws_route" "peering_route" {
  route_table_id            = aws_route_table.lambda_local_nat.id
  destination_cidr_block    = data.aws_ssm_parameter.ecs_vpc_cidr.value
  vpc_peering_connection_id = var.vpc_peering_connection_id
}

resource "aws_route_table" "ecs_local" {
  vpc_id = data.aws_ssm_parameter.ecs_vpc_id.value

  tags = {
    Name = "ecs-vpc-private-local-rtb"
  }
}

// 既存ルートへの影響を考慮して分割
resource "aws_route" "ecs_peering_route" {
  route_table_id                = aws_route_table.ecs_local.id
  destination_cidr_block        = data.aws_ssm_parameter.lambda_vpc_cidr.value
  vpc_peering_connection_id     = var.vpc_peering_connection_id
}
```
ルートテーブルモジュールでは、VPCピアリングのIDを入力として受け取るために、`variables.tf`で変数を定義しています。

```tf:modules/route_table/variables.tf
variable "vpc_peering_connection_id" {
  description = "Connection Id of VPC Peering"
  type        = string
}
```

## セキュリティグループ
最後に、ALBのセキュリティグループを設定します。
これにより、特定のIP範囲からの通信だけを許可するというルールを作ることができます。
今回、Lambdaは2つのプライベートサブネットに存在するので、各サブネットのCIDRブロックを指定しています。

```tf:modules/security_group/main.tf
data "aws_ssm_parameter" "lambda_private_subnet_cidr1" {
  name = "/subnet/private/cidr1/lambda"
}

data "aws_ssm_parameter" "lambda_private_subnet_cidr2" {
  name = "/subnet/private/cidr2/lambda"
}

resource "aws_security_group_rule" "elb_sg_ingress_443_lambda" {
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  security_group_id = aws_security_group.elb_sg.id
  cidr_blocks       = [data.aws_ssm_parameter.lambda_private_subnet_cidr1.value, data.aws_ssm_parameter.lambda_private_subnet_cidr2.value]
}
```

## プライベートDNSと証明書の設定
Terraformの実装は以上なのですが(あとは`terraform apply`するだけ)、VPCピアリングはプライベートな接続を扱うため、Lambdaのアプリケーション内でApplication Load Balancer(ALB)の接続先をパブリックURLからプライベートDNSに変更する必要があります。

加えて、HTTPS接続を行うためには、AWS Certificate Manager（ACM)のPrivate Certificate Authority(CA)を用いてプライベート証明書を作成し、その証明書をALBのリスナーに割り当てる作業が必要になります。

以下が設定手順です。

1.Private CAの作成
- AWS Certificate Manager（ACM)のダッシュボードから「Private CAs」を選択し、「Create CA」をクリックします。
- Root CAまたはSubordinate CAのタイプを選択し、CAの詳細を入力します。
- その後、「Next」をクリックし、設定を確認します。
- 問題がなければ、「Confirm and create」をクリックして、Private CAを作成します。

2.プライベート証明書の発行
- Private CAが作成されたら、「Certificate Manager」ダッシュボードに戻り、「Get started」をクリックします。
- 証明書の設定画面で、「Private」を選択し、「Request a certificate」をクリックします。
- プライベートDNS名を含むドメイン名を入力し、詳細を設定します。
- 設定が完了したら、「Confirm and request」をクリックします。

3.ALBのリスナーに証明書を割り当てる
- ALBの設定画面に移動し、対象のリスナーを選択します。
- 「Actions」ドロップダウンから「Change certificate」を選択し、作成したプライベート証明書を選択します。