個人でRepoCartaというSaaSを作っていて、LambdaをVPC内に置いてRDSに接続する構成にした。インフラを整えた後に最初のAWS請求書を見たら、NAT Gatewayだけで月$45取られていた。個人開発でこれは痛い。
EC2 t3.nanoに自前でNATを立てたら月約$4になった。TerraformのコードとAmazon Linux 2023でのセットアップ手順を残しておく。
なぜNATが要るのか
LambdaをプライベートサブネットVPC内に置くと、デフォルトではインターネットへ出られない。GitHub APIやAnthropic APIなど外部へのリクエストがすべて失敗するので、NATを通して外に出る経路を作る必要がある。
選択肢はNAT GatewayかEC2か。
| NAT Gateway | EC2 t3.nano | |
|---|---|---|
| 月額 | 約$45〜 | 約$4 |
| 可用性 | 高(AWS管理) | EC2が落ちたら止まる |
| 設定 | 簡単 | 少し手間がかかる |
個人開発なら可用性より$40/月の差額の方が大事だった。
Terraformの構成
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
}
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]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
NATインスタンス本体
source_dest_check = falseが絶対に必要。デフォルトではEC2は自分宛て以外のパケットを捨てるので、これをOFFにしないとNATとして機能しない。最初これを知らなくて詰まった。
resource "aws_instance" "nat" {
ami = var.nat_ami_id
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 # ← これがないとNATにならない
iam_instance_profile = aws_iam_instance_profile.nat.name
user_data = file("${path.module}/nat_user_data.sh")
}
プライベートサブネットのルートテーブル
デフォルトルートをNATインスタンスのENIに向ける。
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
}
user_data(NATのセットアップスクリプト)
Amazon Linux 2023はiptablesではなく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
dnf install -y nftables
# 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
systemctl enable --now nftables
masqueradeでプライベートIPをEC2のパブリックIPに変換して外に出す。
IAMロール(SSM用)
SSMのSession Managerを使えばSSHポートを開けずにEC2に入れるので、セキュリティグループをシンプルに保てる。
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の取得
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に固定値で書いておく方が安心。
注意点
EC2が止まったら外に出られなくなる。 1台構成なので単一障害点になる。個人開発なら許容範囲だと思っているが、止まったときはコンソールかTerraformで再起動する。
EC2が再作成されるとENIが変わる。 terraform applyでEC2が再作成されるとENIのIDが変わり、ルートテーブルの参照が壊れる。インスタンスタイプを変えるときは要注意。
差額は月$40、年間$480。個人開発の段階では大きい。プロダクションで可用性が必要になったらNAT Gatewayに戻せばいい。