ReactのSPAをさくらのクラウドで配信すると、奇妙な体験をする。トップページから画面を切り替えていくぶんには完璧に動くのに、/chat/abc123 のようなURLをブラウザへ直接入れると404(ページが見つかりません)が返ってくる。さっきまで動いていた画面で、リロードを押しただけでも404。アプリは壊れていない。壊れているのは、URLと配信側の約束のほうだ。
まず前提から書いておく。EastCloudでは、デジタル庁がOSSとして公開した政府向け生成AI「源内」を、国産のさくらのクラウド上で動かせるかを検証するプロジェクトを進めている。データを国内に置きたい組織にとって、政府製のAIアプリ資産が国産クラウドで動くかどうかは、実利のある問いだからだ。プロジェクトの全体像と「何が動いて、何が動いていないか」は前回の記事にまとめたので、未読の方はそちらから読んでもらえると、本稿の位置づけが掴みやすい。
その検証で最初にぶつかったのが、冒頭の404だった。AWSなら定番の設定ひとつで済む話が、さくらのクラウドでは自明には解けない。本稿は、2026年6月の実機検証で確認できたこと・できなかったことと、最終的に採った構成の記録である。
この記事の主役になる用語(知っている方は読み飛ばしてください)
SPA(シングルページアプリケーション)
Webアプリの作り方のひとつ。ページごとにHTMLファイルをサーバからもらうのではなく、最初に1枚の index.html とJavaScriptを受け取り、以後の画面切り替えはブラウザの中でJavaScriptが行う方式を指す。Reactなどで作られた今どきの管理画面やチャットアプリはたいていこの方式で、源内のWeb画面も例外ではない。
deep link(直リンク)
トップページではなく、アプリ内の特定の画面を指すURLへ直接アクセスすること。/chat/abc123 のような「会話画面そのもののURL」をブックマークする、同僚に共有する、開いたままリロードする——どれもdeep linkの利用にあたる。
CloudFront
AWSのCDN(コンテンツ配信サービス)。CDNの本業は、利用者の近くにファイルのコピー(キャッシュ)を置いて速く配ることだが、CloudFrontにはもうひとつ、後述する「SPA配信の要」になっている機能がある。
SPAのdeep linkはなぜ404になるのか
SPAでは、/chat/abc123 というURLに対応するファイルはサーバ上に存在しない。サーバにあるのは index.html とJavaScript・CSSの束だけで、「/chat/abc123 の画面」はブラウザの中でJavaScriptが組み立てている。
だから、トップページから入ってリンクをたどるぶんには何も起きない。画面の切り替えはすべてブラウザ内で完結しているからだ。ところが、URLを直接開いたりリロードしたりすると、ブラウザは律儀にサーバへ「/chat/abc123 というファイルをください」と聞きに行く。サーバは正直に「そんなファイルはありません」と404を返す。JavaScriptが動き出す前の出来事なので、アプリ側にはどうしようもない。
これを防ぐ方法は昔から決まっている。「実ファイルがないURLへのアクセスには、とりあえず index.html を返す」という約束を、配信側に持たせるのだ。index.html さえ届けばJavaScriptが起動し、URLを見て正しい画面を組み立ててくれる。この約束をSPA fallbackと呼ぶ。
デジタル庁が公開しているオリジナルの源内Web(公式リポジトリ genai-web)もReact製のSPAで、/chat/:chatId や /history といった画面のURLを持つ。つまり、リロードやURL共有のたびに、必ずこの問題を踏む前提の作りになっている。
オリジナルの源内Webは、この問題をどう解いているか
これがAWSの上なら、答えは最初から用意されている。オリジナルの源内WebはAWSで動かす前提で設計されており、公開されているインフラ定義を読むと、ファイル置き場のS3にビルド済みファイルを置き、前段のCloudFrontが配信する構成になっている。そしてCloudFrontの「カスタムエラーレスポンス」という機能で、置き場が403や404を返したら /index.html を代わりに返す——しかもステータスコードは200にする——よう設定されている。
要するに、エラーを握りつぶして全部SPAへ投げる。乱暴に聞こえるが、これがSPA配信の事実上の標準で、S3 + CloudFrontがSPAホスティングの定番になった理由のひとつでもある。実際、当社では源内WebをそのままAWS上で動かす検証も並行して行っているが、この仕組みは何も考えなくても機能する。定番すぎて、依存として意識されないのだ。AWSから離れようとして初めて、「あの404対策」が独立したひとつの機能要件だったことに気づく。
当初考えた構成 — オブジェクトストレージ + ウェブアクセラレータ
最初に組んだのは、静的配信の王道といえる構成だった。フロントエンドのビルド成果物(HTML・CSS・JavaScript)を、さくらのオブジェクトストレージへ置き、前段のウェブアクセラレータから配信する。オブジェクトストレージは「ファイル置き場」のサービスで、AWSのS3と互換のAPI(S3互換)を持つ。つまりS3向けの操作コマンドが、接続先の差し替えだけでそのまま使える。ウェブアクセラレータはさくらのCDNで、オブジェクトストレージのバケット(置き場)を配信元にできる。AWSのS3 + CloudFrontに対する置き換えとしては、もっとも素直な対応関係である。
結論から言えば、この構成でやりたかったことのほとんどは動いた。実機で確認できた範囲は次のとおりである。
| 確認したこと | 結果 |
|---|---|
| HTML / CSS / JSのアップロード | 可。aws s3 sync による一括アップロードも動く |
| Content-Type(ファイル種別の申告)の付与 | 可。text/html、text/css、text/javascript等を確認 |
| Cache-Control(キャッシュの効かせ方)の付与 | 可。レスポンスヘッダでも確認 |
| 公開 / 非公開の制御 | 可。ACL(アクセス権設定)で制御できる |
| 一覧・取得・削除 | 可 |
| ウェブアクセラレータ経由のHTTPS配信 | 可。バケットを公開設定にせず、読み取り権限ベースで配信できる |
| キャッシュの効き | 可。設定した保持時間が反映され、2回目以降はキャッシュから返る |
| 末尾が / のURLへのドキュメントインデックス | 可。index.htmlを補ってくれる |
そして、ひとつだけできなかったことがある。実在しない任意のURLへのアクセスは、素直に404になる。つまり、この記事の主題である deep link が通らない。
ここは正確に書いておきたい。ウェブアクセラレータのドキュメントインデックスは「/ で終わるURLに index.html を補う」機能である。これは「フォルダを開いたら目次ファイルを見せる」という、Webの古い慣習に沿った振る舞いであって、「存在しないあらゆるURLを index.html へ戻す」というエラーハンドリングの話ではない。似ているようで別物だ。SPA fallbackに必要なのは後者であり、CloudFrontのカスタムエラーレスポンスに相当する設定は、検証時点のウェブアクセラレータには見当たらなかった。

