LambdaをVPC内に置いてRDSに接続する構成を作ると、インターネットへの通信にNATが必要になります。AWSのマネージドサービス「NAT Gateway」を使うと月額約$45(東京リージョン、固定費のみ)かかります。
個人開発でこれは痛い。EC2 t3.nanoで代替すると月約$4になります。本記事ではTerraformのコードを含めて手順を説明します。
なぜVPC内LambdaにNATが必要か
LambdaをVPC内のプライベートサブネットに配置すると、デフォルトではインターネットに出られません。GitHub APIやAnthropic APIなど外部サービスへのアクセスがすべて失敗します。
インターネットへの経路を作るにはNATが必要で、選択肢は2つです。
| NAT Gateway | EC2 NATインスタンス | |
|---|---|---|
| 月額 | 約$45〜 | 約$4(t3.nano) |
| 可用性 | 高(AWS管理) | 低(単一EC2) |
| 設定 | 簡単 | 要セットアップ |
| 帯域 | 自動スケール | インスタンス依存 |
個人開発・小規模サービスなら可用性よりコストを取るのが現実的です。
構成
[Lambda(プライベートサブネット)]
↓
[EC2 NATインスタンス(パブリックサブネット)]
↓
[Internet Gateway]
↓
[インターネット]
EC2インスタンスをNATルーターとして機能させ、プライベートサブネットのルートテーブルでデフォルトゲートウェイをそのEC2に向けます。
Terraformの実装
1. VPC・サブネット
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
}
# パブリックサブネット(NATインスタンスを配置)
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = ["ap-northeast-1a", "ap-northeast-1c"][count.index]
map_public_ip_on_launch = true
}
# プライベートサブネット(LambdaとRDSを配置)
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 11}.0/24"
availability_zone = ["ap-northeast-1a", "ap-northeast-1c"][count.index]
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
# パブリックサブネット用ルートテーブル
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
}
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
2. NATインスタンス用セキュリティグループ
Lambdaからの通信だけ受け付け、外向きは全許可します。
resource "aws_security_group" "nat" {
name = "${var.project}-${var.env}-nat"
vpc_id = aws_vpc.main.id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
security_groups = [aws_security_group.lambda.id] # Lambdaのみ
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
3. NATインスタンス
source_dest_check = falseが必須です。これを設定しないとNATとして機能しません(デフォルトでは送信元・送信先が自分でないパケットを破棄します)。
resource "aws_instance" "nat" {
ami = var.nat_ami_id # Amazon Linux 2023のAMI
instance_type = "t3.nano"
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [aws_security_group.nat.id]
associate_public_ip_address = true
source_dest_check = false # ← これが必須
iam_instance_profile = aws_iam_instance_profile.nat.name
user_data = file("${path.module}/nat_user_data.sh")
}
4. プライベートサブネットのルートテーブル
デフォルトゲートウェイをNATインスタンスのネットワークインターフェースに向けます。
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
network_interface_id = aws_instance.nat.primary_network_interface_id
}
}
resource "aws_route_table_association" "private" {
count = 2
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
5. user_data(NATの設定スクリプト)
EC2起動時にIPフォワーディングを有効化し、NATの設定を入れます。Amazon Linux 2023はnftablesを使います(iptablesではありません)。
#!/bin/bash
set -e
# IPフォワーディングを有効化
echo "net.ipv4.ip_forward = 1" > /etc/sysctl.d/99-ip-forward.conf
sysctl -p /etc/sysctl.d/99-ip-forward.conf
# nftablesをインストール
dnf install -y nftables
# NATルールを設定
# AL2023のnftables.serviceは /etc/sysconfig/nftables.conf を読む
cat > /etc/sysconfig/nftables.conf << 'NFTEOF'
#!/usr/sbin/nft -f
flush ruleset
table ip nat {
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
oifname != "lo" masquerade
}
}
NFTEOF
# nftablesサービスを有効化・起動
systemctl enable --now nftables
masqueradeはIPマスカレードで、プライベートIPアドレスをEC2のパブリックIPに変換して外に出します。
6. IAMロール(SSM Session Manager用)
SSM Session Managerを使うとSSHなしでEC2にアクセスできます。NATインスタンスにSSHポートを開けずに済むので、セキュリティグループをシンプルに保てます。
resource "aws_iam_role" "nat_instance" {
name = "${var.project}-${var.env}-nat"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "ec2.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy_attachment" "nat_ssm" {
role = aws_iam_role.nat_instance.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "nat" {
name = "${var.project}-${var.env}-nat"
role = aws_iam_role.nat_instance.name
}
AMI IDの取得方法
var.nat_ami_idに入れるAMI IDはAWSコンソールかCLIで取得します。
# Amazon Linux 2023の最新AMI(東京リージョン)
aws ec2 describe-images \
--owners amazon \
--filters "Name=name,Values=al2023-ami-2023.*-x86_64" \
--query "sort_by(Images, &CreationDate)[-1].ImageId" \
--output text \
--region ap-northeast-1
AMI IDは定期的に更新されますが、Terraformで自動追従させると意図しないEC2の再作成が起きるため、固定値をvariables.tfに書いておく方が安全です。
注意点
単一障害点になる
NATインスタンスが1台なので、EC2が落ちるとLambdaからの外部通信がすべて止まります。個人開発・小規模サービスなら許容範囲ですが、止まったときはAWSコンソールかTerraformでEC2を再起動してください。
EC2の再作成でENIが変わる
terraform applyでEC2が再作成されるとネットワークインターフェースIDが変わり、プライベートルートテーブルの参照が壊れます。AMI IDを固定しておけば基本的には起きませんが、インスタンスタイプを変更するときは注意が必要です。
コスト比較
| NAT Gateway | t3.nano | |
|---|---|---|
| 固定費 | $0.062/時 ≒ $45/月 | $0.0058/時 ≒ $4/月 |
| データ転送 | $0.062/GB | なし |
| 合計(転送少量) | 約$45〜/月 | 約$4/月 |
差額は約$40/月。個人開発なら年間約$480の節約になります。
可用性が求められるプロダクションサービスにはNAT Gatewayを使うべきですが、個人開発や検証環境では十分な代替手段です。