この記事の面白いところは、かなり現実的な悩みから始まっている点です。
フロントエンドはVercelを使えば、PRを作っただけでプレビューURLが自動で生えます。これは本当に便利で、「見た目の確認」はほぼノーコストです。
でも、アプリがフロントとバックエンドで分かれていると話は終わりません。APIの仕様変更やDBの変更が入ると、フロントだけ見ても意味がないからです。
しかもこのケースでは、PRが1本だけじゃなく複数同時に走ることが多い。
生成AIを活用してタスクを並列で進めるようになった結果、レビュー対象のPRも並ぶ。すると「PR Aの変更はAの環境で確認したい」「PR BはBの環境で確認したい」が普通に発生します。
ここ、かなり重要です。
“dev環境をみんなで共有する” 方式では、並行レビューに耐えないんですよね。
個人的にも、ここを雑にすると「たまたま別PRの影響を受けたバグ」が混ざって、レビューの信頼性が一気に落ちると思います。
設計候補は3つありました。
ここで2は、複数PRを同時に確認したいという要件に合わないので却下。
3は分離としてはきれいですが、コストも運用負荷も重すぎる。プレビュー用途としてはやりすぎ、という判断です。
最終的に1を採用しています。
これはかなり現実的です。「ちゃんと分けたい。でも分けすぎると死ぬ」問題への、ちょうどいい着地だと思います。
ここが技術的に気持ちいいポイントです。
PostgreSQLには「schema」という仕組みがあります。ざっくり言うと、1つのDBの中に、独立した名前空間をいくつも作れる感じです。
たとえば pr_123 というschemaを作って、PR番号ごとにそこへデータを閉じ込める。
しかもPrismaでは接続URLに ?schema=pr_123 のような形で指定できるので、アプリ本体のコードをいじらずに接続先だけ切り替えられるのが強い。これはかなり賢いです。
この方式なら、
prisma migrate deploy をそのスキーマに対して流すDROP SCHEMA pr_123 CASCADE で消すという流れで、DBをきれいに分離できます。
個人的には、ここは「フル分離より軽く、共有より安全」というかなり良いバランスだと思います。
プレビュー環境に求めるのは“完璧な隔離”より“実用的な隔離”ですからね。
フロントエンド側はVercelのプレビュー環境を使いますが、そこからPR固有のバックエンドに向ける必要があります。
そのために、Vercelの環境変数 NEXT_PUBLIC_BACKEND_API_URL に、PRごとのURLを入れる方式にしています。
URLの形は pr-{PR番号}.preview.example.com。
ここで使っているのが、
です。
pr-123.preview.example.com みたいな名前を、事前にまとめて受けられる仕組みこれによって、PRごとにDNSレコードを毎回作らなくてよくなっています。
地味ですが、こういう「毎回の手作業を消す」設計が、運用の快適さを決めます。
この記事の肝は、単に「デプロイできた」ではなく、作成から削除までのライフサイクルを自動化しているところです。
流れはこうです。
preview-deploy.yml → 隔離環境を起動preview-deploy.yml → 既存環境を更新preview-cleanup.yml → 全リソース削除preview-scheduled-cleanup.yml → ゴミ掃除この4段構え、かなり堅いです。
特に最後の「毎日深夜のゴミ掃除」は、実務では本当に大事。
人間は削除を忘れます。断言してもいい。だからセーフティネットとして定期クリーンアップを入れるのは正しいです。
preview-deploy.yml の流れがかなり丁寧構築ワークフローは、PR番号を起点にすべての資源名を決めるところから始まります。
たとえばPR #123なら、
...-pr-123preview-pr-123preview-pr-123pr-123pr_123pr-123.preview.example.comという具合です。
これ、地味だけどめちゃくちゃ重要です。
名前のルールが一意だと、作るときも消すときも迷わないんですよね。
運用で一番怖いのは「これ何のリソースだっけ?」になることなので。
ここは記事の中でもハマりどころとして紹介されていました。
最初は「バックエンドができてからVercelの環境変数を設定すればいい」と考えていたそうです。
でもNext.jsの NEXT_PUBLIC_* はビルド時に埋め込まれるので、Vercelがプレビュービルドを始める前に環境変数を入れておかないといけない。
つまり順番は逆で、
という流れになります。
これはかなり「やられがち」なポイントです。
理屈としては単純なんだけど、実装してみないと気づきにくい。
こういう地雷があるから、CI/CDは机上の設計だけでは終わらないんですよね。
サービスを立てる前に、
を順番に走らせています。
ここで前回の記事で使った entrypoint.sh と DB_COMMAND を再利用していて、本番デプロイと同じ仕組みでDBを触るのが印象的でした。
仕組みを増やすと壊れやすくなるので、「同じやり方を流用する」のはかなり良い設計です。
ALBのホストルールには優先度が必要なのですが、ここにPR番号をそのまま足すと、将来的に上限を超える可能性がある。
そこで PR番号 % 49000 + 1 のように剰余で決めたそうです。
ここ、実務っぽくて好きです。
きれいな理屈ではなく、「上限があるなら現実的に収まるように工夫する」という発想。
しかも「49000本のPRが同時に開かれることはまあないだろう」と割り切っているのも、人間味があります。
バックエンドが立った後、Vercel Deployments APIで再デプロイします。
この時点でようやく、フロントからバックまでつながったプレビューになります。
つまりこの仕組みは、
のが本質です。
ここができると、PRレビューの質がかなり上がるはずです。
最後にPRコメントへプレビューURLを投稿します。
これ、派手ではないけど本当に便利。
レビューする側はPRを開いてURLをクリックするだけで済むので、確認のハードルがかなり下がります。
同じPRに対して短時間に複数回pushされることは普通にあります。
そのたびにデプロイ処理が並行すると、名前衝突や二重作成が起きて面倒です。
そこで preview-{PR番号} という concurrency グループを使い、同じPRの処理は同時に走らないようにしています。
古いジョブはキャンセルされ、最新のpushだけが残る。
これはすごく合理的です。
プレビュー環境は「途中経過」を見せるものではなく、「最新状態」を見せれば十分だからです。
preview-cleanup.yml はPRクローズ時に走ります。
削除の順番もかなり重要で、
という順番です。
そして各コマンドに || true を付けて、途中で1つ失敗しても全部止まらないようにしています。
これは運用ではかなり大事です。
「全部成功する前提」で書くと、1個失敗した瞬間に後続が止まってゴミが残りがちなんですよね。
preview-scheduled-cleanup.yml では、GitHub APIでオープンPR一覧を取り、存在しないPRのリソースを探して削除しています。
これは、PRクローズ時のcleanupが失敗した場合の保険です。
正直、こういう保険があると運用の気持ちがかなり楽になります。
「消し忘れたら毎日誰かが代わりに見つけてくれる」というだけで、課金の不安が減るからです。
プレビュー用のFargateサービスそのものはGitHub Actionsから動的に作成する一方で、土台となるものはTerraformで管理しています。
具体的には、
などです。
この分け方はとても筋がいいです。
動的に増えるものはCI/CD、固定の基盤はTerraform。
この線引きが曖昧だと、あとでどこが真実の管理元なのか分からなくなります。インフラ運用でこれは地味に致命傷です。
この記事全体を読んで、特に良いと感じたのは次の3つです。
プレビュー環境は増やすのは簡単、消すのは難しい。
ここを最初からライフサイクル全体で設計しているのがえらいです。
完全分離はきれいだけど重い。
共有だけだと危ない。
その中間としてschema分離を選んだのは、かなり現実を見ていると思います。
NEXT_PUBLIC_* のビルド時埋め込みは、知っていれば当たり前でも、実装中にうっかり忘れやすい。
こういう「理屈では理解してたけど運用では刺さる」話が、記事としてすごく価値あると思います。
この記事は、単なる「PRごとにプレビュー環境を作った話」ではありません。
実際には、
まで含めた、かなり本気のCI/CD設計の話です。
特に印象的なのは、
“理想的な完全分離”より、“運用できる現実的な分離”を選んでいるところ。
このバランス感覚は、実務ではかなり強い武器だと思います。
参考: Pull Requestごとにバックエンドも含めたプレビュー環境を自動構築する仕組みを作った - Sweet Escape