TalentX Tech Blog

Tech Blog

Atlasによる宣言的マイグレーションの導入

バックエンドエンジニアの中山です。

今回は、MyReferチームにDBの宣言的マイグレーションツールAtlasを導入し、DBスキーマのマイグレーション運用フローを改善した取り組みについて紹介します。

導入前の課題

導入前、MyReferチームではローカル環境のみで、従来のバージョニング方式によるDBスキーマのマイグレーションを行っていました。 一方で、テスト・ステージング・本番環境では、GitHub上のマイグレーションファイルからSQLを抽出し、Bastionサーバー経由で直接SQLを実行していました。

サービスリリース当初は、全環境でマイグレーションツールを用いて運用していましたが、長期の運用の中で、障害対応や緊急リリース時にツールを介さず直接SQLを実行してロールバックするケースが発生しました。結果として、ツールを使わない運用となり、環境ごとにスキーマが微妙に異なる状態になっていました。

この状況を解消するため、全環境でバージョニングによるマイグレーション運用を再開しようと思いましたが、次のような課題がありました。

  • 環境間のスキーマ差分をすべて手動で解消する必要がある
  • 最新の状態に合わせるために、マイグレーション管理テーブルを手動で調整する必要がある
  • 手動対応では、スキーマの完全一致を保証するのが難しい

こうした課題を踏まえ、スキーマの理想状態を定義し、それに基づいてDDLを自動生成できる「宣言的マイグレーション」へ移行することにしました。

宣言的マイグレーションとは

宣言的マイグレーションとは、データベースの理想状態をコードで定義し、現在のスキーマとの差分から必要な変更を自動生成する方式です。 理想状態と実際のスキーマにズレがある場合、その差分を解消するSQLが自動で生成されるため、環境間でスキーマを一致させることができます。また、スキーマがコードとして定義されているため、DBに接続せずとも現在の状態を容易に確認することができます。

Atlasとは

Atlasは、Ariga社が開発した宣言的マイグレーションをサポートするツールです。 以下のような特徴があります。

HCLによるスキーマ記述

DBスキーマを HCL(HashiCorp Configuration Language) で記述できます。 MyReferチームではインフラをTerraformで管理しており、HCLを扱えるメンバーが多いため、親和性があります。

柔軟なマイグレーション方式

宣言的マイグレーションと従来のバージョニング方式の両方をサポートしており、用途に応じて使い分けることが可能です。例えば、スキーマは宣言的マイグレーションで管理し、都道府県などのマスターテーブルへのレコード登録や、テーブル間のデータ移行はバージョニング方式のマイグレーションで対応するといったことが可能です。

豊富なエコシステム

公式でDockerイメージGitHub Actionsが提供されており、環境構築やCI/CDへの組み込みが容易です。 また、MySQLのGenerated Columnのような複雑なスキーマ定義にも対応しており、既存のスキーマを問題なく取り扱えます。

事前確認機能

スキーマ変更前に差分をプレビューできるため、安全にマイグレーションを実行できます。

これらの特徴により、MyReferチームの要件を満たし、既存環境への導入も容易であると判断したため、Atlasを採用することにしました。

実装サンプル

ここからは実際にAtlasを使った宣言的マイグレーションの実装例をご紹介します。

サンプルプロジェクト構成

まず、サンプルプロジェクトのディレクトリ構成の全体像は以下のようになります。

.
├── atlas.hcl              # Atlas設定ファイル
├── docker-compose.yml     # Docker Compose設定
├── initdb.d/              # DBの初期化スクリプト
│   └── init.sql
├── migrations/            # バージョンニングによるマイグレーションファイル
│   ├── 20251001123000_insert.sql
│   └── atlas.sum
└── schemas/
    └── test-db/
        ├── test-db.my.hcl       # データベース定義
        └── tables/
            ├── companies.my.hcl  # 企業テーブル定義
            └── users.my.hcl      # ユーザーテーブル定義

Docker Composeの設定

docker-compose.ymlでは、MySQL DBコンテナとAtlasが提供するDockerイメージを使用したマイグレーション用コンテナを定義します。

