データベーススキーマにもバージョン管理が必要な理由
想像してみてください。あなたのチームはアプリケーションコードに対して堅牢なCI/CDパイプラインを構築しています。すべてのプルリクエストは自動テストをトリガーし、コンテナイメージをビルドし、ステージング環境にデプロイします。そして本番デプロイの番が来ました。パイプラインが実行され、アプリケーションが起動した瞬間、column-not-foundエラーでクラッシュします。誰かがphone_numberカラムを追加するデータベースマイグレーションを実行し忘れたのです。デプロイは失敗し、ユーザーはエラーを目にし、チームは何が悪かったのかを必死に解明しようとします。
このシナリオは、毎日のように多くのチームで発生しています。アプリケーションコードはバージョン管理され、テストされ、パイプラインを通じてデプロイされます。しかし、データベーススキーマの変更は後回しにされ、誰かがデプロイの前後に手動で実行するだけのものとして扱われています。コードデプロイとスキーマ変更の間の断絶が、エラーがすり抜ける隙間を作り出しています。
問題:パイプラインは何が実行済みかをどうやって知るのか?
V001_create_users.sql、V002_add_phone.sql、V003_add_index.sqlのようなマイグレーションスクリプトのディレクトリがある場合、パイプラインはどのスクリプトがデータベースにすでに適用されているかを把握する必要があります。デプロイのたびに最初からすべてのファイルを実行することはできません。本番データベースには実際のデータがすでに存在します。V001を再実行すると、テーブルがすでに存在するために失敗するか、さらに悪いことにテーブルを削除して再作成し、顧客データを破壊してしまう可能性があります。
追跡メカニズムがないと、チームは手動チェックに頼らざるを得ません。誰かがデータベースにログインし、\dtやSHOW TABLESを実行し、先週何をデプロイしたかを思い出そうとします。あるいは、誰も更新しない共有スプレッドシートに依存します。あるいは、マイグレーションを実行して、うまくいくことを祈るだけです。
これらのアプローチはどれもスケールしません。ヒューマンエラーを招き、デプロイを遅らせ、データベース変更のたびに恐怖を生み出します。
解決策:データベース内のマイグレーションテーブル
答えは驚くほどシンプルです。データベース自身にマイグレーション履歴を追跡させるのです。schema_migrationsやmigration_historyとよく名付けられる特別なテーブルを作成し、実行されたすべてのマイグレーションスクリプトを記録します。
実際の動作は以下の通りです。
- マイグレーションツールが空のデータベースに対して初めて実行されると、マイグレーションテーブルを作成します。
- 各マイグレーションスクリプトが正常に実行された後、ツールはスクリプト名と実行タイムスタンプを含む行を挿入します。
- その後のデプロイでは、パイプラインがマイグレーションテーブルを読み取り、利用可能なマイグレーションファイルのリストと比較し、まだ記録されていないスクリプトのみを実行します。
例えば、V001_create_users.sqlが実行された後、マイグレーションテーブルにはV001_create_users.sqlという1行が含まれます。次のデプロイにV002_add_phone.sqlが含まれている場合、パイプラインはテーブルを確認し、V002がないことを検出して実行します。成功後、新しい行が追加されます。データベース自体が、現在実行中のスキーマのバージョンに関する唯一の信頼できる情報源となります。
これがパイプラインにとって重要な理由
このメカニズムはバージョンロックと呼ばれます。データベースは自身の状態に関する信頼できるレコードを保持します。別の設定ファイルや、同期がずれる可能性のある環境変数、誰かが更新を忘れる手動チェックリストは必要ありません。
CI/CDパイプラインにとって、これは極めて重要です。パイプラインは「データベースが教えてくれる情報に基づいて、これら3つのマイグレーションファイルを実行する必要がある」という客観的な決定を下せるようになります。推測も、手動チェックも、同じマイグレーションを二度実行する恐れもありません。
以下のシーケンス図は、この正確なフローを示しています。
さまざまなマイグレーションツールがこれを少しずつ異なる方法で実装しています。すでに実行されたスクリプトが変更されたかを検出するために、各マイグレーションファイルのチェックサムを記録するものもあります。ファイル名の代わりに連続したバージョン番号を使用するものもあります。マイグレーション履歴を別のスキーマやデータベースに保存するツールもあります。しかし、核となる原則は同じです。データベースが自身の履歴を追跡し、パイプラインがその履歴を読み取って次のステップを決定するのです。
ブートストラップ:最初のマイグレーション
ここには鶏が先か、卵が先かという問題があります。マイグレーションテーブル自体が、他のマイグレーションを記録する前に存在している必要があります。どうやって作成するのでしょうか?
ほとんどのマイグレーションツールはこれを自動的に処理します。空のデータベースに対してツールを初めて実行すると、ブートストラッププロセスの一部としてマイグレーションテーブルを作成します。ツールによっては、このブートストラップアクションをマイグレーション履歴の最初のエントリとして記録するものもあります。
すでにテーブルとデータが存在する既存のデータベースにマイグレーションスクリプトを導入する場合は、別のアプローチが必要です。ここでベースラインマイグレーションが登場します。過去のすべての変更を再現しようとする代わりに、データベーススキーマの現在の状態をキャプチャする単一のマイグレーションスクリプトを作成します。これをベースラインとしてマークし、マイグレーションツールはそれがすでに適用済みであると記録します。その時点以降は、将来の変更に対してのみ新しいマイグレーションスクリプトを追加します。
ベースラインマイグレーションは現実的な解決策です。過去を書き換えることはできないが、今日から変更を追跡し始めることはできる、ということを認めています。代替案は、これまでに行われたすべてのスキーマ変更をリバースエンジニアリングすることですが、これはほとんどのチームにとって非現実的です。
マイグレーションテーブルが解決しないこと
マイグレーションテーブルは、どのスクリプトがすでに実行されたかを知るという特定の問題を解決します。パイプラインに現在のスキーマバージョンを確実に判断し、保留中の変更を適用する方法を提供します。
しかし、すべてを解決するわけではありません。マイグレーションテーブルは、テーブル、カラム、インデックスの追加のような追加的な変更には適しています。これらの変更は、古いスキーマ用に書かれた既存のアプリケーションコードを壊しません。問題が始まるのは、カラムの削除やリネーム、データ型の変更、テーブルの再構成が必要な場合です。これらの破壊的または変換的な変更は、実行中のアプリケーションを壊したり、ダウンタイムを引き起こしたり、データを破損させたりする可能性があります。
マイグレーションテーブルは何が実行されたかを教えてくれますが、実行中のアプリケーションが新しいスキーマと互換性があるかどうかは教えてくれません。それには、後方互換性のあるマイグレーション、段階的なロールアウト、コードデプロイとスキーマ変更の間の慎重な調整に関する、別の一連のプラクティスが必要です。
マイグレーション追跡のための実践的チェックリスト
- マイグレーションテーブルによる自動追跡をサポートするマイグレーションツールを選択する。
- マイグレーションテーブルが最初のデプロイ時に作成され、手動ではないことを確認する。
- 本番環境にすでに適用されたマイグレーションスクリプトは決して変更しない。
- 既存のデータベースにマイグレーションを導入する場合は、最初にベースラインマイグレーションを作成する。
- マイグレーションの実行をデプロイパイプラインのステップとして含め、手動プロセスにしない。
- 本番データ量を反映したステージング環境でマイグレーションをテストする。
具体的な takeaways
データベーススキーマはアプリケーションコードと同じくらい重要であり、同じレベルのバージョン管理と自動化に値します。マイグレーションテーブルは、パイプラインに何が適用され、何をまだ実行する必要があるかをデータベースに基づいて確実に知る方法を提供します。これがなければ、推測に頼ることになります。これがあれば、データベース変更に関連するデプロイ失敗の最も一般的な原因を排除する、唯一の信頼できる情報源が得られます。今日からスキーマバージョンの追跡を始めれば、次のデプロイの際に過去の自分に感謝されるでしょう。