💫

Atlasを使った宣言的スキーマ管理・マイグレーション

に公開

はじめに

横浜銀行アジャイル開発チームのwatariです。

業務・個人開発によらず、バックエンドを作成するにあたりRDBを使うことが多いかと思います。
※設計方針によって、RDBを使う必要がないケースもままあります。

その時に、よく出てくる話題としてマイグレーションどうするというものがあります。
そこでタイトルにある、Atlasによる宣言的マイグレーションを、簡単にGitLabCIで動かすところまで扱って記事にしました!

Atlas:Atlas | Manage your database schema as code

記事中、マイグレーションという単語にも、整理を兼ねて触れます。

マイグレーションとは

一言で言うと、データベースの構造(スキーマ)を変更したり、データを別のデータベースに移行したりすることです。

手段として、2種類あります。

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

こちらが従来というか一般的でしょうか。
既存のデータが失われないように、かつ履歴を管理しながら安全におこなうための仕組みです。

開発の初期段階にあたって、テーブル定義の更新は珍しくありません。

この時、バージョニングによるマイグレーションが担保することは以下の通りです。

  • 一貫性の確保: 開発チーム内でデータベースの構造を共有し、どの環境でも同じデータベース構造を再現する
  • 安全な変更: 既存のデータを失うことなく、データベースの構造を変更する
  • 履歴管理: データベースの変更履歴を追跡し、いつ、どのような変更がおこなわれたかを把握できる
  • ロールバック: 問題が発生した場合に、データベースを以前の状態に戻すことができる
  • 効率化: 手動でSQL文を書いて変更するよりも、自動化されたツールを使うことで作業を効率化する

宣言的マイグレーション

こちらはバージョニングのように履歴ファイルを持ちません。

DBの理想状態(つまり設計)をファイル定義し、その理想状態をDBに適用します。
そして履歴ファイルを持たないといいますが、このファイル定義自体がGitなどで管理されることで、履歴ファイルの役割を担います。

メリットは以下の通りです。

  • 手動でSQLを書く手間が省けるため、開発者の負担が減る。
  • スキーマの定義ファイルだけを見れば、データベースの最終的な構造が把握しやすい。
  • 特定のマイグレーションがスキップされたり、順序が入れ替わったりしても、最終的な状態に収束する。

また、デメリットは以下の通りです。

  • 複雑なデータ移行は、宣言的な記述では難しい場合がある。
    ※外部キーに変更を入れようとしたら、制約などに引っかかりツールだけでエラーを解消できませんでした...。
  • 生成するSQLが、開発者の意図と異なる場合がある。
  • ツールの成熟度や機能に依存する部分が大きい。

ただ多くのデメリットは以下の用途に限ることで解消されるでしょう。

  • スキーマ変更が比較的シンプルで頻繁におこなわれる場合
  • 開発初期でスクラップ&ビルド的にDBが変わる場合

Why Atlas

もともと宣言的マイグレーションの動きは好んでいて、flyway(JVM)やridgepole(Ruby)などは知っていました。
ただ、Ruby経験者が不在でしたので、ridgepole導入には学習コストが発生します。
そしてflywayは、JVMのプロジェクトにおいては有効な選択肢ですがマイグレーションのためにJava環境を作りたくありませんでした。

その状況でAtlasを見つけて、以下理由で導入となりました。

  • 宣言的マイグレーション
  • Atlas単体で動作する
  • 公式ドキュメントにはAIによる検索機能があり、検索性が高い

では手順です。Golangプロジェクトで動かしていますが、前述通り単体で動作します。

Quick Start

公式をなぞりつつ、少し変更を加えた手順です。DBはMySQLになります。
※postgreSQLなど他のDBにも対応しています。

インストール

$ curl -sSf https://atlasgo.sh | sh
$ atlas version

これでローカル環境にAtlas本体がインストールされます。

ちなみに、後から触る人のためにMakefileがあるなら記載しておくと良いです(以降、同様に)。

