LiquibaseとCI/CDによるMySQLデータベーススキーママイグレーション自動化ガイド

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

クイックスタート:5分で最初のマイグレーションを実行する

こんな光景に心当たりはないでしょうか。深夜、Slackでpatch_v2.sqlを送信し、DevOps担当者に本番環境での手動実行を祈るような気持ちで依頼する——。たった1つのコマンド漏れや実行順序の食い違いで、アプリケーション全体がダウンしかねません。Liquibaseは、データベーススキーマをコードとして扱い、Gitのように厳密なバージョン管理を可能にすることで、こうしたリスクを根本から解消します。

複雑なインストール作業は不要です。Dockerと2つの設定ファイルさえあれば、すぐに始められます。

まず、MySQLへの接続情報を定義するliquibase.propertiesを作成します。

changeLogFile=changelog.xml
url=jdbc:mysql://localhost:3306/app_db?useSSL=false&allowPublicKeyRetrieval=true
username=root
password=rootpassword
driver=com.mysql.cj.jdbc.Driver

次に、サンプルとしてusersテーブルを定義するchangelog.xmlを作成します。

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
    http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="20241015-01" author="hoang-dev">
        <createTable tableName="users">
            <column name="id" type="BIGINT" autoIncrement="true">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="username" type="VARCHAR(50)">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="email" type="VARCHAR(100)">
                <constraints nullable="false"/>
            </column>
            <column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
        </createTable>
    </changeSet>
</databaseChangeLog>

1行のDockerコマンドでマイグレーションを実行します。

docker run --rm --network="host" \
  -v $(pwd):/liquibase/changelog \
  liquibase/liquibase \
  --defaults-file=/liquibase/changelog/liquibase.properties \
  update

コマンドの実行完了後、MySQLを確認してみましょう。usersテーブルに加えて、DATABASECHANGELOGとDATABASECHANGELOGLOCKという2つのシステムテーブルが新しく作成されていることが確認できます。これこそが、Liquibaseの動作を支える中核です。

内部アーキテクチャ:Liquibaseの仕組み

プレーンなSQLファイルは自身の適用状態を保持できません。15人の開発者が数十のマイクロサービスにわたってスキーマを変更しているような現場では、ステージング環境のテーブルが本番環境と一致しているかどうかを把握するのは至難の業です。

Liquibaseは、主に2つのコアコンポーネントを通じてこのライフサイクル全体を制御します。

1. ChangeSetとChangeLog

データベースへの個々の変更はChangeSetとしてカプセル化されます。そして、複数のChangeSetを束ねて管理するファイルがChangeLogです。

  • id: 変更の識別子。日付フォーマットやJiraチケット番号(例:PROJ-102-add-phone-col)での命名が推奨されます。
  • author: 変更を作成した作業者名。ログ調査時の監査証跡(Audit Trail)として役立ちます。
  • 一意識別キー: id + author + filepathの組み合わせがユニークキーを構成します。Liquibaseはこの3つの組み合わせをもとに、スクリプトを実行すべきかスキップすべきかを判定します。

2. 2つのシステムテーブルによる実行プロセス

updateコマンドを実行すると、Liquibaseは次の4つのステップを順次実行します。

  1. ロックの確認と取得: DATABASECHANGELOGLOCKテーブルを読み取ります。LOCKEDカラムが1に設定されます。この仕組みにより、2つのCI/CDパイプラインが同時に実行された際の競合を防止します。
  2. チェックサムの照合: DATABASECHANGELOGテーブルをスキャンし、各ChangeSetのMD5ハッシュ値を読み取って適用済みのスクリプトを特定します。
  3. 変更の実行: 未適用のChangeSetをMySQLに適用します。実行完了後、直ちに実行時間(ミリ秒単位)を含む監査ログを記録します。
  4. ロックの解除: DATABASECHANGELOGLOCKのLOCKEDを0に戻し、後続のプロセスへ実行権を譲渡します。

3. 障害発生時の安全なロールバック

