AWSの小さな課金を、100倍になった時を想像して下げた

上原正吉(EarthLink Network Co., Ltd.)。Claude Codeを開発の主体に据え、20を超えるプロダクトを1人で同時に開発・運用しています。これは、その現場の実測記です。

AWSの小さな課金を、100倍になった時を想像して下げた

AWSの小さな課金を、100倍になった時を想像して下げた

2026 年 6 月、AWS 請求の見直しを始めました。目的は、今月の請求を削ることではありません。いま数ドルの小さな課金でも、利用が 100 倍になれば固定費として重く効きます。将来の額を先に想像し、運用しながら直していく。これはその実測記録です。

最初に分かったのは、一番コストがかかっているのは本体機能ではない、という事実でした。かかっていたのは、CDK(コードで AWS 構成を定義するツール)が自動で立てた NAT Gateway(プライベート網から外部へ出る中継。立てているだけで課金される)、ビルドだけをしている Amplify(AWS のビルド・ホスティング基盤)、15 分ごとに走る稼働確認の Lambda(イベントで起動する関数実行基盤)、そして誰も見ていない dev(開発)環境に溜まった 82 万件のゴミデータでした。どれも今は小さい、あるいは気づきにくい課金です。ですが放置すると、利用や時間に比例して伸びます。だから「100 倍になったらどうなるか」で見て、順に対処しました。

結論を先に置きます。

  • 一番高い固定費は本体機能ではなく、IaC(インフラをコードで定義する手法)のデフォルトが立てた NAT Gateway でしたnatGateways を明示しないと AZ(アベイラビリティゾーン。データセンターの区画)ごとに 2 基立ち、月 $58〜66 かかっていました。「サーバーレスだから固定費ゼロ」は自動では成立しません
  • Amplify 請求の 87〜89% は「ビルド時間」でした — 内訳を割ると、移すべきは runtime(月 $3〜7)ではなくビルドそのもの。git 連携の自動ビルドを止め、1 回作った成果物を昇格させる形へ切り替えました
  • 月 $47 の DynamoDB(AWS の NoSQL データベース)読み取りは、監視 Lambda 自身が生んでいました — 15 分ごとに 1.22GB の索引を全件数え直していました。データが増えるほど比例して伸びる、放置できないコストでした
  • 可視化そのものが 100 倍間違えることもあります — コストダッシュボードが、あるプロバイダの料金を 100 倍に膨らませて表示していました。数字で判断するなら、その数字が正しいかをまず疑う必要があります

私のポートフォリオの請求は、ほぼ 1 つのニュースキュレーション基盤に集中していて、他のプロダクト(静的サイトや小さな SaaS(サービスとして提供されるソフトウェア))は月数ドル以下でした。だからこそ「本体以外」を先に対処するのが効きました。以下、何を測り、どう直したかを残します。

本体以外の定常コストの内訳と before→after。プロダクト名は伏せた再構成、金額は検証済み実測

戦線 1: CDK が勝手に作った NAT Gateway 2 基、月 $64

最初に見つかった大きな固定費は、稼働監視 SaaS のスタックにありました。ブラウザ監視を ECS Fargate(サーバー管理不要のコンテナ実行基盤)で動かす構成で、CDK に VPC(仮想プライベートネットワーク)を明示していなかったため、暗黙 VPC が自動生成され、AZ ごとに NAT Gateway が計 2 基立っていました。これが月 $58〜66。dev 専用ワークロードの Lambda と DynamoDB を全部足した額より、この固定費の方が高いという逆転が起きていました。

IP allowlist(接続元 IP を限定する運用)もしておらず、NAT の固定出口 IP に価値はありませんでした。そこで VPC を明示し、NAT をゼロにして、public subnet(外部から直接到達できるネットワーク区画)からの直接 egress(外向き通信)へ変えました。

// 暗黙 VPC に NAT を勝手に立てさせない。ゼロを明示する
const vpc = new ec2.Vpc(this, "MonitorVpc", {
  natGateways: 0,
  subnetConfiguration: [
    { name: "public", subnetType: ec2.SubnetType.PUBLIC },
  ],
});
// Fargate タスクは public subnet + パブリック IP で外に出る
new ecs.FargateService(this, "BrowserWorker", {
  cluster,
  assignPublicIp: true,
  vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
});