atlas-install:
	curl -sSf https://atlasgo.sh | sh

envファイル設定

愚直にコマンドを書くこともできますが、設定できるものは設定しましょう。
※ciのほうにdb名を書いていない理由は後述。

atlas.hcl
env "local" {
  url = "mysql://user:pass@localhost:3306/some_db"
  src = "file://db/schemas"
}
env "ci" {
  url = "mysql://user:pass@docker:3306/"
  src = "file://db/schemas"
}

これはコマンドにすると次の通りです(localの場合)。

atlas schema apply \
  -u "mysql://user:pass@localhost:3306/some_db" \
  --to file://db/schemas

file://db/schemasでディレクトリ、ファイルともに指定できます。
ディレクトリの場合は、その配下の対象ファイルをまとめて読みます。

これは特に順序(ファイル名など)は気にしなくて良いそうです。

ファイルの場合は以下のようになります。
file://db/schemas/main.hcl

schemaファイル設定

以下はサンプルです。簡単な例ではありますが、テーブル定義は直感的です。

common.hcl
variable "tenant" {
  type        = string
  description = "The name of the tenant (schema) to create"
}
schema "tenant" {
    charset = "utf8mb4"
    collate = "utf8mb4_general_ci"
    name = var.tenant
}
users.hcl
table "users" {
  schema = schema.tenant
  column "id" {
    null = false
    type = int
  }
  column "name" {
    null = false
    type = varchar(255)
  }
  column "project_id" {
    null = false
    type = int
  }
  column "created_at" {
    null = false
    type = datetime
    default = sql("CURRENT_TIMESTAMP")
  }
  column "updated_at" {
    null = false
    type = datetime
    default = sql("CURRENT_TIMESTAMP")
    on_update = sql("CURRENT_TIMESTAMP")
  }
  index "idx_users_name" {
    unique = true
    columns = [column.name]
  }
  primary_key {
    columns = [column.id]
  }
  foreign_key "fk_project_id" {
    columns = [column.project_id]
    ref_columns = [table.projects.column.id]
    on_delete = NO_ACTION
    on_update = NO_ACTION
  }
}
projects.hcl
table "projects" {
  schema = schema.tenant
  column "id" {
    null = false
    type = int
  }
  column "name" {
    null = false
    type = varchar(255)
  }
  column "created_at" {
    null = false
    type = datetime
  }
  column "updated_at" {
    null = false
    type = datetime
  }
  primary_key {
    columns = [column.id]
  }
}

variable "tenant"は変数です。以下のように設定します。

$ atlas schema apply --env "local" --schema some_db --var tenant=some_db

--var tenant=some_dbと指定することで、向き先(Common.hcl内のschema "tenant")を自在に変更できます。
※env(atlas.hcl)は実行時同じディレクトリに置いてください。
※tenantについて、本当はenvに埋め込みたいところですが、こちらは調査中になります。

マイグレーション

これまでの説明でapplyコマンドが出てきていますが、applyします。

$ atlas schema apply --env "local" --schema some_db --var tenant=some_db

これで変更内容が表示され、適用するかどうかが選択可能になります。
適用する場合は、この操作で変更が反映され、DBは理想状態になります。

diffを取りたい場合、オプションに--dry-runと付ければ確認できます。

apply schema diffというコマンドでも良いのですが、筆者の用途では--dry-runがお手軽でした。
diffコマンド

まとめ

初めて触れる場合はHCLファイルの記述方法に戸惑う可能性がありますが、atlas単体で動き、HCLファイルも慣れれば書きやすいです。
HCLファイル以外にSQLファイルでも扱うことが出来ます。
個人的には非常に使いやすいマイグレーションツールといえます。

不明点が出ても、公式ドキュメントや公式のAIが助けてくれます(英語ですが、最近は翻訳ツールも優秀です)。

「マイグレーションの履歴ファイルは管理したくないけど、それ用に環境作るのは難しい...」という方はぜひ試してみてください!

