PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

Pull Requestごとにバックエンドまで動くプレビュー環境を自動化した話を読み解く

キーポイント

まず何が困るのか

この記事の面白いところは、かなり現実的な悩みから始まっている点です。

フロントエンドはVercelを使えば、PRを作っただけでプレビューURLが自動で生えます。これは本当に便利で、「見た目の確認」はほぼノーコストです。
でも、アプリがフロントとバックエンドで分かれていると話は終わりません。APIの仕様変更やDBの変更が入ると、フロントだけ見ても意味がないからです。

しかもこのケースでは、PRが1本だけじゃなく複数同時に走ることが多い。
生成AIを活用してタスクを並列で進めるようになった結果、レビュー対象のPRも並ぶ。すると「PR Aの変更はAの環境で確認したい」「PR BはBの環境で確認したい」が普通に発生します。

ここ、かなり重要です。
“dev環境をみんなで共有する” 方式では、並行レビューに耐えないんですよね。
個人的にも、ここを雑にすると「たまたま別PRの影響を受けたバグ」が混ざって、レビューの信頼性が一気に落ちると思います。

採用した方針は「バックエンドはPRごとに分離、DBはスキーマで分ける」

設計候補は3つありました。

  1. PRごとに独立したFargateタスクを立てるが、DBはdev環境のAuroraを共有する
  2. dev環境のFargateを一時的にPRのイメージに差し替える
  3. PRごとにFargateもAuroraも完全に独立させる

ここで2は、複数PRを同時に確認したいという要件に合わないので却下。
3は分離としてはきれいですが、コストも運用負荷も重すぎる。プレビュー用途としてはやりすぎ、という判断です。

最終的に1を採用しています。
これはかなり現実的です。​​「ちゃんと分けたい。でも分けすぎると死ぬ」問題への、ちょうどいい着地だと思います。

DB分離はPostgreSQLのschemaで実現

ここが技術的に気持ちいいポイントです。

PostgreSQLには「schema」という仕組みがあります。ざっくり言うと、​1つのDBの中に、独立した名前空間をいくつも作れる感じです。
たとえば pr_123 というschemaを作って、PR番号ごとにそこへデータを閉じ込める。

しかもPrismaでは接続URLに ?schema=pr_123 のような形で指定できるので、​アプリ本体のコードをいじらずに接続先だけ切り替えられるのが強い。これはかなり賢いです。

この方式なら、

という流れで、DBをきれいに分離できます。

個人的には、ここは「フル分離より軽く、共有より安全」というかなり良いバランスだと思います。
プレビュー環境に求めるのは“完璧な隔離”より“実用的な隔離”ですからね。

フロントとバックエンドの接続もPR単位

フロントエンド側はVercelのプレビュー環境を使いますが、そこからPR固有のバックエンドに向ける必要があります。

そのために、Vercelの環境変数 NEXT_PUBLIC_BACKEND_API_URL に、PRごとのURLを入れる方式にしています。
URLの形は pr-{PR番号}.preview.example.com

ここで使っているのが、

です。

ざっくり説明

これによって、PRごとにDNSレコードを毎回作らなくてよくなっています。
地味ですが、こういう「毎回の手作業を消す」設計が、運用の快適さを決めます。

全体は3本のGitHub Actionsで回す

この記事の肝は、単に「デプロイできた」ではなく、​作成から削除までのライフサイクルを自動化しているところです。

image_0007.svg

流れはこうです。

この4段構え、かなり堅いです。
特に最後の「毎日深夜のゴミ掃除」は、実務では本当に大事。
人間は削除を忘れます。断言してもいい。だからセーフティネットとして定期クリーンアップを入れるのは正しいです。

preview-deploy.yml の流れがかなり丁寧

構築ワークフローは、PR番号を起点にすべての資源名を決めるところから始まります。

たとえばPR #123なら、

という具合です。

これ、地味だけどめちゃくちゃ重要です。
名前のルールが一意だと、作るときも消すときも迷わないんですよね。
運用で一番怖いのは「これ何のリソースだっけ?」になることなので。

Step 1でVercel環境変数を先に設定するのがポイント

ここは記事の中でもハマりどころとして紹介されていました。

最初は「バックエンドができてからVercelの環境変数を設定すればいい」と考えていたそうです。
でもNext.jsの NEXT_PUBLIC_*ビルド時に埋め込まれるので、Vercelがプレビュービルドを始める前に環境変数を入れておかないといけない。

つまり順番は逆で、

  1. 先にVercelへ環境変数を入れる
  2. そのあとバックエンドを立てる
  3. 最後にVercelを再デプロイする

という流れになります。

これはかなり「やられがち」なポイントです。
理屈としては単純なんだけど、実装してみないと気づきにくい。
こういう地雷があるから、CI/CDは机上の設計だけでは終わらないんですよね。