つまりこの構成は、「/ から入って画面内を遷移する」使い方なら成立するが、deep linkのあるSPAを丸ごと任せるには一歩足りなかった。
ひとつ補足すると、ここで言っているのはあくまで「画面を配るためのファイル置き場」の話である。利用者がアップロードするファイルやチャット履歴のような、アプリのデータの保存は別の問題であり、今回の検証範囲には含めていない。S3互換には癖もあり、たとえばTerraformのbackendに使う場合の問題は以前の記事で扱われているが、フロントエンド配信の用途では引っかからなかった。
現実解: AppRun上のゲートウェイに配信とルーティングを寄せる
そこで、静的配信に任せる方針をやめ、構成を変えた。AppRunは、コンテナ(アプリ一式を箱詰めしたもの)を渡すとそのまま動かしてくれる、さくらのクラウドのアプリ実行サービスである。このコンテナへフロントエンドのビルド成果物を同梱し、Go言語で書いた薄いゲートウェイ——玄関口に立って、来たリクエストを行き先ごとに振り分ける小さなプログラム——が、配信とルーティングの両方を引き受ける。
ゲートウェイの仕事は、リクエストのURLを見て分類することに尽きる。
- 実在する静的ファイル(
/assets/配下など)→ そのまま返す - 対応済みの画面ルート(
/chat/:id、/historyなど)→index.htmlを返す。SPA fallbackはここだけ /api/*(画面ではなく、プログラム同士の通信窓口)→ APIとして処理する。エラーになってもJSONで返し、index.htmlへは逃がさない/health(死活監視用)→ 稼働確認の応答を返す- 上記のどれでもないURL → 成功扱いにしない

CloudFront方式との違いは、fallbackの範囲を「対応済みの画面ルート」に限定した点だ。「404ならなんでもindex.html」は手軽だが、APIのエラーや設定ミスまで画面側へ流れ込み、失敗が見えなくなる副作用がある。とくにAIアプリでは、裏側の失敗が画面上それらしく見えてしまう事故を避けたかった。
ルートを明示的に分類する方式は、手間のぶんだけ「このアプリはどのURLを引き受けるのか」が設計として言語化される。壁を回避するための構成が、結果としてルート境界の棚卸しになったのは思わぬ収穫だった。
逆に言えば、この構成を選んだ時点で、配信の正本はオブジェクトストレージではなくコンテナの中という割り切りをしている。CDNによる配信効率やキャッシュ戦略は別途設計が要るし、静的配信へ寄せたい場面ではfallback問題が残り続ける。万能の解ではなく、検証時点での現実解である。
「ない」のは欠陥ではなく、設計思想の違い
誤解のないように書いておくと、これはさくらのクラウドの落ち度という話ではない。「404のときに別のファイルを200で返す」というCloudFrontの挙動は、考えてみればかなり特殊なことをしている。エラーをエラーとして返すのは、配信サービスとしてはむしろ素直な設計だ。SPA fallbackはあくまでアプリ側の都合であり、それを配信側が肩代わりするか、アプリ側(今回ならゲートウェイ)が自分で引き受けるかは、役割分担の思想の違いと捉えるのが正確だと思う。
そのうえで、静的配信側に任意URLのfallback設定が加われば、SPAは置き場とCDNだけで完結し、AppRunはAPIに専念できるようになる。選択肢が増えること自体は素直に歓迎したい。
なお、この種の検証結果には賞味期限がある。今回の検証は2026年6月中旬のもので、さくらのクラウドは機能追加が活発だ。当社の別の検証でも、一度「構造的に不可能」と結論づけた制約が、後日の機能提供であっさり解けた例がある。この記事を読んでいる時点では状況が変わっているかもしれない。設計判断の前に、現行の公式ドキュメントとリリースノートを確認してほしい。
まとめ
- SPAは「1枚のindex.htmlとJavaScriptで全画面を描く」作りのため、実ファイルのないURLへの直接アクセス(deep link)では配信側の協力が要る
- オリジナルの源内WebはCloudFrontのカスタムエラーレスポンスでこれを解いており、定番すぎて依存として意識されにくい
- オブジェクトストレージ + ウェブアクセラレータの静的配信は、アップロード・Content-Type・キャッシュなど大半がそのまま動いた(S3互換操作を実機確認)
- ただし任意URLのSPA fallbackだけは検証時点では確認できず、deep linkが通らなかった
- 現実解として、AppRun上のGoゲートウェイでルートを分類し、対応済みの画面ルートだけにfallbackを限定した
- これは欠陥ではなく設計思想の違い。AWSからの移行では、CloudFrontが何を肩代わりしてくれていたかを先に棚卸しするのが早道になる
前回の記事で「壁1」として一行だけ触れたものの正体が、この404である。AWSで当たり前だったものは、離れてみて初めて輪郭が見える。移行の検証とは、結局その輪郭をひとつずつなぞる作業なのだと思う。
よくある質問
Q1. さくらのクラウドでReactなどのSPAをホスティングできるか
できる。ただしURL直接アクセス(deep link)への対応に工夫が要る。2026年6月の検証時点では、オブジェクトストレージ+ウェブアクセラレータの静的配信だけでは任意URLのfallbackを実現できず、AppRun上のゲートウェイで対応した。
Q2. さくらのオブジェクトストレージで静的サイトを公開できるか
できる。S3互換APIでHTML・CSS・JavaScriptを配置し、公開設定またはウェブアクセラレータ経由での配信が可能である。Content-TypeやCache-Controlの付与も実機で確認できた。
Q3. ウェブアクセラレータで404をindex.htmlに差し替えられるか
検証時点(2026年6月)では、CloudFrontのカスタムエラーレスポンスに相当する機能は確認できなかった。ドキュメントインデックスは「/で終わるURL」への補完機能であり、任意URLのfallbackとは別物である。最新の仕様は公式ドキュメントで確認してほしい。
Q4. SPAのdeep linkが404になる原因は何か
SPAでは /chat/123 のようなURLに対応する実ファイルがサーバ上に存在しないためである。配信側に「実ファイルのないURLには index.html を返す」仕組み(SPA fallback)がないと、URL直接アクセスやリロードで404になる。
Q5. AppRunでフロントエンドとAPIを同居させるときの注意点は何か
SPA fallbackの範囲を対応済みの画面ルートに限定し、/api/* のエラーを index.html へ逃がさないことだ。APIの失敗が画面上の成功に見えてしまう事故を防げる。
Q6. CloudFrontのカスタムエラーレスポンスとは何か
配信元が403や404を返した際に、指定したファイル(SPAでは /index.html)を指定したステータスコードで返すCloudFrontの機能である。S3 + CloudFrontによるSPA配信は、事実上この機能を前提にしている。