これで月 $58〜66 が消えました。金額は小さく見えますが、NAT は立っているだけで課金される固定費で、AZ やプロダクトを増やすほど基数が増えて比例して伸びます。今 $64 でも、構成が 100 倍に育てば重い固定費になります。だから小さいうちに、明示的にゼロへ倒しました。サーバーレスだから固定費ゼロは、自動では成立しません。IaC のデフォルトが何を立てるかまで見ないと、掲げた設計原則が裏で破られます。

戦線 2: Amplify 請求の 87% は「ビルド時間」だった

次はデプロイ基盤です。CI/CD(変更のたびに自動でテストしデプロイする仕組み)の自作ポータルでコストを可視化し、Amplify の請求を Cost Explorer(AWS の費用内訳を分析するツール)で分解したところ、月次請求の 87〜89% が BuildDuration、つまりビルドに費やした時間でした。実際のランタイム(SSR(サーバー側で HTML を生成する方式)ホスティング)はわずか月 $3〜7。6 月は一時 $60 台に跳ねています。

当初の計画は SSR を別基盤へ移設することでしたが、それはコストの 11% 側を触る話でした。触るべきは 89% 側、すなわちビルドそのものを止めることです。git 連携の自動ビルドを停止し、自前の CI ランナーでビルドした成果物だけをデプロイする「build once, promote many(1 回作って各環境へ昇格させる)」に切り替えました。成果物を dev→staging→prod へ昇格させるので、Amplify 上のビルドは基本ゼロになります。ビルドは開発が活発になるほど回数が増えるため、今の額が小さくても将来は比例して伸びます。だから移設ではなく、丸ごと止めました。

ここで 1 つ、プラットフォーム固有の制約に当たりました。git 連携済みの Amplify アプリは、成果物を直接送り込む create-deployment API(成果物を直接アップロードしてデプロイする API)を拒否します。既存アプリの設定変更では回避できず、環境ごとに repo 未連携の「手動デプロイ専用アプリ」を新設して対処しました。

戦線 3: 月 $47 の原因は「15 分ごとの稼働確認」

3 つ目が、15 分ごとの monitor(監視)です。キュレーション基盤の DynamoDB 読み取りコストが月 $47 に達していて、内訳を追うと本体機能ではなく、パイプラインの健康状態を監視する Lambda が 15 分ごとに 1.22GB の GSI(グローバルセカンダリインデックス。検索用の副次索引)を全ステータス分 COUNT(全件を数える走査)していたことが原因でした。しかもその大半は、一度終端に落ちたら変化しない除外系ステータス(パースエラーや anti-bot(ボット除け)による除外)まで毎回数え直していました。監視そのものがコスト源になっていた形です。

応急処置は、ほぼ変化しない終端ステータスの集計頻度を落とすことでした。

# 全ステータスを 15 分毎に COUNT していたのをやめ、
# 変化しうるものだけ高頻度、終端系は 2 時間毎に間引く
ACTIVE = {"RAW", "CRAWLED", "ANALYZING"}          # 動くもの
TERMINAL = {"EXCLUDED_PARSE_ERROR", "EXCLUDED_ANTIBOT", "PUBLISHED"}

def collect_counts(now):
    statuses = ACTIVE.copy()
    if now.minute < 15 and now.hour % 2 == 0:      # 終端系は 2h に 1 回だけ
        statuses |= TERMINAL
    return {s: count_by_status(s) for s in statuses}

これで読み取りコストは約 87% 減(月 $47 → $5〜6 相当)になりました。ただしこれは応急処置です。この設計は、記事が増えるほど COUNT の対象も増え、コストがデータ量に比例して伸びます。今 $47 でも、扱う記事が 100 倍になれば読み取りもおよそ 100 倍。だから恒久解として、ステータス集計を毎回 GSI で数えるのをやめ、DynamoDB Streams(テーブルの変更を逐次流すしくみ)で件数を差分更新するカウンタテーブルにして、集計の読み取りを O(1)(件数に関係なく一定の処理量)にしました。これは翌月に実装しています。