services:
  test-db: # DBコンテナ
    image: mysql:8.0
    container_name: test-db
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
    ports:
      - '3306:3306'
    volumes:
      - test-db-volume:/var/lib/mysql

  migration:  # マイグレーションコンテナ
    container_name: migration
    image: arigaio/atlas:latest
    working_dir: /src
    env_file:
      - .env
    volumes:
      - .:/src
    restart: "no"

volumes:
  test-db-volume:

initdb.d/init.sqlでユーザーの権限を設定しておきます。

CREATE USER IF NOT EXISTS 'user'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON `test-db`.* TO 'user'@'%';
FLUSH PRIVILEGES;

Atlas設定ファイル

atlas.hclは、DBの接続情報やスキーマ、マイグレーションファイルの出力先などを管理する設定ファイルです。 今回はdevという環境を設定しました。

locals {
  # 共通設定
  schema_file_paths = fileset("schemas/**/*.hcl")
  migration_dir     = "file://migrations"

  # 環境別データベース設定
  databases = {
    dev = {
      user        = getenv("MYSQL_USER")
      password    = getenv("MYSQL_PASSWORD")
      host        = getenv("MYSQL_HOST")
      port        = getenv("MYSQL_PORT")
      db_name     = getenv("MYSQL_DATABASE")
    }
  }

  # URL生成ヘルパー関数
  mysql_url = {
    for env_name, config in local.databases :
    env_name => "mysql://${config.user}:${config.password}@${config.host}:${config.port}/${config.db_name}"
  }
}

# スキーマ定義
data "hcl_schema" "dev" {
  paths = local.schema_file_paths
}

# 環境定義
env "dev" {
  src = data.hcl_schema.dev.url
  url = local.mysql_url.dev

  migration {
    dir = local.migration_dir
  }
}

この設定ファイルでは、環境変数から接続情報を取得し、環境ごとの設定を定義しています。fileset関数を使用することで、schemas/配下のすべてのHCLファイルを自動的に読み込むことができます。

環境変数の設定

.envファイルに、データベース接続情報を記述します。

MYSQL_ROOT_PASSWORD=root
MYSQL_HOST=test-db
MYSQL_PORT=3306
MYSQL_DATABASE=test-db
MYSQL_USER=user
MYSQL_PASSWORD=password

また、charset や collate など、データベースの設定情報を定義するスキーマファイルとして、schemas/test-db/test-db.my.hclを作成しておきます。

schema "test-db" {
  charset = "utf8mb4"
  collate = "utf8mb4_0900_ai_ci"
}

ここからは、実際にAtlasを使ってスキーマを作成する手順を紹介します。

スキーマの解析

まず、既存のデータベーススキーマをHCLファイルとして抽出してみます。 事前にtest-dbに以下のcompaniesテーブルを作成しておきます。

