現実的な課題:手動でのSQL実行という悪夢
キャリアの浅い頃、あるシニアエンジニアがチャットからターミナルへ誤って ALTER TABLE コマンドをコピー&ペーストしたせいで、ステージング環境のデータベースが消失しかけたのを目撃したことがあります。このシナリオは珍しくありません。開発者がローカルでDBを修正し、SQLコマンドを .sql ファイルやメモ帳に保存する。リリース当日、不遇なDevOps担当者が本番環境にログインし、各スクリプトを手動で実行するのです。
この手法は極めてリスクが高いです。スクリプトを一つ忘れたり、実行順序を間違えたりするだけで、アプリケーションは即座にダウンします。ロールバックが必要になった際、逆の操作を行う DROP や RENAME コマンドを思い出しながら書くのはまさに苦行です。私はかつて、MySQLからPostgreSQLへの100GBのデータベース移行の計画だけで3日間を費やしたことがあります。環境間で数百のスクリプトを一致させるためでした。
なぜコードにはGitがあるのに、データベースにはないのか?
私たちはコード管理にGitを使い、誰が何を修正したかを把握し、いつでも以前の状態に戻すことができます。しかし、多くのプロジェクトチームはいまだに「口伝」のようなやり方でデータベースを管理しています。
問題は、データベース構造に対する Source of Truth(信頼できる唯一の情報源)が欠如していることにあります。システムが数百のテーブルを抱えるほど肥大化すると、バラバラのSQLファイルを使用することで、開発・ステージング・本番環境の間でスキーマの不一致(スキーマドリフト)が確実に発生します。
データベース・マイグレーション管理の3つのレベル
プロジェクトの規模に応じて、以下の3つの方法から選択できます:
- レベル1: SQLファイルを日付順にGitに保存する(例:
20231027_add_user_col.sql)。この方法は簡単ですが、依然として手動実行が必要で、ミスが起こりやすいです。 - レベル2: フレームワーク内蔵のマイグレーション機能(TypeORM、Entity Frameworkなど)を使用する。しかし、この方法ではデータベースがアプリケーションのプログラミング言語に強く依存してしまいます。
- レベル3: Flyway や Liquibase のような専用ツールを使用する。これは、データベース管理ロジックをコードから完全に分離するためのプロフェッショナルな選択肢です。
なぜLiquibaseはCI/CDにとって「最良の選択」なのか?
Flywayと比較して、Liquibaseは多様な形式(XML、YAML、JSON、SQL)をサポートしており、非常に強力なロールバックスクリプトの自動生成機能を備えている点で優れています。以下は、私が実際のプロジェクトでLiquibaseをどのように適用しているかです。
1. 標準的なChangesetファイルの構造
純粋なSQLを書く代わりに、YAMLまたはXML形式を使用することをお勧めします。Liquibaseは、各DBの種類に応じた構文に自動的に変換してくれます。プロジェクトがMySQLからPostgreSQLに変更されたとしても、マイグレーションスクリプトを修正する必要はほとんどありません。
databaseChangeLog:
- changeSet:
id: 1
author: tech_editor
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: int
autoIncrement: true
constraints:
primaryKey: true
- column:
name: username
type: varchar(50)
constraints:
nullable: false
2. 「タイムマシン」DATABASECHANGELOG
初回実行時、Liquibaseは履歴を追跡するために DATABASECHANGELOG テーブルを自動作成します。実行に成功した各 changeSet は、ハッシュ値(MD5Sum)とともに保存されます。
チェックメカニズムは非常に厳格です:
- 実行済みのものはスキップする。
- 新しい変更のみを実行する。
- 古いファイルの内容が変更された場合(ハッシュ値の不一致)、データの破損を防ぐために即座にエラーを報告する。
3. データベースをCI/CDパイプラインに組み込む
開発者が個人のPCから本番環境へ直接Liquibaseを実行するようなことは絶対に避けてください。Jenkins、GitLab CI、またはGitHub Actionsに統合しましょう。標準的なプロセスは通常、以下の4つのステップで構成されます:
- 開発者がブランチを作成し、新しい
changeSetを記述する。 - プルリクエスト作成時、CIが
liquibase validateを実行して構文をチェックする。 developブランチにマージされた後、CIが自動的にステージング環境のデータベースをupdateする。- 最終的に、承認プロセスを経て、パイプラインが本番環境に変更をデプロイする。
ヒント:Dockerを使用してLiquibaseを実行すると、環境を常に同一に保つことができます:
docker run --rm -v $(pwd):/liquibase/changelog \
liquibase/liquibase \
--changelog-file=/liquibase/changelog/db-changelog.yaml \
--url="jdbc:postgresql://db_host:5432/mydb" \
--username=admin --password=secret update
4. ロールバックの秘策:デプロイ失敗時の救命ボート
XML/YAML形式を使用すると、Liquibaseは逆のコマンド(例:createTable に対しては dropTable)を自動的に推論できます。ただし、複雑なロジックの変更の場合は、100%の安全を確保するために rollback ブロックを自分で定義することをお勧めします。
- changeSet:
id: 2
author: tech_editor
changes:
- renameColumn:
tableName: users
oldColumnName: username
newColumnName: login_name
rollback:
# ロールバック処理:カラム名を元のusernameに戻す
- renameColumn:
tableName: users
oldColumnName: login_name
newColumnName: username
実戦から得た貴重な経験
- 分割して統治せよ: 各ChangeSetは単一のタスクのみを行うべきです。10個のテーブルを一つのChangeSetにまとめ、最後のテーブルでエラーが発生した場合、デバッグが非常に困難になります。
- 不変の原則: コミット済みのChangeSetは絶対に修正しないでください。間違いがあった場合は、修正用の新しいChangeSetを作成してください。古いファイルを修正するとMD5Sumが壊れ、Liquibaseが動作しなくなります。
- Contextsの活用:
context: testを使用して、開発・ステージング環境のみにサンプルデータを挿入し、本番環境のデータベースを汚さないようにします。 - 常にドライランを実行:
update-sqlコマンドを使用して、実際に適用する前にLiquibaseが実行しようとしているSQL文をプレビューしてください。
Liquibaseの導入には、初期設定に少し時間がかかるかもしれません。しかし、デプロイボタンを押す時の安心感は、それに見合う価値のある報酬です。もうSQLの一行一行を凝視したり、スキーマの不一致を心配したりする必要はありません。皆さんのデータベース管理がより快適になることを願っています!