Step 3.5 の DB操作がきれい

サービスを立てる前に、

を順番に走らせています。

ここで前回の記事で使った entrypoint.shDB_COMMAND を再利用していて、​本番デプロイと同じ仕組みでDBを触るのが印象的でした。
仕組みを増やすと壊れやすくなるので、「同じやり方を流用する」のはかなり良い設計です。

Step 4 のALBルールの優先度が面白い

ALBのホストルールには優先度が必要なのですが、ここにPR番号をそのまま足すと、将来的に上限を超える可能性がある。
そこで PR番号 % 49000 + 1 のように剰余で決めたそうです。

ここ、実務っぽくて好きです。
きれいな理屈ではなく、​​「上限があるなら現実的に収まるように工夫する」​という発想。
しかも「49000本のPRが同時に開かれることはまあないだろう」と割り切っているのも、人間味があります。

Step 6でVercelを再デプロイしてやっと本番っぽくなる

バックエンドが立った後、Vercel Deployments APIで再デプロイします。
この時点でようやく、フロントからバックまでつながったプレビューになります。

つまりこの仕組みは、

のが本質です。
ここができると、PRレビューの質がかなり上がるはずです。

Step 7でPRコメントにURLを貼る

最後にPRコメントへプレビューURLを投稿します。
これ、派手ではないけど本当に便利。
レビューする側はPRを開いてURLをクリックするだけで済むので、確認のハードルがかなり下がります。

同じPRに連続pushされたときのための concurrency 制御

同じPRに対して短時間に複数回pushされることは普通にあります。
そのたびにデプロイ処理が並行すると、名前衝突や二重作成が起きて面倒です。

image_0008.svg

そこで preview-{PR番号} という concurrency グループを使い、​同じPRの処理は同時に走らないようにしています。
古いジョブはキャンセルされ、最新のpushだけが残る。

これはすごく合理的です。
プレビュー環境は「途中経過」を見せるものではなく、「最新状態」を見せれば十分だからです。

削除処理はむしろ作成より大事かもしれない

preview-cleanup.yml はPRクローズ時に走ります。
削除の順番もかなり重要で、

  1. ECSサービスを止める
  2. タスク定義を解除する
  3. ALBルールとターゲットグループを消す
  4. PostgreSQLのschemaを削除する
  5. ECRイメージを消す
  6. Vercelの環境変数を消す

という順番です。

そして各コマンドに || true を付けて、​途中で1つ失敗しても全部止まらないようにしています。
これは運用ではかなり大事です。
「全部成功する前提」で書くと、1個失敗した瞬間に後続が止まってゴミが残りがちなんですよね。

毎日深夜のゴミ掃除が保険として効く

preview-scheduled-cleanup.yml では、GitHub APIでオープンPR一覧を取り、存在しないPRのリソースを探して削除しています。

これは、PRクローズ時のcleanupが失敗した場合の保険です。
正直、こういう保険があると運用の気持ちがかなり楽になります。
「消し忘れたら毎日誰かが代わりに見つけてくれる」というだけで、課金の不安が減るからです。

Terraformは“土台”だけを管理する

プレビュー用のFargateサービスそのものはGitHub Actionsから動的に作成する一方で、土台となるものはTerraformで管理しています。

具体的には、

などです。

この分け方はとても筋がいいです。
動的に増えるものはCI/CD、固定の基盤はTerraform
この線引きが曖昧だと、あとでどこが真実の管理元なのか分からなくなります。インフラ運用でこれは地味に致命傷です。

個人的に「ここが良い」と思った点

この記事全体を読んで、特に良いと感じたのは次の3つです。

1. “作る”より“消す”をちゃんと設計している

プレビュー環境は増やすのは簡単、消すのは難しい。
ここを最初からライフサイクル全体で設計しているのがえらいです。

2. DBをスキーマ分離にした判断が実務的

完全分離はきれいだけど重い。
共有だけだと危ない。
その中間としてschema分離を選んだのは、かなり現実を見ていると思います。

3. Next.js + Vercelの落とし穴をちゃんと踏んでいる

NEXT_PUBLIC_* のビルド時埋め込みは、知っていれば当たり前でも、実装中にうっかり忘れやすい。
こういう「理屈では理解してたけど運用では刺さる」話が、記事としてすごく価値あると思います。

まとめ

この記事は、単なる「PRごとにプレビュー環境を作った話」ではありません。
実際には、

まで含めた、かなり本気のCI/CD設計の話です。

特に印象的なのは、
“理想的な完全分離”より、“運用できる現実的な分離”を選んでいるところ。
このバランス感覚は、実務ではかなり強い武器だと思います。


参考: Pull Requestごとにバックエンドも含めたプレビュー環境を自動構築する仕組みを作った - Sweet Escape

同じ著者の記事