戦線 4: 監視・バックアップ・dev 環境という「本体以外」

戦線を重ねて見えてきたのは、コスト源がどれも「本体以外」に集まることでした。バックアップエンジンが毎日 dev テーブルをフルスキャンしていて、そこに溜まった 82 万件超のクロールジョブ痕跡・記事ゴミが、スキャン量とバックアップ量を押し上げていました。dev を運用系の 665 件までパージして解消しました。稼働監視 SaaS 側でも、dev の 22 モニターを 5 分間隔から日次に落として月 $18→$4 に、prod の詳細コンテナメトリクスを無効化して CloudWatch(AWS の監視・メトリクス基盤)費を削りました。取り込み前トリアージで分析対象を約 94% 減らし、1 日あたりの分析コストを $0.13 に抑えたのも同じ流れです。

横断で効いたのが、複数プロバイダのコストを 1 画面に集めるダッシュボードでした。ここで 1 つバグに当たりました。ある日 Anthropic のコストだけ異様に大きく表示され、調べると Admin API(Anthropic の管理用 API)はコストをセント建て(最小単位)で返すのに、スクリプトがドルとして合算していました。API の生値 "58.3135" は $0.58 が正しく、100 倍に膨れていたのです。÷100 の 2 行修正で直りましたが、3 プロバイダで通貨単位が全部違う(一方はセント、一方はドル、請求は円)という点をメモに残しました。将来の額を想像する前提は、まず今の数字が正しく見えていることです。可視化が 100 倍間違っていれば、コスト判断はまるごと狂います。

コストダッシュボードが100倍表示になっていたバグの発見。Admin API のセント建てをドルとして合算していた2行の修正(実端末の再構成・ホスト名/パスは匿名)

仕組み: なぜ「本体以外」が将来のコストになるのか

やってみて分かったのは、コストの出やすい場所には共通構造があることです。本体機能はリクエストが来たときだけ課金される (弾力的) のに対し、NAT・ビルド・周期監視・バックアップは「動いていなくても・変化がなくても走り続ける」定常コストです。定常コストは、規模や時間、データ量に比例して静かに伸びます。しかも IaC のデフォルトや「とりあえず 15 分間隔」のような無意識の設定として紛れ込むため、機能開発の視界に入りません。

だから「今いくらか」ではなく「100 倍になったらいくらか」で見ます。そのうえで「測ってから直す・直したら測る」を運用に固定し、コスト変化を請求ではなくダッシュボードで日次に見るようにしました。請求書は 1 か月遅れて届くので、それだけを見ていると手遅れになります。適切なインフラとは、機能が動くことではなく、この定常コストが将来も破綻しない形に整えることでした。

まとめ — 転用できる教訓

  • 今の額ではなく「100 倍になったら」で見る。小さい定常コストほど、規模やデータ量に比例して静かに伸びます。小さいうちに、伸びる構造ごと消す
  • IaC のデフォルトを疑うnatGateways を明示しないと NAT が勝手に立ちます。「サーバーレス=固定費ゼロ」は自動では成立しません
  • 請求は内訳を分解してから直す。Amplify の 87% がビルド時間と分かって初めて、11% 側の移設ではなく 89% 側の停止が正解だと分かりました
  • 周期実行は増えるほど高くつく。15 分ごとの全 COUNT は差分カウンタで O(1) に。監視・バックアップ自身がコスト源になり得ると疑う
  • 可視化を正しく作る。プロバイダごとに通貨単位が違います。ダッシュボードが 100 倍間違えていれば、将来の額どころか今の判断ごと狂います

筆者について

上原正吉。EarthLink Network Co., Ltd. でAI開発をしています。2025年からClaude Codeを開発の主体に据え、今は20を超えるプロダクトを1人で同時に開発・運用しています。この連載では、その現場で実際に起きたこと(うまくいったことも、失敗も)を、数字と一緒に書いていきます。

また、AIで業務や開発を組み替えたい会社・チーム向けに、AI活用のコンサルティングも受け付けています。ご相談は www.eln.ne.jp からどうぞ。

AWSの小さな課金を、100倍になった時を想像して下げた