nano-cms ドキュメントは株式会社ケイズ・ソフトウェアが運営しています。
nano-cms技術ドキュメント
← ドキュメント一覧へ

サブディレクトリにしない ― 事前確認サイトを専用ホスト名で、本番と同じ階層のまま配る設計

事前確認サイトは、公開で出るのと同じ静的サイト一式を、専用のアドレスに丸ごと建てて確認する仕組みです。ここではその配信の中身 ―― なぜ専用ホスト名で出すのか、なぜサブディレクトリにしないのか、そして「新しい確認サイトを 1 つ建てる」がディレクトリを 1 つ作るだけで済むのはどういう設計か ―― を書きます。

サブディレクトリにすると、本番と「階層」が変わる

素朴には、確認用サイトを本番ホストのサブディレクトリ ―― たとえば cms.example.com/preprod-123/ の下 ―― に置く手があります。動きそうに見えて、これは本番の忠実な複製になりません

静的サイトのページは、アセットやリンクをルートからの絶対パスで書きます(/assets/style.css/keis-cms-preprod/ など)。本番ではこれが正しく解決します。ところがサブディレクトリ配下では、ページが /preprod-123/index.html にあっても、/assets/style.csscms.example.com/assets/style.css(ルート)を指します ―― /preprod-123/です。CSS もリンクも画像も、軒並み別の場所を指して壊れます。

「ならパスを書き換えればいい」とも思えますが、そこが落とし穴です。確認のためにパスを書き換えた時点で、確認しているサイトのパス構造が本番と違うものになります。ルート相対リンク・正規 URL・リダイレクト ―― パスに依存するものはすべて、本番と挙動が変わりうる。「事前確認サイトでは動くのに、本番で壊れる(あるいはその逆)」が起きる。確認の意味そのものが薄れます。

図1: サブディレクトリで出すと /assets/style.css がサブディレクトリの外を指して壊れる。専用ホスト名で出すとルートからのパスが本番と1対1で一致する。
図1: サブディレクトリは本番と階層が変わる(絶対パスが外を指す)。専用ホスト名なら、違うのはホスト名だけでパスは本番と 1 対 1。

専用ホスト名なら、違うのは「ホスト名だけ」

nano-cms は、確認サイトごとに専用のホスト名を割り当てます ―― {slug}-preprod-{日時}.cms.example.com のような、日付つきのホストです。サイトの中身は、そのホストのルート直下に置きます。だから /assets/style.css は、本番とまったく同じく「そのホストのルート+ /assets/style.css」に解決します。

本番と違うのはホスト名ただ一つ。パスの階層は 1 対 1 で同じです。だから、ルート相対のリンクもアセットも、書き換えずにそのまま本番と同じく解決する。確認したものが、そのまま本番で出る ―― これが「専用ホスト名で出す/サブディレクトリにしない」という判断の核心です。

「新しい確認サイト」は、ディレクトリを 1 つ作るだけ

専用ホスト名にすると、こんどは「確認サイトを建てるたびに、ホストごとの配信設定が要るのでは?」という心配が出ます。nano-cms はここも、設定を増やさない形にしてあります。

配信の nginx には、preprod 用の server_name 正規表現が 1 本だけ置いてあります。これがすべての preprod ホストにまとめて一致し、ホスト名から slug日時 をその場で取り出します。そして配信元(document root)を、取り出した値からその場で組み立てます

# nginx(要点)― ホスト名の slug・日時をキャプチャし、root を組み立てる
server_name ~^(?<site>[a-z0-9][a-z0-9-]*)-preprod-(?<datetime>\d{4}-\d{2}-\d{2}-\d{2}-\d{2})\.cms\.example\.com$;
root /var/www/static-output/.preprod-static-$site-$datetime;

つまり、ホスト名の中の slug と日時が、そのまま配信ディレクトリ名になる。だから新しい確認サイトを建てるのは、.preprod-static-{slug}-{日時}/ というディレクトリを 1 つ作るだけ。nginx の設定を足す必要も、nginx を reload する必要もありません(既にある正規表現が新しいホストにそのまま一致し、root が自動で決まるから)。消すときも、ディレクトリを消すだけ ―― そのホストは静かに 404 へ戻ります。

「配信面を増やす/減らす」が「ディレクトリを作る/消す」に化けている。設定ファイルを触らないので、設定ミスもダウンタイムも構造的に起きません。なお、配信そのもの(try_files でファイルを探して返す部分)は公開サイトと同一で、違うのは root のディレクトリだけ。ここでも、パスの扱いは本番とまったく同じです。

図2: nginx の server_name 正規表現1本がすべての preprod ホストに一致し、ホスト名の slug・日時から document root をその場で組み立てる。ディレクトリを作る/消すだけで配信ホストが増減し、reload は要らない。
図2: nginx は正規表現 1 本。ホスト名の slug・日時がそのまま root のディレクトリ名になる ― 作る/消すだけで配信面が増減(reload 不要)。

社内だけ・古いものは自動で片付く ― 後始末もディレクトリ 1 つ

確認サイトは社内 IP からのみ開けます(公開ページには一切出ません)。そして溜めっぱなしにしません。消えるタイミングは大きく 2 系統 ―― イベント駆動の即時削除と、取りこぼしを拾う日次の掃除です。

即時削除は、予約の公開が消化されたとき・公開予定日時を変更(reschedule)したとき・ワークスペースをアーカイブしたとき・先行する予約が変わって内容が古く(stale)になったときに、不要になった確認サイトをその場で(fire-and-forget で)消します。取りこぼしは、毎日 1 回、全サイトをまとめてスイープするバックストップが拾います(公開が長く走っていないサイトの確認サイトが滞留しないように)。

ただし、むやみには消しません。未来分(公開予定日時がまだ来ていないもの)と、予約一覧 UI がリンク中の日時は常に保持します(消すとリンクが 404 になるため)。消すのは「過去・参照されていない」確認サイトのうち、保持件数を超えた古いものだけ。保持件数はサイト設定で、このサイトでは 5 件(フレームワークの既定は 0 =無効で永久保持、各サイトが件数を入れて初めて働きます)。配信面が「ディレクトリ」である設計だから、どの経路でも後始末は「ディレクトリを消す」一手で完結する ―― これはなぜ 10 年運用しても重くならないのかの自己剪定の一つで、「既定は無効、運用側が件数を入れて初めて働く」という形まで同じです。

設計のポイント

  • 専用ホスト名で出す(サブディレクトリにしない):パスの階層が本番と 1 対 1 になり、違うのはホスト名だけ。確認したものが、そのまま本番で出る。
  • ディレクトリ=配信面:nginx は正規表現 1 本で、ホスト名から root を組み立てる。建てるのはディレクトリを作るだけ、消すのは消すだけ ― 設定追加も reload も無い。
  • 配信は公開サイトと同一:違うのは root のディレクトリだけ。社内 IP 限定で、最新数世代だけを残して自己剪定する。

事前確認サイトが「本番そっくり」でいられるのは、凝った変換のおかげではありません。本番と変えていいのはホスト名だけ、という一線を引いたこと、そして配信面をディレクトリに落とし込んだこと ―― この 2 つです。中身として何を書き出しているか(予約を時刻で畳んだ結果を静的化したもの)は予約変更を時系列で積み上げる設計に、機能としての使い方は事前確認サイトにあります。なお、このドキュメントサイト自体も @nano-cms/core で動いており、各ページも予約公開で「当日の見え方」を確認してから出しています。