深夜のデプロイ失敗は常に付きまとうリスクです。<rollback>ブロックを定義しておけば、rollback-count 1コマンドを実行するだけでデータベースを以前の状態へと安全に戻せます。

<changeSet id="20241015-02" author="hoang-dev">
    <addColumn tableName="users">
        <column name="phone_number" type="VARCHAR(20)"/>
    </addColumn>
    <rollback>
        <dropColumn tableName="users" columnName="phone_number"/>
    </rollback>
</changeSet>

GitHub ActionsパイプラインへのLiquibase統合

開発者が個人のローカル端末から本番サーバーに対して直接マイグレーションを実行することは絶対に避けるべきです。すべての操作はCI/CDパイプライン内で完結・隔離されなければなりません。

以下は、標準的なGitHub Actionsワークフローのテンプレートです。

name: Database Migration

on:
  push:
    branches:
      - main
    paths:
      - 'db/changelog/**'

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4

      - name: Liquibase Update
        uses: liquibase-github-actions/[email protected]
        with:
          changelogFile: 'db/changelog/changelog.xml'
          url: ${{ secrets.DB_JDBC_URL }}
          username: ${{ secrets.DB_USER }}
          password: ${{ secrets.DB_PASSWORD }}
          headless: true

絶対的な安全性を担保するために、以下の3ステージ構成のデプロイフローを採用しましょう。

  • 第1フェーズ(Dry-run & レビュー): update-sqlコマンドを実行して実行予定の生SQL(Raw SQL)をアーティファクトとして出力します。実データに適用される前に、DBAが実行クエリを厳密にレビューできます。
  • 第2フェーズ(ステージング環境へのマイグレーション): 本番と同等のデータを持つステージング環境へ自動適用し、タイムアウトや構文エラーを早期に検知します。
  • 第3フェーズ(本番環境の手動承認): GitHub Actionsで手動承認(Manual Approval Gate)を設定します。テックリードの承認を経て初めて、本番データベースへの接続・適用ジョブが実行されます。

MySQLでLiquibaseを運用する際の3つの教訓

4,000万件のレコード(約85GB)を保持するMySQL 8.0環境では、不用意なDDLコマンド1つで決済やECなどの本番システム全体が麻痺する恐れがあります。以下の原則を必ず心に留めておいてください。

  • 大規模テーブルでのメタデータロック(Metadata Lock)に警戒する: MySQL 8.0ではカラム追加などでALGORITHM=INSTANTがサポートされているものの、データ型の変更やインデックス作成では依然として排他ロック(Exclusive Lock)が必要です。500万行を超えるテーブルでは、DDLを直接実行するのではなく、<customChange>タグを活用してgh-ostやpt-online-schema-changeなどのオンラインスキーマ変更ツールを組み込みましょう。
  • 適用済みのChangeSetは絶対に直接修正しない: LiquibaseはMD5チェックサムを用いて整合性を検証します。すでに本番環境へ適用されたファイルを書き換えると、パイプラインはValidationFailedException: MD5Sum check failedエラーを出力して即座に停止します。ロジックを修正したい場合は、必ず新しいIDを採番した別のChangeSetを作成してください。
  • Preconditionsで重複エラーを未然に防ぐ: 実行前の前提条件チェックを活用します。<preConditions>タグを記述しておくことで、テーブルやインデックスがすでに存在する場合でもエラーで落ちることなく、安全に再実行可能(冪等性を担保)になります。
<changeSet id="20241015-03" author="hoang-dev">
    <preConditions onFail="MARK_RAN">
        <not>
            <indexExists indexName="idx_users_email" tableName="users"/>
        </not>
    </preConditions>
    <createIndex indexName="idx_users_email" tableName="users">
        <column name="email"/>
    </createIndex>
</changeSet>

スキーママイグレーションの自動化は、開発者を深夜の緊張を伴うデプロイ作業から解放するだけにとどまりません。さらに重要なのは、データベースを現代のDevOpsサイクルにおいて、テスト可能で追跡可能、かつ迅速に復旧可能なコンポーネントへと進化させられる点にあります。

Share: