Claude Media
Claude CodeでNAT Gatewayの料金が高い原因を探る — フローログとAthena

Claude CodeでNAT Gatewayの料金が高い原因を探る — フローログとAthena

NAT Gatewayのデータ処理料金を、VPCフローログとAthenaで宛先別に分け、S3やECR向けをVPCエンドポイントへ振り替えるまでの手順です。集計と差分作りをClaude Codeに任せる権限設定も示します。

NAT Gatewayの請求は、稼働時間ではなく処理したデータ量で膨らむことがあります。Cost Explorerで処理量の課金が大きいと分かったら、次に要るのは「どこ宛ての通信か」です。VPCフローログをAthenaで宛先別に集計すると、S3やECR向けのようにVPCエンドポイントで外せる通信がどれだけあるかが見えます。

この記事では、フローログの取得から集計、エンドポイントへの振り替えまでを順に進めます。集計クエリとTerraformの差分づくりはClaude Codeに任せ、実行権限は読み取りと plan までに絞ります。

NAT Gatewayの料金は「時間」と「処理量」の二本立て

NAT Gatewayは、稼働している時間と、処理したデータ量の両方に課金されます。処理量が主因のときは、通る通信の中身を変えない限り料金は下がりません。

AWSのドキュメントは、処理量の課金を抑える方法を2つ挙げています。

対策

処理量の課金を抑える2つの方法

  • NAT Gatewayと同じAZに置く

    リソースがAZをまたいでNAT Gatewayを使うと、AZ間のデータ転送料金が加わります。リソースと同じAZのNAT Gatewayを使うか、AZごとにNAT Gatewayを作ります。

  • エンドポイントに振り替える

    通信の大半がインターフェースエンドポイントかゲートウェイエンドポイントに対応するAWSサービス宛てなら、そのエンドポイントを作ります。

後者を狙うには、通信の宛先の内訳が要ります。Cost Explorerには宛先が載らないので、ここからフローログに進みます。請求側の切り分けは、ClaudeでAWSのコスト急増を調べるが先に扱っています。

まずCost Explorerで処理量の課金かを確かめる

フローログを仕込む前に、NAT Gatewayの課金が処理量側かを確認します。サービスで絞らずに使用タイプ別に集計して、名前に NatGateway を含む行の金額を見ます。課金が載るサービス名は環境で異なる場合があるため、フィルタは付けません。

aws ce get-cost-and-usage \
  --time-period Start=2026-09-01,End=2026-10-01 \
  --granularity MONTHLY \
  --metrics UnblendedCost UsageQuantity \
  --group-by Type=DIMENSION,Key=USAGE_TYPE

出力から NatGateway を含む使用タイプだけを見ます。時間の行より処理量の行が大きければ、宛先の分解に進みます。時間の行が大きいなら、NAT Gatewayの台数や稼働期間のほうを疑います。

NAT Gatewayのネットワークインターフェースにフローログを付ける

フローログは、NAT Gatewayのネットワークインターフェースに付けます。インスタンス側に付けると、NAT Gatewayを通った後の最終宛先が記録の差として残りません。

AWSの記録例では、ゾーン型のNAT Gatewayのインターフェースに付けたログで、dstaddr にNAT GatewayのプライベートIP、pkt-dstaddr に最終宛先のIPが出ます。この2つが食い違う行が、「VPC内からNAT Gatewayへ入る通信」です。戻りの通信では、srcaddr と pkt-srcaddr が食い違います。

出力先はS3にします。カスタム形式は、後で作るAthenaのテーブル列と同じ順に並べます。

variable "nat_gateway_eni_id" {
  type = string # NAT Gatewayのネットワークインターフェース
}
 
