Webサービスを作り直した話——RenderとAWSをやめて月5,000円→ほぼ0円にしたサーバーレス構成
2026-08-01
受託案件の設計は、クライアントが特定できる形では出せない。その点、自社で運営しているサービスなら数字まで含めて全部出せる。
この記事では、当社が運営している画像コンテンツを扱うメディア系Webサービス(名前は伏せる)のアーキテクチャの変遷を公開する。きれいな完成図だけ見せても仕方がないので、2回作り直して今の形に落ち着くまでの、失敗した構成も含めて書く。
要点(先に結論)
- 現在の構成は常駐サーバーゼロ。フロントはNext.js(NetlifyのCDN配信)、API側はCloudflareに全寄せ——Hono(Workers)、DBはD1、キャッシュはKV、画像はR2、日次バッチはCronトリガー
- ここに至る理由は2つ。表示速度(前構成はAPIサーバーの応答が遅かった)と費用(アクセスがない時間にも月5,000円前後の固定費を払っていた)
- 移行は一気にやらず、画像→API→切り替えの順で段階的に。サービスは止めていない
- 引き換えに諦めたものもある。D1(SQLite)の制約で重い集計はできず、実際に一部機能を取り下げた。書き込みが多い業務システムにはこの構成を勧めない
前提:どんなサービスか
- 画像コンテンツを扱うメディアサービス。検索流入が主で、ユーザー投稿と管理画面がある
- コンテンツの自動タグ付けに生成AIを使っている
- 規模は大きくない。個人開発に毛が生えた程度のトラフィックで、だからこそ「アクセスが少ない時期のインフラ費をいかに抑えるか」が設計要件になる
ここまでの歩み(2回の作り直し)
第1世代:一体型。 フロントエンドのフレームワークに表示もデータ取得も全部載せた、よくある構成。動くには動いたが、2026年1月の自社計測でこうなっていた。
- Lighthouse Performanceスコア 57点(目標80点)
- TTI(操作可能になるまでの時間)1 14.2秒(目標5秒以下)
- データ取得のエラーハンドリング不足で5xxエラーのページが267件発生し、クローラーがブロック。インデックス登録率は22%まで低下
第2世代:フロントとAPIを分離。 フロントをNetlify、APIをRender(Laravel+MySQL+Redis)、画像をAWS(S3+CloudFront、DNSはRoute 53)という構成にした。分離自体は正解だったが、新しい問題が出た。APIの応答が遅い。安いプランの常駐サーバーは非力で、無料枠に至ってはアクセスが途切れるとスリープし、起床に数十秒かかる。そして費用は、Render側が月3,000円前後、AWSの画像まわりが月2,000円前後。合計約5,000円/月の固定費が、使っていない深夜にも発生し続ける。売上が立つ前の小規模サービスにとって、この「遅いのに固定費」は一番割に合わない組み合わせだった。
第3世代(現行):問題が出ていたAPI側だけを、Cloudflareのエッジ2に作り替えた。 常駐サーバーを廃止し、リクエストが来た瞬間だけコードが動く形にした。一方で、問題が出ていなかったフロントのCDN配信(Netlify)はそのまま残した。動いているものまで道連れにしないのも設計判断のうち。以下が全体図。
以下、判断を一つずつ。
判断1:表示と業務ロジックの分離は維持する
フロントはNext.jsのSSR3。SEOが生命線のメディアなので、HTMLはサーバー側で組み立て、画像も最適化して返す。業務ロジックと管理機能はHonoで書いたAPIに分け、フロントはAPIを呼ぶだけ。
分ける理由は表示とロジックの寿命が違うから。フロントエンドの技術は数年で入れ替わるが、「投稿を保存する」「権限を確認する」という業務ロジックは何年も変わらない。実際、今回の第2→第3世代の作り直しでは、この分離のおかげでフロントには一切手を触れず、API側だけを丸ごと作り替えられた。一体型のままだったら全部を道連れにしていた。
判断2:常駐サーバーをやめる
第2世代の反省がそのまま理由になっている。小規模サービスにおける常駐サーバーは、
- アクセスがなくても毎月固定費がかかる
- 安いプランだと非力で、ピーク時に遅い
- 無料枠はスリープがあり、最初の訪問者が数十秒待たされる(メディアでこれは致命傷)
という「小さく始める」と相性が悪い性質を持つ。Workersはリクエストが来た瞬間だけ動く従量課金で、起動待ちは体感できないほど短い。この構成にしてから、インフラの固定費はほぼゼロになった。アクセスが増えたら増えた分だけ払う。小規模メディアの経済構造に合っている。
判断3:データもAPIと同じ場所に置く(D1・KV・R2)
APIだけエッジに置いても、DBが遠くにあれば毎回そこまで取りに行くことになって意味が半減する。だからデータも同じ場所に寄せた。
- D14:データ本体。実体はSQLiteで、読み取り中心のメディアにはこれで足りる
- KV5:キャッシュ。「今日のピックアップ」のような全員に同じものを返すデータは、DBに毎回聞かずKVから返す
- R26:画像・ファイル。CDNからそのまま配信するので、アプリのコードは画像のバイト列を一切運ばない。画像サービスでは帯域の大半を画像が食うので、ここを剥がすと配信コストが桁で変わる(R2は下り転送量が課金されない。執筆時点)
判断4:日次バッチは同じWorkerのCronトリガーに
日次の集計と、生成AIによる自動タグ付けは、CloudflareのCronトリガーで毎日0時に同じWorker内で走らせている。
じつは以前、バッチを別言語の独立プロセスとして持っていた時期もある。処理の分離としては正しいが、少人数運用ではデプロイ・監視・環境変数の管理が1系統増えること自体がコストで、Workerに統合した。それでも設計上の原則は守っている——生成AIの処理はユーザーの画面と同期で混ぜない。AIのAPIは遅いし失敗もする。バッチに置いておけば、失敗してもリトライすればいいだけで、ユーザーは何も知らずに済む。AIを機能に組み込むときの一般原則として「失敗できる場所に置く」は毎回効く。
判断5:認証は自作しない
ログインは外部の認証基盤(IDaaS7)に任せた。認証は「作るのは簡単、正しく作るのは難しい」の典型で、事故ったときの被害が最大の領域でもある。ここは自社の独自性が出る場所ではないので、外部に払う費用は保険料と考えている。
移行の実際——止めずに引っ越した手順
作り直しと言っても、動いているサービスを止めて一斉切り替えするのは怖い。実際の順序はこう。
- まず画像だけ移した(S3 → R2)。一番リスクが低く、料金インパクトが一番大きい部分から着手した。これが想像よりずっと軽作業で、R2はS3互換APIなので、アプリ側はストレージの接続先(エンドポイントとキー)を差し替えるだけ。うちのコードの変更は実質「S3ドライバの向き先をR2にする」の1点だった。データ本体もCloudflare公式の移行ツールがあり、移行元のAWSアクセスキーを渡すとバケットを丸ごとコピーしてくれる(一括のSuper Slurperと、アクセスされたものから順に移すSippyの2種類。執筆時点)。ここでAWS側の約2,000円/月がほぼ消えた
- 旧APIを動かしたまま、新APIを並行開発。Renderの旧構成は生かしたまま、Hono+D1版を別で作って検証した。旧APIが動いている限りユーザーには何も起きない。DBの移行はここが本体で、MySQLとSQLite(D1)は方言が違うのでダンプをそのまま食わせることはできない。スキーマは全テーブルをD1用に定義し直し、データはMySQLのダンプをSQLiteで通る形に変換してから流し込んだ
- 動作確認が終わってから向き先を切り替え。フロントの参照先を新APIに変更。DNSはRoute 53からCloudflareに移管して、ドメイン・CDN・APIの管理を一箇所にまとめた。旧環境はすぐ解約せず、しばらく残してから畳んだ
順序の原則は「リスクが低く効果が大きいものから、常に戻れる状態で」。切り替えの瞬間に賭けをしない。
費用のビフォーアフター
| 項目 | 移行前 | 移行後 |
|---|---|---|
| 画像配信 | S3+CloudFront+Route 53:約2,000円/月 | R2:ほぼ0円(下り転送無料・保存量のみ) |
| API+DB | Render(Laravel+MySQL+Redis):約3,000円/月 | Workers+D1+KV:無料枠内 |
| 合計 | 約5,000円/月(固定) | ほぼ0円/月(ドメイン代を除く・従量課金のみ) |
年に直すと約6万円の固定費が消えた計算になる。金額そのものより、「アクセスが増えるまで費用が発生しない」構造に変わったことが大きい。小規模サービスは「当たるまで安く生かしておく」が最重要で、固定費はその寿命を直接削る。
※金額は当社の規模(個人開発に毛が生えた程度のトラフィック)での実績。規模が違えば当然変わる。
課題はどうなったか
| 旧世代の課題 | 現行構成での対応 |
|---|---|
| 重い処理と表示の同居(TTI 14.2秒) | 表示専用のNext.jsに分離、画像最適化を標準化 |
| データ取得の失敗がページの5xxになる | フロントとAPIを分離し、失敗時の表示をフロントで制御 |
| APIサーバーが非力で遅い・スリープで数十秒待ち | 常駐サーバー廃止。リクエスト時だけ実行、起動待ちは実質ゼロ |
| アクセスがなくても固定費(約5,000円/月) | 従量課金でほぼ0円に |
| 画像配信がサーバーの帯域を食う | R2+CDN配信に分離 |
| AI処理の失敗がWebに波及 | Cronバッチに隔離、リトライ前提 |
正直な話
いいことばかり書いたので、諦めたものも書く。
D1はSQLiteなので、重い集計に弱い。 実際、リアルタイムランキング機能は集計負荷の問題で一時取り下げた。管理画面のデータエクスポートでも、一度に渡せるパラメータ数の上限に当たって作り方を変えている。RDBの感覚で複雑なクエリを投げる用途には向かない。
Cloudflareへの依存度は高い。 API側の実行・DB・ストレージ・キャッシュを1社に寄せているので、移転するとなれば大仕事になる。うちは費用対効果で割り切ったが、ここを嫌う判断もありうる。
そして、この構成は「読み取り中心・小規模・費用最優先」だから成立している。書き込みが多い業務システム、複雑なトランザクションが要る決済まわり、社内システムのような安定第一の案件では、王道のRDB+常駐サーバー構成のほうが向くし、当社も受託ではそちらを提案することが多い。構成に貴賎はなくて、サービスの性質と規模で選ぶだけ。
こういう依頼に対応できます
この記事でやったこと——「今の構成のどこが限界かを数字で特定し、どこを分けてどこを分けないかを決め、費用構造ごと作り替える」——が、そのまま当社の新規開発・作り直しの仕事のやり方になっている。技術選定・設計だけの相談でも構わない。
Footnotes
-
TTI(Time to Interactive) — ページが表示されてから、ボタンや入力が実際に反応するようになるまでの時間。表示が速く見えても、この値が悪いと「押しても効かないページ」になる。 ↩
-
エッジ — 世界中に分散したCDNのサーバー網の上でコードを動かす仕組み。利用者に近い場所で実行されるため速く、常駐サーバーを持たない。Cloudflare Workersが代表例。 ↩
-
SSR(サーバーサイドレンダリング) — ページのHTMLをサーバー側で組み立てて返す方式。検索エンジンに内容が読まれやすく、初回表示も速い。 ↩
-
D1 — Cloudflareのサーバーレスデータベース。実体はSQLiteで、Workersから近距離で読み書きできる。小〜中規模の読み取り中心ワークロードに向く。 ↩
-
KV — Cloudflareのキー・バリュー型ストレージ。「このキーならこの値」を世界中に複製して高速に返す。頻繁に読まれてたまに更新されるデータのキャッシュに向く。 ↩
-
オブジェクトストレージ — 画像やファイルを置く保管専用サービス(Cloudflare R2、Amazon S3など)。安く大量に保存でき、CDNと組み合わせて配信に使う。 ↩
-
IDaaS — ログイン・認証の仕組みをまるごと提供する外部サービス(Auth0など)。パスワード管理やSNSログインを自前で実装せずに済む。 ↩