CREATE TABLE `companies` (
  `company_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '企業ID',
  `name` VARCHAR(255) NOT NULL COMMENT '企業名',
  PRIMARY KEY (`company_id`)
) ENGINE=InnoDB COMMENT='企業テーブル';

次に、以下のコマンドを実行します。

docker compose run --rm migration \                       
  schema inspect \
  --env="dev" \
  --format '{{ hcl . | split "object" ".my.hcl" | write "." }}'

atlas schema inspectコマンドにより、現在のDBスキーマを解析し、拡張子.my.hclを持つHCLファイルがテーブルごとに自動生成されます。生成されたファイルを見てみましょう。

table "companies" {
  schema  = schema.test-db
  comment = "企業テーブル"
  column "company_id" {
    null           = false
    type           = int
    unsigned       = true
    comment        = "企業ID"
    auto_increment = true
  }
  column "name" {
    null    = false
    type    = varchar(255)
    comment = "企業名"
  }
  primary_key {
    columns = [column.company_id]
  }
}

先ほど実行したCREATE TABLE文がHCL形式で表現され、カラムの属性、制約、コメントなどがすべて保持されています。

テーブルを追加する

続いて、新しくusersテーブルを追加してみます。このテーブルはuser_idcompany_idemailの3つのカラムを持ち、company_idに外部キー制約、emailカラムにユニークキー制約を設定します。

table "users" {
  schema  = schema.test-db
  comment = "ユーザーテーブル"
  column "user_id" {
    null           = false
    type           = int
    unsigned       = true
    comment        = "ユーザーID"
    auto_increment = true
  }
  column "company_id" {
    null           = false
    type           = int
    unsigned       = true
    comment        = "企業ID"
  }
  column "email" {
    null    = false
    type    = varchar(255)
    comment = "メールアドレス"
  }
  primary_key {
    columns = [column.user_id]
  }
  foreign_key "fk_company_id" {
    columns     = [column.company_id]
    ref_columns = [table.companies.column.company_id]
  }
  index "fk_company_id" {
    unique  = false
    columns = [column.company_id]
  }
  index "uk_email" {
    unique  = true
    columns = [column.email]
  }
}

このHCLファイルを作成後、以下のコマンドでスキーマを適用します。

docker compose run --rm migration \
    schema apply \
    --env="dev"

Atlasが生成したDDLが表示されます。

Planning migration statements (1 in total):

  -- create "users" table:
    -> CREATE TABLE `users` (
         `user_id` int unsigned NOT NULL COMMENT "ユーザーID" AUTO_INCREMENT,
         `company_id` int unsigned NOT NULL COMMENT "企業ID",
         `email` varchar(255) NOT NULL COMMENT "メールアドレス",
         PRIMARY KEY (`user_id`),
         INDEX `fk_company_id` (`company_id`),
         UNIQUE INDEX `uk_email` (`email`),
         CONSTRAINT `fk_company_id` FOREIGN KEY (`company_id`) REFERENCES `companies` (`company_id`)
       ) COMMENT "ユーザーテーブル";

-------------------------------------------

? Approve or abort the plan: 
  ▸ Approve and apply
    Abort

この画面で「Approve and apply」を選択すると、実際にDDLが実行されます。事前にSQLを確認できるので安心ですね。

Applying approved migration (1 statement in total):

  -- create "users" table
    -> CREATE TABLE `users` (
         `user_id` int unsigned NOT NULL COMMENT "ユーザーID" AUTO_INCREMENT,
         `company_id` int unsigned NOT NULL COMMENT "企業ID",
         `email` varchar(255) NOT NULL COMMENT "メールアドレス",
         PRIMARY KEY (`user_id`),
         INDEX `fk_company_id` (`company_id`),
         UNIQUE INDEX `uk_email` (`email`),
         CONSTRAINT `fk_company_id` FOREIGN KEY (`company_id`) REFERENCES `companies` (`company_id`)
       ) COMMENT "ユーザーテーブル";
  -- ok (65.312833ms)

  -------------------------
  -- 66.875376ms
  -- 1 migration
  -- 1 sql statement

テーブルを変更する

次にusersテーブルにnameカラムを追加してみましょう。

table "users" {
  schema  = schema.test-db
  comment = "ユーザーテーブル"
  column "user_id" {
    null           = false
    type           = int
    unsigned       = true
    comment        = "ユーザーID"
    auto_increment = true
  }
  column "company_id" {
    null           = false
    type           = int
    unsigned       = true
    comment        = "企業ID"
  }
  # カラム追加
  column "name" {
    null    = false
    type    = varchar(255)
    comment = "ユーザー名"
  }
  # ...
}

同様に以下のコマンドを実行します。

docker compose run --rm migration \
    schema apply \
    --env="dev"
Planning migration statements (1 in total):

  -- modify "users" table:
    -> ALTER TABLE `users` ADD COLUMN `name` varchar(255) NOT NULL COMMENT "ユーザー名" AFTER `company_id`;

-------------------------------------------

? Approve or abort the plan: 
  ▸ Approve and apply
    Abort

nameカラムを追加するALTER TABLE文が生成されました。

ロールバック

users.my.hclを削除して同様に以下のコマンドを実行します。

docker compose run --rm migration \
    schema apply \
    --env="dev"

DROP TABLE文が生成され、変更をロールバックできます。

Planning migration statements (1 in total):

  -- drop "users" table:
    -> DROP TABLE `users`;

-------------------------------------------

? Approve or abort the plan: 
  ▸ Approve and apply
    Abort

バージョニングによるマイグレーション

次に、従来のバージョニング用のマイグレーションも試してみます。マイグレーションファイルmigrations/20251001123000_insert.sqlを作成します。

INSERT INTO `companies` (`name`) VALUES ('company1');

バージョニングによるマイグレーションでは、チェックサムによるファイルの検証を行います。 以下のコマンドを実行すると、migrations配下にatlas.sumというファイルが生成されます。

docker compose run --rm migration \
    migrate hash

マイグレーションを適用してみましょう。

docker compose run --rm migration \
    migrate apply \
    --env="dev" \
    --allow-dirty

--allow-dirtyフラグは、データベースがクリーンでない場合でもマイグレーションを実行することを許可するオプションです。通常、Atlasはatlas migrate diffコマンドを用いて、現在のDBスキーマを解析した上で全テーブルのCREATE TABLE文を含むマイグレーションファイルを自動生成します。しかし、今回のようにスキーマの管理を宣言的マイグレーションに任せ、データ操作などの任意のSQLのみを実行したい場合は、このフラグを指定することでマイグレーションを適用できます。

コマンドを実行するとSQLが実行され、レコードが登録されます。

Migrating to version 20251001123000 (1 migrations in total):

  -- migrating version 20251001123000
    -> INSERT INTO `test-db`.`companies` (`name`) VALUES ('company1');
  -- ok (5.637792ms)

  -------------------------
  -- 44.174084ms
  -- 1 migration
  -- 1 sql statement

運用方針

Atlasを導入するにあたり、以下のような運用方針を定めました。

宣言的とバージョニングの使い分け

基本的に、DBのスキーマ構造は宣言的マイグレーションで管理しますが、以下のようなデータ操作が必要な場合はスキーマとして表現できないため、バージョニングによる従来のマイグレーション方式を利用します。

  • マスターデータの登録
  • データ移行やデータ変換

運用フロー

ローカル開発時にAtlasが生成するDDLをGitHubのPRでレビューする形を取っています。

マイグレーション実行タイミングに自由度を持たせるため、CI/CDには組み込まず、AWS ECS Fargateを使用してBastionサーバーとしてマイグレーション用のコンテナを運用しています。 開発者はコンテナ上で以下の操作を行うスクリプトを実行し、スキーマを更新します。スクリプトを用意することで実行手順を簡素化し、オペレーションミスを防いでいます。

  1. マイグレーションファイルがコミットされているGitHubリポジトリをpull
  2. Atlasコマンドを実行してスキーマを適用

運用フロー図

移行

各環境へAtlasを導入する際は、以下の手順で移行を行いました。

  1. 本番環境のDBスキーマからテーブルのDDLを抽出
  2. ローカル環境にインポート
  3. atlas schema inspectコマンドでHCLファイルを生成
  4. 生成されたHCLファイルを各環境に適用

この手順により、すべての環境のスキーマを容易に統一することができました。

導入して良かった点

Atlasを導入したことで、スキーマの統一以外にも以下のようなメリットを実感しています。

  • スキーマの変更を行う際に、CREATE TABLE文やALTER TABLE文を考える必要がなく、より直感的に変更を行えるようになり、開発者体験(DX)が向上した
  • DBに接続することなく、スキーマをコードで即座に確認できるようになった

運用上の注意点

スキーマ定義が複雑な場合は、Atlas特有の記法を調べる必要があります。ただし、ドキュメントが充実しており、非常に多くの機能をサポートしているため、実運用においてそれほど困ることはありません。

まとめ

本記事では、Atlasを用いた宣言的マイグレーションの導入により、環境ごとにスキーマが異なるという課題を解決し、より安全で管理しやすい運用体制を構築した取り組みを紹介しました。

宣言的マイグレーションという新しいアプローチにより、以下のような成果を得ることができました

  • 環境間のスキーマの統一と完全一致の保証
  • スキーマ変更時のDX向上
  • コードベースでのスキーマ管理による可視性の向上

最後に

現在、TalentXでは一緒に働く仲間を募集しております。

talentx.brandmedia.i-myrefer.jp

カジュアル面談も行っておりますので、ぜひご応募ください!

i-myrefer.jp