1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PR: JISOU | Reactプログラミングコーチング
AIで不安を感じたら!    JISOUメンバー募集中

Vercel × Neon Preview Branching でPRごとのプレビュー環境を作る

1
Posted at

はじめに

Neon と Vercel の統合機能は非常に便利です。
環境変数を自動で注入してくれますし、GitHub の PR から自動作成された Preview 環境に合わせて、Neon 側でも使い捨てのブランチを作成してくれたりします。

しかし、Neon の Vercel Integration を入れただけの状態では、Production と Preview の両方に本番環境 ( main ブランチ)の DATABASE_URL が注入されます。Preview デプロイが本番 DB を参照している状態です。

これには二つの問題があります。

一つは、Preview デプロイが本番 DB への書き込み権限を持ってしまうことです。管理画面をデプロイしている場合、Preview から本番データを直接編集できてしまいます。

もう一つは、スキーマを変更する PR の Preview が必ず失敗することです(理由は後述します)。これは独立した問題ではなく、Preview が本番と同じスキーマ、つまり常に「今の本番の状態」を参照し続ける以上、避けられない帰結です。

この記事では、PR ごとに「本番と同じデータ + その PR のスキーマ」で動くプレビュー環境を用意する構成をまとめます。

Preview の失敗が「必ず」起きる理由

Drizzle の insert().values() は、スキーマ定義上の全列を INSERT 文に並べます(未指定の列は default)。値を渡していない新しい列でも文に載るため、列をスキーマに追加した時点で、その列が存在しない DB への insert はすべて失敗します。

NeonDbError: column "ekidata_company_cd" of relation "operators" does not exist
  code: '42703'

Preview が本番 DB を参照している限り、これは構造的に避けられません。

ここで重要なのは、書き込み権限とスキーマの古さを同時に直す必要があるという点です。書き込み権限だけを塞いでも、preview ブランチは main の CoW コピーなのでスキーマは本番のまま古く、この失敗は再発します。逆にスキーマの失敗だけを直そうとして本番に対して直接 migrate してしまうと、さらに危険です。

完成形

環境 Neon ブランチ 生成方法 寿命
Production main 恒久
Preview(Git ブランチごと) preview/<git-branch> main から CoW で自動生成 PR クローズ時に削除

image.png

git-flow の場合、develop への Preview デプロイが preview/develop を作り、これが実質のステージング環境になります。

手順

1. Preview Branching を有効化する(Vercel ダッシュボード)

Storage → 対象 DB → Projects → 対象 Project の → Update Project Connection

  • Preview をオンにします
  • Require Active Resource Before Deploy もオンにします(ブランチ生成の完了を Vercel が待ちます)

2. Build Command にマイグレーションを組み込む(Vercel ダッシュボード)

Settings → Build and Deployment → Build Command の Override をオン

pnpm -w run db:migrate && turbo run build

pnpm のモノレポ(workspace)構成では -w が必要
Vercel は Build Command を Root Directory(例:apps/web)をカレントディレクトリとして実行します。
db:migrate はワークスペースルートの package.json にしか定義されていないため、-w で明示的にルートを指します。素の turbo run build が動作するのは、turbo が Root Directory からフィルタを自動推論するためで、これも cwd が Root Directory であることと整合しています。

workspace を組んでいない場合はこの限りではありません。package.json が1つしか無く Root Directory もそれと一致するため、単に pnpm run db:migrate && next build のように書けば十分です。

ポイントとして、DB へのマイグレーションは turbo run build の外に置きます。 副作用のある操作を turbo のキャッシュ対象に入れないためです。

この配置の要点は、環境ごとの分岐をどこにも書かずに適用先が正しくなることです。Vercel が注入する接続先に従うだけで、Preview は preview ブランチ、Production は main に適用されます。

副次的な効果として、これは「マイグレーションを適用する経路がリポジトリにも CI にも無い」という状態そのものを解消します。push のたびに db:migrate が走るため、適用忘れはこの時点でビルド失敗として必ず表面化し、後から本番デプロイの失敗として気づくということがなくなります。

3. マイグレーションは直結(非プール)エンドポイントで流す

Vercel が注入する DATABASE_URL は PgBouncer 経由のプール接続です。
Neon の Pooler はトランザクションモード固定で、SET やセッションレベルのアドバイザリロックが使えません。
Neonの公式ドキュメント でも、スキーママイグレーションには直結接続を使うことが推奨されています。

// drizzle.config.ts
const connectionString =
  process.env.MIGRATION_DATABASE_URL ??
  process.env.DATABASE_URL_UNPOOLED ??  // ← Vercel Integration が注入する直結用
  process.env.DATABASE_URL;             // ← これしか無い場合のフォールバック

if (!connectionString) {
  throw new Error('No migration connection string is defined.');
}

4. ビルドから DB への書き込みを外す

build が seed などで DB に書き込んでいると、Preview ビルドが本番データに触れます。

- "build": "pnpm --filter scripts seed && next build",
+ "build": "next build",

preview ブランチは main の CoW コピーで最初からデータが入っているため、「デプロイ先が空かもしれない」という seed の存在理由そのものが消えます。seed は、空の DB を立ち上げた直後の手動操作に戻します。

5. preview ブランチの掃除(GitHub Actions)

