データベースカラムの削除が本番環境を壊すとき:破壊的なスキーマ変更の管理

使われていないカラムを削除するデータベースマイグレーションを実行したとします。SQLはきれいに見え、マイグレーションはエラーなく完了します。しかし5分後、アラートが鳴り始めます。本番アプリケーションがエラーを吐いています。まだそのカラムを参照しているコードが残っていたのです。単純なクリーンアップのはずが、障害を引き起こしました。

このシナリオは、多くのチームが認めるよりもはるかに頻繁に発生しています。問題はマイグレーション自体ではありません。問題は、「もう誰も使っていない」と思い込んでデータベースから何かを削除することが安全だと決めつけてしまうことです。

変更が「破壊的」になる条件

データベースの変更は2種類に分類されます。追加的変更は、新しいカラム、新しいテーブル、新しいインデックスなど、何か新しいものを追加します。既存のコードは知らないものを単に無視するため、一般的に安全です。

破壊的変更は、既存の構造を削除、リネーム、または変更します。カラムの削除、テーブルのリネーム、カラム型の変更、制約の削除はすべて破壊的です。リスクは明白です。実行中のアプリケーションがその構造に依存している場合、変更が適用された瞬間に壊れます。

この危険性は、現代的なデプロイ戦略によってさらに増幅されます。ローリングアップデートやブルーグリーンデプロイでは、新旧のアプリケーションバージョンが数分から数時間にわたって並行して動作します。デプロイ中に破壊的なマイグレーションが実行されると、トラフィックを処理している古いインスタンスが即座に壊れます。

多段階マイグレーションパターン

最も安全なアプローチは、何かを一度に削除しないことです。代わりに、破壊的な変更を複数のフェーズに分割します。各フェーズは、その時点で実行されているアプリケーションバージョンと互換性がなければなりません。

次のフローチャートは、安全なカラムリネームの3つのフェーズを示し、各ステップで互換性のあるアプリケーションバージョンを表しています。

flowchart TD A[フェーズ1: 新しいカラムを追加し、古いカラムを維持] --> B[アプリ v1 をデプロイ: 古いカラムから読み取り、両方に書き込み] B --> C[フェーズ2: データをバックフィルし、二重書き込み] C --> D[アプリ v2 をデプロイ: 新しいカラムから読み取り、両方に書き込み] D --> E[フェーズ3: 古いカラムへの書き込みを停止] E --> F[アプリ v3 をデプロイ: 新しいカラムのみ読み書き] F --> G[フェーズ4: 古いカラムを削除] G --> H[アプリ v4 をデプロイ: 新しいカラムのみ使用] style A fill:#d4edda,stroke:#28a745 style C fill:#fff3cd,stroke:#ffc107 style E fill:#f8d7da,stroke:#dc3545 style G fill:#f8d7da,stroke:#dc3545

カラム名を status から status_code に変更する場合を考えてみましょう。単一のマイグレーションでカラム名を変更すると、まだ status を読み取っているコードはすべて壊れます。多段階アプローチは次のようになります。

フェーズ1: 古いカラムを削除せずに新しいカラムを追加します。古いカラムから新しいカラムにデータをコピーします。アプリケーションコードを更新して、新しいカラムから読み取りつつ、両方に書き込むようにします。この変更をデプロイします。

フェーズ2: すべてのアプリケーションインスタンスが新しいカラムを使用していることを確認した後、古いカラムへの書き込みを停止します。コードを更新して status_code のみを参照するようにします。再度デプロイします。

フェーズ3: 実行中のコードが古いカラムに触れていないことを確信したら、別のマイグレーションでそれを削除します。トラフィックが少ない時間帯にスケジュールします。

同じパターンはテーブルの削除にも適用されます。古い機能を置き換えるビューまたは新しいテーブルを作成します。アプリケーションコードを新しい構造にリダイレクトします。古いテーブルを参照するコードがなくなるまで待ちます。その後、削除します。

セーフティネットとしてのソフトデリート

データベースから実際に削除せずに、アプリケーションのビューからデータを削除したい場合があります。ここでソフトデリートが役立ちます。

DELETE文を実行する代わりに、deleted_atis_active のようなカラムを追加します。アプリケーションはWHERE句で削除された行をフィルタリングします。データは監査、復旧、または他の機能からの予期しない依存関係のためにテーブルに残ります。

ソフトデリートは、データがまだ必要かどうか完全に確信が持てない場合に特に便利です。安全バッファーを提供します。何かが壊れた場合、データベースの復元なしに可視性を回復できます。トレードオフとして、テーブルは大きくなり、クエリはフィルタを考慮する必要があります。しかし、多くのチームにとって、このトレードオフは安全性に見合う価値があります。

制約を慎重に扱う

外部キーやユニーク制約のような制約を削除することは、データを削除するよりもリスクは低いですが、それでも影響があります。制約はデータの整合性を強制します。アプリケーションコードが重複エントリや孤立レコードを防ぐためにデータベースに依存している場合、制約を削除するとデータ破損につながる可能性があります。

制約を削除する前に、コードベースを監査して、その制約に依存するロジックがないことを確認します。データベースがサポートしている場合は、制約を完全に削除するのではなく、まず無効にすることを検討します。これにより、再び有効にする機能を永久に失うことなく、影響をテストできます。

破壊的変更のための実践的チェックリスト

  • 削除しようとしている構造を参照する実行中のアプリケーションコードがないことを確認します。現在のリリースと進行中のデプロイの両方を確認します。
  • 変更を少なくとも2つのマイグレーションに分割します。1つは新しい構造を追加してコードをリダイレクトするため、もう1つは待機期間の後に古い構造を削除するためです。
  • 破壊的なマイグレーションは機能デプロイとは別に実行します。カラム削除を新しいエンドポイントのリリースと一緒に行わないでください。
  • 破壊的な変更はトラフィックが少ない時間帯にスケジュールします。多段階計画を立てていても、影響を受けるユーザーが少ないほど、予期しない問題に対処しやすくなります。
  • 古い構造を削除した後、リネームしたカラムや無効にした制約などの残骸を後続のマイグレーションでクリーンアップします。ただし、クリーンアップも破壊的であることを忘れずに、同じ段階的アプローチを適用します。

核となる原則

実行中のアプリケーションからアクセスされる可能性があるものを決して削除してはなりません。これは明白に聞こえますが、データベースマイグレーションでチームが犯す最も一般的な間違いです。スキーマをきれいに保ちたいというプレッシャー、「もう誰も使っていない」という思い込み、そして複数の小さなマイグレーションではなく1つのきれいなマイグレーションをリリースしたいという願望が、チームをリスクの高い単一ステップの削除へと駆り立てます。

多段階マイグレーションは、より多くの時間とより多くのデプロイを必要とします。しかし、単純なスキーマクリーンアップを緊急ロールバックに変えるような本番障害を防ぎます。きれいなスキーマは、壊れたアプリケーションに見合う価値はありません。