本番投入前にデータベースマイグレーションをテストする

マイグレーションスクリプトを書いたとします。見た目は正しく、構文も妥当で、ロジックも問題なさそうです。ローカルデータベースで実行すると正常に動作します。ところが本番環境にデプロイすると、すべてが壊れます。

テーブルには数百万行のデータがありました。あなたのローカルデータベースには12行しかありませんでした。マイグレーションはNOT NULLカラムを追加しようとしましたが、本番環境には知らされていないNULL値が既に存在していました。ALTER TABLEがピークトラフィック時に15分間書き込みをロックしました。

このシナリオは珍しくありません。マイグレーションをテストする環境が、実際に実行される環境と一致していないために発生します。空のデータベースや不一致のデータベースでパスしたマイグレーションは、誤った安心感を与えます。修正方法は、ローカルマシンでより注意深くテストすることではありません。本番環境の現実を反映したテスト環境を構築することです。

スキーマの一致が重要な理由

マイグレーションスクリプトは前提条件に基づいて書かれています。特定のカラムが存在すること、特定のインデックスが存在すること、特定の制約が設定されていることを前提としています。テストデータベースのスキーマが異なる場合、異なる前提条件に対してテストしていることになります。

NOT NULLカラムを追加するマイグレーションを考えてみましょう。空のテーブルを持つテストデータベースでは、マイグレーションは瞬時に実行されます。本番環境では、そのカラムにNULL値を持つ行が存在します。マイグレーションは失敗し、本番インシデントが発生します。

スキーマを一致させる最も信頼性の高い方法は、本番環境からスキーマダンプを取得し、テスト環境にリストアすることです。これにより、本番環境に存在する正確なテーブル構造、インデックス、制約、データ型が得られます。テストスキーマが現実と一致しているかどうかを推測する必要はもうありません。

以下はPostgreSQLを使用した具体例です:

# 本番環境からスキーマのみ(データなし)をダンプ
pg_dump --schema-only --no-owner --no-acl production_db > schema.sql

# スキーマをテストデータベースにリストア
psql test_db < schema.sql

実際の問題を露呈させるデータ

空のテーブルやランダムなテストデータではエッジケースを発見できません。マイグレーションの失敗は、本番環境には存在するがテストセットにはないデータが原因で発生することがよくあります。

マイグレーションがカラム型を変更する場合、テストデータにはエッジ値を含める必要があります:長い文字列、多数の桁を持つ小数、そのカラムのNULL値などです。マイグレーションが一意制約を追加する場合、テストデータにはその制約に違反する重複行を含める必要があります。マイグレーションがカラムを削除する場合、テストデータにはそのカラムを参照するクエリを含める必要があります。

本番データセット全体をコピーする必要はありません。数百万行のテーブルの場合、適切に選択された数千行で十分にデータベースエンジンが現実的な実行計画を生成できます。目標は本番のボリュームを再現することではありません。目標は、マイグレーションの動作を変える可能性のあるデータパターンを再現することです。

セーフティネットとしてのドライラン

ドライランは、データベースを恒久的に変更せずにマイグレーションSQLを実行します。一部のマイグレーションツールには組み込みのドライランモードがあります。ない場合は、マイグレーションをトランザクションでラップし、最後にロールバックできます。

ドライランの目的は、マイグレーションが成功することを確認することではありません。警告、エラー、実行計画の変更を確認し、問題を示唆する可能性のあるものを特定することです。小さなテーブルでは正常に実行されるマイグレーションでも、現実的なスキーマに対して実行するとフルテーブルスキャンの警告が表示されることがあります。大規模なテーブルに対するALTER TABLEは、メンテナンスウィンドウに許容できない推定実行時間を示すことがあります。

ドライラン後、出力を手動または自動チェックでレビューします。長期間テーブルをロックする可能性のある操作、パフォーマンスを低下させる可能性のあるクエリ、ローカルテストでは現れなかった予期しない動作を探します。

マイグレーション中の軽負荷シミュレーション

一部のマイグレーションはアイドル状態のデータベースでは安全でも、実際のトラフィック下では問題を引き起こします。ロックを取得するALTER TABLEは、誰もテーブルを使用していなければ数秒で完了します。本番負荷下では、同じロックがクエリのタイムアウトやアプリケーションエラーを引き起こす可能性があります。

完全な負荷テストは必要ありません。マイグレーション中に対象テーブルに対してSELECTとINSERTクエリを実行する簡単なスクリプトで十分です。シミュレートされたクエリが失敗したりタイムアウトしたりした場合、本番ユーザーに影響を与える問題を発見したことになります。

このシミュレーションをドライランフェーズ中に実行します。軽負荷下でマイグレーションがデッドロックや過剰な待機を引き起こす場合、アプローチを再検討する必要があります。より影響の少ないロッキング戦略を使用する必要があるかもしれません。マイグレーションをより小さなステップに分割する必要があるかもしれません。トラフィックの少ない時間帯にスケジュールする必要があるかもしれません。

テスト環境の自動化

毎回のマイグレーションのために手動でテスト環境をセットアップするのは遅く、エラーが発生しやすいです。より良いアプローチは、CIパイプラインの一部として自動化することです。

新しいマイグレーションがコミットされると、パイプラインは本番スキーマスナップショットからテスト環境を作成します。関連するテストデータをロードします。ドライランを実行します。軽負荷をシミュレートします。その後、環境を破棄します。

この自動化により、すべてのマイグレーションが一貫したベースラインに対してテストされることが保証されます。急いでいるからといってテストをスキップすることはできません。異なるスキーマを使用したためにテストがパスしたと主張することもできません。パイプラインが毎回同じ条件を強制します。

実用的なチェックリスト

本番環境でマイグレーションを実行する前に、以下の条件が満たされていることを確認してください:

  • テストスキーマが本番スキーマと完全に一致している
  • テストデータにマイグレーションに関連するエッジケースが含まれている
  • ドライランが予期しない警告やエラーなしで完了した
  • 軽負荷シミュレーションが失敗やタイムアウトを引き起こさなかった
  • テスト環境が最新の本番スナップショットから作成された

これがパイプラインにとって意味すること

データベースマイグレーションはアプリケーションコードとは異なります。単にデプロイして観察するだけではいけません。失敗したマイグレーションはデータを破損し、テーブルをロックし、長時間のダウンタイムを引き起こす可能性があります。悪いマイグレーションのコストは、悪いアプリケーションリリースのコストよりもはるかに高くなります。

現実的な環境でのマイグレーションテストはオプションではありません。それは、マイグレーションが機能することを「知っている」ことと「願っている」ことの違いです。テスト用に構築する環境は、高価であったり複雑であったりする必要はありません。代表的なものである必要があります。

本番環境からのスキーマダンプから始めてください。対象を絞ったテストデータを追加してください。ドライランを実行してください。軽負荷をシミュレートしてください。プロセス全体を自動化してください。あなたの本番データベースが感謝するでしょう。