name: Cleanup Neon preview branch

on:
  pull_request:
    types: [closed]

jobs:
  delete-branch:
    runs-on: ubuntu-latest
    steps:
      - uses: neondatabase/delete-branch-action@v3
        with:
          project_id: ${{ vars.NEON_PROJECT_ID }}
          branch: preview/${{ github.head_ref }}
          api_key: ${{ secrets.NEON_API_KEY }}

Vercel 側に自動削除機能がありますが、Vercel-Managed Integration の preview ブランチ削除は、Git ブランチの削除ではなく Vercel のデプロイ削除に連動します。既定のデプロイ保持期間は180日です。一方、Neon 無料プランのブランチ上限は10本なので、先に上限へ到達します。

そのため、GitHub Actions で PR が close されたら、DBのブランチも自動で削除されるようにします。

NEON_API_KEY は、Organization の Settings (https://console.neon.tech/app/{org-id}/settings#api-keys) から作成できます。
※ プロジェクトの Settings ではないので注意。

なお、preview/develop のように「PR の head にならないブランチ」は自動削除されないため、手動で削除する必要があります。

モノレポで Vercel プロジェクトが複数あるとき

同一リポジトリに複数のNext.jsアプリがあり、それぞれ別々の Vercel プロジェクトとしてデプロイし、同じ Neon プロジェクトにつなぐ場合の挙動です。

preview ブランチは共有される

同じ Git ブランチに対する preview/<git-branch> は、両プロジェクトで1本を共有します。プロジェクトごとに別ブランチができることはありません。

実測として、2つの Vercel プロジェクトを接続した状態で3つの Git ブランチにデプロイし、Neon 上の preview ブランチが preview/develop / preview/feature-a / preview/fix-b の3本(6本ではない)であることを確認しています。

db:migrate は1プロジェクトにだけ入れる

共有されるということは、両方の Build Command に db:migrate を入れると、1回の push で2つのビルドが同時に同じブランチへマイグレーションを流すということです。drizzle-kit はジャーナルで冪等ですが、同時実行は別問題で、後発の CREATE TABLEalready exists で落ちてビルドが失敗します。データは壊れませんが、毎回どちらかが失敗します。

「所有者側のビルドがスキップされる」ことは起きない

Vercel の Skipping unaffected projects は既定(Automatic)で有効で、変更が影響しないプロジェクトのビルドはスキップされます。ただし判定条件は以下です。

  1. プロジェクトのソースコードが変更された
  2. プロジェクトの内部依存が変更された
  3. ロックファイルの変更がそのプロジェクトの依存にのみ影響する

マイグレーションは必ず DB パッケージ(スキーマ定義とマイグレーションファイル)を変更します。そのパッケージを全アプリが package.json で明示的に依存しているなら、未適用のマイグレーションがある push では所有者プロジェクトが必ずビルドされます。つまりこの保証は、依存グラフによって成立しています。

逆向き(所有者だけビルドされ他方がスキップ)は起こりえますが、それは「稼働中の旧デプロイが新しいスキーマに直面する」ケースと同じで、追加のみの変更なら影響しません。

【注意点】破壊的マイグレーションは二段階デプロイにする

ビルド時に migrate が走るということは、新しいコードが公開される前に DB が変わるということです。Build Command 内では db:migratenext build より先に走るため、マイグレーションが完了してから新しいデプロイが本番トラフィックに切り替わるまで、残りのビルド時間分のタイムラグが生まれます。この間は、稼働中の旧デプロイ(まだ古いコードのまま)が新しいスキーマに対して動くことになります。

破壊的マイグレーションの時系列図

変更の種類 旧デプロイへの影響 手順
ADD COLUMN / CREATE TABLE / 制約追加 無し 1回のデプロイでよい
DROP COLUMN / DROP TABLE / NOT NULL 即座に壊れる 二段階に分ける

そのため、下記のように破壊的マイグレーションに対しては二段階で変更を適用する必要があります。

  1. その列を読まないコードをデプロイする
  2. 次のデプロイでマイグレーションが列を落とす

まとめ

  • Neon の Vercel Integration を入れただけでは、Preview に本番の DATABASE_URL が注入される。Preview Branching は別途有効化が必要
  • Drizzle の insert はスキーマ上の全列を文に載せるため、Preview が本番 DB を見ている限り、列追加 PR の Preview は必ず失敗する
  • 「Preview の書き込み権限」と「Preview のスキーマずれ」は同時に直す必要がある
  • Build Command に pnpm -w run db:migrate && turbo run build を置くと、環境ごとの分岐なしで適用先が正しくなる
  • マイグレーションはプール接続ではなく直結エンドポイント(DATABASE_URL_UNPOOLED)で流す
  • seed をビルドから外す。preview ブランチは最初からデータが入っているので seed の理由が消える
  • preview ブランチの掃除は GitHub Actions で行う。Vercel 側の自動削除(180日)は Neon 無料プランの上限10本に間に合わない
  • 複数 Vercel プロジェクトが同じ Neon プロジェクトを使う場合、preview ブランチは共有される。db:migrate は1プロジェクトにだけ入れる
  • 破壊的マイグレーション(DROP / NOT NULL 化)は二段階デプロイにする

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?