resource "aws_flow_log" "nat" {
  eni_id                   = var.nat_gateway_eni_id
  traffic_type             = "ALL"
  log_destination_type     = "s3"
  log_destination          = aws_s3_bucket.flow_logs.arn
  max_aggregation_interval = 600
 
  log_format = join(" ", [
    "$${version}", "$${account-id}", "$${interface-id}", "$${srcaddr}",
    "$${dstaddr}", "$${srcport}", "$${dstport}", "$${protocol}",
    "$${packets}", "$${bytes}", "$${start}", "$${end}", "$${action}",
    "$${log-status}", "$${vpc-id}", "$${subnet-id}", "$${instance-id}",
    "$${tcp-flags}", "$${type}", "$${pkt-srcaddr}", "$${pkt-dstaddr}",
    "$${az-id}", "$${sublocation-type}", "$${sublocation-id}",
    "$${pkt-src-aws-service}", "$${pkt-dst-aws-service}",
    "$${flow-direction}", "$${traffic-path}",
  ])
}

pkt-src-aws-service と pkt-dst-aws-service は、通信相手のIPがAWSサービスのIP範囲に当たるとき、そのサービス名を記録する項目です。値には S3 や DYNAMODB、EC2 などがあります。ECR専用の値は一覧にありません。ECR向けの通信は、後述のとおり別の見方で拾います。

この記事のログ形式と集計は、ゾーン型のNAT Gateway向けです。リージョナルNAT Gatewayでは interface-id が - になり、srcaddr と pkt-srcaddr、dstaddr と pkt-dstaddr も一致します。下の集計クエリの条件では拾えないので、リージョナル型は対象外として読んでください。

落とし穴

ログは作成後の通信しか残らない

フローログは、作成した時点から先の通信を記録します。過去の分は遡れないので、集計に足る日数が溜まるまで待ちます。ピークが週次や月次の処理を含むなら、その周期を一巡する日数が目安です。

Athenaで宛先別のバイト数を出す

S3に溜まったログを、パーティション射影つきのテーブルで読みます。列の並びは上のTerraformの log_format と一致させます。ずれると、バイト数の列に別の値が入って集計が崩れます。

CREATE EXTERNAL TABLE IF NOT EXISTS vpc_flow_logs_nat (
  version int, account_id string, interface_id string,
  srcaddr string, dstaddr string, srcport int, dstport int,
  protocol bigint, packets bigint, bytes bigint,
  start bigint, `end` bigint, action string, log_status string,
  vpc_id string, subnet_id string, instance_id string,
  tcp_flags int, type string, pkt_srcaddr string, pkt_dstaddr string,
  az_id string, sublocation_type string, sublocation_id string,
  pkt_src_aws_service string, pkt_dst_aws_service string,
  flow_direction string, traffic_path int
)
PARTITIONED BY (accid string, region string, day string)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ' '
LOCATION 's3://<バケット名>/AWSLogs/'
TBLPROPERTIES (
  "skip.header.line.count"="1",
  "projection.enabled"="true",
  "projection.accid.type"="enum",
  "projection.accid.values"="<アカウントID>",
  "projection.region.type"="enum",
  "projection.region.values"="ap-northeast-1",
  "projection.day.type"="date",
  "projection.day.range"="2026/10/01,NOW",
  "projection.day.format"="yyyy/MM/dd",
  "storage.location.template"="s3://<バケット名>/AWSLogs/${accid}/vpcflowlogs/${region}/${day}"
);

テーブルができたら、ゾーン型のNAT Gatewayを通った通信を宛先のサービス別に足します。行きの通信は dstaddr と pkt_dstaddr が食い違う行、戻りの通信は srcaddr と pkt_srcaddr が食い違う行です。S3やECRからの大きなダウンロードは戻り側に乗るので、両方を数えます。

WITH legs AS (
  SELECT pkt_dst_aws_service AS service, bytes
  FROM vpc_flow_logs_nat
  WHERE day BETWEEN '2026/10/01' AND '2026/10/07'
    AND dstaddr <> pkt_dstaddr
  UNION ALL
  SELECT pkt_src_aws_service AS service, bytes
  FROM vpc_flow_logs_nat
  WHERE day BETWEEN '2026/10/01' AND '2026/10/07'
    AND srcaddr <> pkt_srcaddr
)
SELECT coalesce(nullif(service, '-'), '(サービス名なし)') AS service,
       round(sum(bytes) / 1e9, 2) AS gb
FROM legs
GROUP BY 1
ORDER BY gb DESC
LIMIT 20;

S3 や DYNAMODB の行がGB単位で上位に来れば、ゲートウェイエンドポイントだけで処理量を落とせる見込みがあります。(サービス名なし) が上位を占めるなら、同じ形で pkt_dstaddr ごとに集計し直し、アドレスを引き当てます。ECRやCloudWatch Logsには専用のサービス名がないため、こちらに含まれる可能性があります。

Cost Explorerの金額とGB数が合わないとき

フローログの bytes は、記録された行のバイト数の合計です。NAT Gatewayが課金の対象とする処理量と同じ数え方かどうかは、このクエリだけでは確かめられません。金額の按分に使うのではなく、宛先の順位と比率を見るために使います。

宛先のサービスごとにエンドポイントを選ぶ

集計の結果は、次の表で振り分けます。

宛先作るもの追加料金
S3作るものゲートウェイエンドポイント追加料金なし
DynamoDB作るものゲートウェイエンドポイント追加料金なし
ECR(イメージの取得)作るものインターフェースエンドポイント2つ + S3のゲートウェイエンドポイント追加料金インターフェース側にあり
CloudWatch Logs作るものインターフェースエンドポイント追加料金あり
その他のAWSサービス作るもの対応していればインターフェースエンドポイント追加料金あり

ゲートウェイエンドポイントはS3とDynamoDBの専用で、追加料金はかかりません。S3とDynamoDBは、インターフェースエンドポイントにも対応しています。VPC内からの通信なら、無料のゲートウェイエンドポイントから試す順になります。

ECRは組み合わせが要ります。イメージを取得するときは、ECRにイメージのマニフェストを問い合わせたうえで、レイヤーをS3から取ります。そのため com.amazonaws.<リージョン>.ecr.api と ecr.dkr の2つのインターフェースエンドポイントに加えて、S3のゲートウェイエンドポイントも必要です。ECRを使うFargateのタスクでログをCloudWatch Logsへ送る構成では、logs のインターフェースエンドポイントも加わります。

インターフェースエンドポイントは追加料金がかかります。NAT Gatewayの処理料金より安くなるかは、集計したGB数とPrivateLinkの料金表から見積もります。小さな通信量のために作ると、かえって高くなることがあります。

エンドポイントのTerraform差分を作る

S3のゲートウェイエンドポイントは、ルートテーブルに関連付けて作ります。作成すると、選んだルートテーブルにプレフィックスリスト宛てのルートが自動で追加されます。

resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.ap-northeast-1.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [aws_route_table.private.id]
}
 
resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.ap-northeast-1.ecr.api"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = aws_subnet.private[*].id
  security_group_ids  = [aws_security_group.endpoints.id]
  private_dns_enabled = true
}

ecr.dkr も ecr_api と同じ形で、service_name だけを変えます。

エンドポイントを作る前後に押さえる点は次のとおりです。

  • ゲートウェイエンドポイントは、作成したリージョンのS3にだけ使えます。別リージョンのバケット向けの通信は、NAT Gatewayを通り続けます
  • 同じルートテーブルを使うサブネットのインスタンスは、自動でエンドポイントを使います。関連付けていないルートテーブルのサブネットは、従来の経路のままです
  • 0.0.0.0/0のルートがあっても、S3宛てはより具体的なエンドポイントのルートが優先されます
  • 既存の接続は、ゲートウェイエンドポイントを足したときに一時的に切れることがあります

最後の点は、ECSのタスクで接続が切れることがある挙動です。定常運用のサービスへ入れるときは、時間帯を選びます。

