午前2時14分のPagerDutyインシデント
それは火曜日の早朝、ナイトスタンドの上でスマートフォンが激しく振動し始めた時のことでした。またPagerDutyです。エラーログは簡潔でした。ERROR: could not create unique index "idx_users_email"。ローカル環境では難なく通り、すべてのCI/CDチェックもパスしたはずのルーチン的なマイグレーションが、本番環境のAPIを麻痺させていたのです。
私はその後の90分間、スキーマ変更の手動ロールバック and 破損した状態の修復に追われました。何が悪かったのでしょうか? 本番環境には、大文字と小文字が混在する重複したメールアドレス(’[email protected]’ と ‘[email protected]’ など)を含む45万件のデータが存在していました。一方、数ヶ月前に手動で作成したわずか50MBのスナップショットである私のローカル開発用データベースは、完全にクリーンな状態でした。つまり、現実を反映していなかったのです。
これはバックエンドエンジニアにとって繰り返される悪夢です。私たちはコードをGitのように扱い、自信を持ってブランチを作成しマージしますが、データベースについては、壊れやすく静的なモノリスのように扱っています。開発データが本番環境を反映していない場合、すべてのデプロイはハイリスクな賭けになってしまいます。
なぜローカルデータベースは失敗するのか
デプロイの失敗は、構文の間違いから起こることは稀です。多くの場合、「データギャップ」が原因で発生します。ほとんどのチームは、次の3つの罠のいずれかに陥っています。
- 「空のシェル」: 空のPostgresインスタンスに対してテストを行うケース。基本的なSQLエラーは検出できますが、大規模環境でしか発生しないパフォーマンスのボトルネックや制約違反を見逃します。
- 「古いダンプ」: 月に一度、本番環境のバックアップをダウンロードするケース。3日も経てばスキーマは古くなります。さらに悪いことに、コンプライアンスを維持するためにPII(個人を特定できる情報)を削除する作業に4時間も費やす必要があります。
- 「共有ステージング」の混乱: 5人の開発者が1つのステージングデータベースを共有するケース。ある機能のために破壊的な
ALTER TABLEを実行すると、他の全員の環境を壊してしまいます。
コードをブランチするように、データも簡単にブランチできる必要があります。Neonのアーキテクチャは、これを可能にします。
解決策:Copy-on-Write(コピーオンライト)ブランチ
Neonは、ストレージとコンピューティングを分離したサーバーレスPostgreSQLプラットフォームです。「Copy-on-Write」ブランチをサポートする、カスタム構築されたストレージエンジンを使用しています。500GB의 データベースをブランチする際、Neonは実際に500GBのデータをコピーするわけではありません。代わりに、特定のLSN(Log Sequence Number)におけるスナップショットを作成します。
これにより、本番データの100%を保持する隔離されたエンドポイントを即座に取得できます。ブランチ内で行われた変更はその中に留まります。親ストレージに影響を与えることはなく、本番環境のユーザーへのパフォーマンス低下も一切ありません。
Neon CLIを使い始める
手動のスナップショットから脱却するために、まずはNeonユーティリティをインストールしましょう。これがターミナルから環境を管理する最速の方法です。
npm install -g neonctl
neonctl auth
インスタンスなサンドボックスを起動する
リスクのあるマイグレーションを実行する前に、main データベースのブランチを作成します。このクローンには、本番環境のすべてのテーブル、インデックス、行が含まれています。
# 'main' から 'migration-test' という名前のブランチを作成
neonctl branches create --name migration-test --parent main
約3秒以内に、一意の接続文字列が発行されます。これは本番環境の正確なレプリカです。まずここでマイグレーションスクリプトを実行してください。もしマイグレーションでデータ競合が発生したり、完了までに10分かかったりしても、ライブサーバーではなく安全なサンドボックス内で問題を発見できたことになります。
外部データのインポート処理
システムが新しい外部データをどのように処理するかテストする必要がある場合もあります。乱雑なスプレッドシートを扱う場合は、toolcraft.app/ja/tools/data/csv-to-json を使って入力を準備しています。これはすべてブラウザ内で処理されるため、新しいデータベースブランチ用のシードファイルを生成している間、機密データがマシン外に出ることはありません。
ワークフローへのブランチ統合
このシステムが真価を発揮するのは、自動化した時です。午前2時の呼び出しを防ぐために、私がどのようにワークフローを再構築したかを紹介します。
1. PRごとのエフェメラル(一時的)な環境
単一のステージングサーバーを使うのはやめましょう。GitHub Actionsを設定して、プルリクエスト(PR)ごとにNeonブランチをトリガーするようにします。これにより、すべての開発者がテスト用にプライベートで本番グレードのデータ環境を持つことができます。
# GitHub Actionsのスニペット(概念図)
- name: Neonブランチを作成
run: |
BRANCH_NAME="pr-${{ github.event.number }}"
neonctl branches create --name $BRANCH_NAME
DATABASE_URL=$(neonctl connection-string $BRANCH_NAME)
echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_ENV
2. 破壊的な変更を安全にテストする
カラムを削除したり、データ型を INT から BIGINT に変更したりする必要がありますか? 従来のデータベースでは、これは恐ろしい作業です。Neonなら、ブランチを立ち上げ、DROP COLUMN を実行し、フルインテグレーションテストスイートを実行するだけです。もしアプリがクラッシュしたら、単にそのブランチを削除して別の方法を試せばいいのです。
# テスト後のクリーンアップ
neonctl branches delete migration-test
3. 数秒で状態をリセットする
数千行を修正するロジックのテストは、その後に状態を「リセット」しなければならないため困難です。Neonでは、複雑なクリーンアップスクリプトは不要です。ブランチを削除して再作成するだけです。本質的に、データベースをステートレスで使い捨て可能なリソースとして扱うことができます。
比較
Neonのブランチ機能は、DockerでPostgresを実行するよりも優れているでしょうか? 通常は「はい」と言えます。
| 機能 | ローカルDocker | Neonブランチ |
|---|---|---|
| データ量 | ノートPCのSSDによる制限 | 数テラバイトの本番スケール |
| セットアップ時間 | 数分(ダンプの復元) | 5秒未満(Copy-on-Write) |
| データの鮮度 | 常に古い | リアルタイムの本番クローン |
| 分離性 | 完全 | 完全 |
最後に
インフラストラクチャは「目に見えないもの」になりつつあります。VercelやAWS Lambdaのようなサーバーレスコンピューティングですでに見てきた流れです。データベースブランチは、その論理的な次のステップです。データをブランチして破棄できるものとして扱うことで、「本番環境へのマイグレーション」という恐怖を排除できます。
次に複雑なスキーマ変更に直面したときは、クリーンに加工されたローカル環境に頼らないでください。ブランチで実際のデータに立ち向かいましょう。ユーザーに触れる前に、本物のデータに対してマイグレーションがすでに成功したと分かっていれば、ずっと安らかに眠れるはずです。