おまけ(Atlas * sqlboilerによるCI)

CICDのうち、CDは公式にて以下のように記述しています。
https://github.com/ariga/atlas

Atlas is a language-agnostic tool for managing and migrating database schemas using modern DevOps principles.

DevOpsに組み込んで使うことを考えて作られているツールです。

そのためCDは他に譲り、本記事ではCIについて触れます。

sqlboiler

マイグレーションした後の内容です。
本事例では、GolangプロジェクトにおけるORMツールとしてsqlboilerを採用しています。
以下をおこなうと生成されたテーブルに応じたコードが自動生成されます。

$ sqlboiler mysql -c sqlboiler.toml

tomlファイルは以下の内容です(サンプル、ローカル実行のものです)。

output = "internal/adapter/driven/db/models"
wipe = true
[mysql]
    dbname="gitlab"
    host="localhost"
    port="3306"
    user="root"
    pass="root"
    sslmode="false"

これはflyway + mybatisとかでもできる動きですね。
これでgolangのコードが出来て、テストコードも生成してくれるのでCIで動かしてみましょう。

もちろんローカルのテストも忘れずにおこないます。

$ go test ./... -race -cover (など任意オプション)

CI

GitLab上で動かしているので、それに則ります。

gitlab-ci.yml
stages:
  - test
test:
  stage: test
  image: cimg/go:1.24.2
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  services:
    - name: docker:25.0.3-dind
      alias: docker
  before_script:
    - docker info
    - sudo apt-get update && sudo apt-get install -y make curl mysql-client
  script:
    - docker compose up -d
    - make atlas-install
    - export PATH=$PATH:~/.atlas/bin
    # DBが完全に起動してポートがリッスンされるまで待機
    - echo "Waiting for DB to start on docker:3306..."
    - ./scripts/wait-for-it.sh docker:3306 -t 90 # 90秒待機(必要に応じて調整)
    - make atlas-ci
    - go mod tidy
    - cp sqlboiler_ci.toml sqlboiler.toml
    - make ci-test
  only:
    - merge_requests

gitlab-ci.ymlのポイント

dind(docker in docker)

DBをdockerで動かすので必須になります。

一部コマンドでのsudo

CI環境ではroot権限での実行が前提ではなかったため、必要最低限のコマンドに限定してsudoを利用しています。
make, curl, mysql-clientのインストールが必須です。
wait-for-it.shはリポジトリ管理していますが、CI環境で必要以上にsudoを使用しない運用としています。
※ 必要に応じて wait-for-it.shのインストールコマンドを記載すれば同じことが可能です。

wait-for-it.sh

公式リポジトリは以下です。こちらからwgetなどでシェルを取得します。
https://github.com/vishnubob/wait-for-it
docker-composeでDBの立ち上がりが遅いと後続のatlasによるマイグレーションが失敗します。その回避のため指定のホスト、ポートをリッスンして待つシェルです。

make

make atlas-install
冒頭に記載した、atlasのインストールコマンドになります。

make atlas-ci
CI上で実行するapplyコマンドです。

$ atlas schema apply --env "ci" --schema some_db --var tenant=some_db --auto-approve

ciのenvは以下です。
urlのほうにschema(some_db)まで記載すると、以下エラーが発生したため、エラー内容をただす形で(URLからschema除去)修正しています。
Error: *schema.ModifySchema is not allowed when migration plan is scoped to one schema

env "ci" {
    url = "mysql://user:pass@docker:3306/"
    src = "file://db/schemas"
}

make ci-test
これはgo testです。オプションは適宜指定してください。

$ go test ./... -race

cp sqlboiler_ci.toml sqlboiler.toml
これはlocalとciでhostが異なるためです。ci側の内容で上書きしています。

最後に

Atlas * sqlboilerの設定はこのようなものです。
Atlasによる理想状態の宣言によるスキーマ管理とあわせて紹介いたしました。

ご参考になれば幸いです!

横浜銀行(内製担当有志)Tech Blog

Discussion