データベーススキーマ変更にコードと同じ規律が必要な理由
想像してみてください。チームが新しい機能をデプロイしたところです。アプリケーションコードは正常に動作しています。しかし5分後、エラーが続々と発生し始めます。新しいコードが期待するカラムがまだ存在していないのです。あるいはもっと悪いケースでは、削除されたカラムを、更新されていない古いサービスがまだクエリしているかもしれません。データベースは不整合な状態になり、誰も何が起こったのか、どうすれば素早く修正できるのかを正確に把握できません。
このシナリオは、多くのチームが認めるよりもはるかに頻繁に発生しています。根本原因はほとんどの場合同じです。データベーススキーマが、再現可能なプロセスなしに、またそれに依存するアプリケーションコードとの調整なしに、手動で変更されたことです。
コードとスキーマの根本的な違い
アプリケーションコードはステートレスです。新しいバージョンをデプロイすると、古いファイルは置き換えられます。問題が発生した場合、以前のバージョンにロールバックすれば、サーバーは既知の状態に戻ります。残存データも、隠れた依存関係もありません。
データベースはその逆です。本質的にステートフルです。すべてのテーブル、カラム、インデックス、制約には、時間をかけて蓄積されたデータが保持されています。スキーマを変更するとき、あなたは単にファイルを置き換えているわけではありません。既存のデータを保持する構造そのものを変更しているのです。新しいカラムには、既存の行に対するデフォルト値が必要かもしれません。削除されたカラムは、システムの別の部分がまだ依存しているデータを削除するかもしれません。新しいインデックスは、大きなテーブル上で構築に数分から数時間かかる可能性があります。
このステートフルな性質により、スキーマ変更はコード変更よりも本質的にリスクが高くなります。悪いデプロイは数秒でロールバックできますが、悪いマイグレーションはデータを破壊し、クエリを壊し、システム全体をダウンさせる可能性があります。そしてデータベースは複数のサービスや環境で共有されているため、影響範囲ははるかに大きくなります。
旧来の方法:手動、脆弱、再現不可能
長い間、データベースの変更は別個の手動ワークフローとして扱われてきました。DBAやシニア開発者が本番データベースサーバーにログインし、いくつかのSQLコマンドを実行して待つ、という方法です。マイグレーションが成功すれば良し、失敗すれば、何が行われたかの明確な記録もないまま、その場で修正を試みることになります。
このアプローチにはいくつかの問題があります。
- 再現不可能です。 正確な手順は、誰が実行するか、何を覚えているか、実行中に何に気づくかに依存します。同じマイグレーションでも、異なる二人が実行すれば異なる結果になる可能性があります。
- 監査不可能です。 何が、いつ、誰によって変更されたかの履歴がありません。数日後に問題が発生した場合、原因を追跡することはほぼ不可能です。
- 脆弱です。 たった一つの手順の忘れや実行順序の誤りが、データベースを不整合な状態に陥れます。復旧は手動で、プレッシャーのかかる作業になります。
- コラボレーションを阻害します。 マイグレーションを実行できるアクセス権と知識を持つのはごく一部の人だけです。チームの他のメンバーは、スキーマ変更のレビュー、テスト、貢献ができません。
チームが成長し、システムが複雑になるにつれて、この手動アプローチはボトルネックとなり、リスクとなります。スキーマ変更を伴うすべてのデプロイは、不安の高いイベントになります。
スキーマ変更をコードのように扱う
解決策は単純です。アプリケーションコードに適用するのと同じ規律でデータベーススキーマの変更を管理することです。このプラクティスはスキーママイグレーションと呼ばれ、いくつかのシンプルな原則に基づいています。
すべての変更をマイグレーションスクリプトとして記述します。 マイグレーションスクリプトは、データベーススキーマを変更するために必要なSQLコマンドを含むファイルです。カラムの追加、テーブルの作成、インデックスの追加、制約の変更などを行います。各スクリプトは1つの論理的な変更を表します。
例えば、本番環境にログインして以下を実行する代わりに:
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
次のようなマイグレーションファイルを作成します:
-- V001__add_phone.sql
-- フォワードマイグレーション
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
そして、対応するロールバックファイルも作成します:
-- V001__add_phone_rollback.sql
-- ロールバックマイグレーション
ALTER TABLE users DROP COLUMN phone;
これらのファイルはリポジトリに格納され、プルリクエストでレビューされ、デプロイパイプラインによって自動的に実行されます。手動の手順も、忘れられたコマンドも、謎もありません。
マイグレーションスクリプトをアプリケーションコードと同じリポジトリに保存します。 これにより、スキーマ変更がそれに依存するコードと一緒にバージョン管理されます。コードの特定のバージョンをチェックアウトすると、そのバージョンのスキーマを作成するために使用された正確なマイグレーションスクリプトも手に入ります。
既存のマイグレーションスクリプトは決して編集しないでください。 変更が必要な場合は、新しいスクリプトを作成します。これにより履歴がそのまま保持され、実行順序が明確になります。マイグレーションツールは通常、バージョン番号やタイムスタンプを使用して、どのスクリプトが既に実行され、どれが保留中かを判断します。
マイグレーションをデプロイパイプラインの一部として実行します。 テストの実行やアーティファクトのビルドと同様に、スキーマ変更の適用はCI/CDパイプラインの自動化されたステップであるべきです。これにより手動実行への依存がなくなり、すべての環境が同じ変更を同じ順序で確実に受け取るようになります。
マイグレーションスクリプトをコードのようにレビューします。 マイグレーションスクリプトがマージされる前に、コードレビューを通過する必要があります。チームメンバーは、デフォルト値の欠落、長時間実行される操作、既存のクエリを壊す可能性のある変更など、潜在的な問題をチェックできます。これにより、問題が本番環境に到達する前に発見されます。
これが実際に重要な理由
スキーマ変更がコードのように管理されると、デプロイプロセスは予測可能になります。チームはマイグレーションが実行されたときに何が起こるかを正確に把握できます。まずステージング環境でテストできます。各マイグレーションスクリプトには対応するロールバックスクリプトがあるため、問題が発生した場合にロールバックできます。スキーマ変更は、それを導入したコミットまで遡って追跡できます。
さらに重要なことは、このアプローチがデプロイへの恐れを軽減することです。スキーマ変更は、別個の高リスクな活動ではなくなります。それらは開発ワークフローの通常の一部となり、他のコード変更と同様にレビューおよびテストされます。データベースはもはや、ごく一部の人だけが触れることができるブラックボックスではありません。
スキーママイグレーションの実践的チェックリスト
マイグレーションスクリプトをマージする前に、このクイックチェックリストを実行してください:
- マイグレーションには対応するロールバックスクリプトがありますか?
- マイグレーションはエラーを起こさずに複数回実行できますか(冪等性)?
- マイグレーションは長時間テーブルをロックしますか?もしそうなら、バッチ処理やオンラインスキーマ変更ツールの使用を検討してください。
- この変更後に壊れる可能性のある既存のクエリやコードはありますか?
- 本番データのコピーに対してマイグレーションをテストしましたか?
- マイグレーションスクリプトは少なくとも1人の他のチームメンバーによってレビューされましたか?
まとめ
データベーススキーマは静的な成果物ではありません。アプリケーションとともに進化します。スキーマ変更を手動で一回限りの操作として扱うことは、本番インシデントとチーム内の摩擦のレシピです。スキーママイグレーションをコードと同じ規律で管理することで、データベース変更を再現可能で、監査可能で、安全なものにできます。データベースは恐れの対象ではなくなり、チームが自信を持って変更できるシステムの単なる別の部分になります。