Claude Codeに任せる範囲と権限

集計クエリの作成と、Terraformの差分づくりは、Claude Codeに任せられます。ただしAthenaの実行には費用がかかり、terraform apply はネットワークの経路を変えます。許可する操作は、読み取りと plan までに絞ります。

非対話モードの claude -p では、--allowedTools に許可する操作を書きます。Bash(git diff *) のような前方一致の書式で、末尾の * の前に空白が要ります。

claude --bare -p "NAT Gatewayのフローログを集計し、宛先上位を表にして。
Terraform差分はplanまで" \
  --allowedTools "Read,Edit,Bash(aws athena start-query-execution *),Bash(aws athena get-query-execution *),Bash(aws athena get-query-results *),Bash(terraform plan *)" \
  --permission-prompts none \
  --max-budget-usd 2

--bare を付けると、実行ホストのフックやCLAUDE.mdを読み込まずに起動します。そのぶん、テーブル名やワークグループは依頼文に書きます。--permission-prompts none は、許可が必要な操作を尋ねずに拒否します。無人の実行で止まらないための指定です。

常設するなら、プロジェクトの設定ファイルに書く方法もあります。

{
  "permissions": {
    "allow": [
      "Bash(aws athena start-query-execution *)",
      "Bash(aws athena get-query-execution *)",
      "Bash(aws athena get-query-results *)",
      "Bash(terraform plan *)"
    ],
    "deny": [
      "Bash(terraform apply *)",
      "Bash(terraform destroy *)"
    ]
  }
}

Athenaの権限設計とクエリの任せ方は、Claude CodeでAthenaのALBログを集計する記事と共通です。差分をレビューする段では、未使用のリソースを describe 系だけで洗い出すTerraformの手順も参考になります。

振り替えが効いたかを確かめる

エンドポイントを入れたら、同じ集計をやり直します。確認先は3つあります。

  1. NAT Gatewayのログで、S3 向けの行が減っているか
  2. エンドポイントを使う側のインスタンスのログで、traffic_path が 7(ゲートウェイエンドポイント経由)になっているか
  3. 翌日以降のCost Explorerで、処理量の行が下がっているか

traffic_path は、外向き通信の経路を表す項目です。値の 7 はゲートウェイVPCエンドポイント経由を指します。インスタンス側のフローログを一時的に取れば、振り替えが成立したかを直接見られます。

うまく減らないときの切り分け

  • S3向けが残る: そのサブネットのルートテーブルが、エンドポイントに関連付いていない
  • 別リージョンのバケットだった: ゲートウェイエンドポイントは同一リージョンのS3にだけ効く
  • ECRだけ残る: ecr.api、ecr.dkr、S3のゲートウェイエンドポイントのどれかが欠けている
  • 集計列がずれる: フローログの log_format とテーブルの列の並びが一致していない

まとめ

NAT Gatewayの処理料金は、Cost Explorerで「処理量が大きい」と分かった後、フローログで宛先を分けて初めて対策の的が定まります。S3とDynamoDB向けはゲートウェイエンドポイントで追加料金なしに外せます。ECRやCloudWatch Logs向けは、インターフェースエンドポイントの料金と見比べてから決めます。

Claude Codeに渡すのは、集計クエリの作成、結果の表づくり、plan までの差分です。apply は人が差分を読んでから実行する、という線引きにしておくと、通信経路の変更が意図せず走る心配がありません。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-pricing.html
  2. docs.aws.amazon.com/vpc/latest/userguide/flow-log-records.html
  3. docs.aws.amazon.com/vpc/latest/userguide/flow-logs-records-examples.html
  4. docs.aws.amazon.com/athena/latest/ug/vpc-flow-logs-partition-projection.html
  5. docs.aws.amazon.com/vpc/latest/privatelink/vpce-gateway.html
  6. docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html
  7. docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html
  8. code.claude.com/docs/en/headless
この記事を共有:XはてブLinkedIn