0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

個人開発でNAT Gatewayは高すぎる。EC2で自前NATにしてAWSコストを月$40節約した

0
Last updated at Posted at 2026-06-03

個人で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に戻せばいい。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?