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名を書いていない理由は後述。
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ファイル設定
以下はサンプルです。簡単な例ではありますが、テーブル定義は直感的です。
variable "tenant" {
type = string
description = "The name of the tenant (schema) to create"
}
schema "tenant" {
charset = "utf8mb4"
collate = "utf8mb4_general_ci"
name = var.tenant
}
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
}
}
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は公式にて以下のように記述しています。
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上で動かしているので、それに則ります。
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
などでシェルを取得します。
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による理想状態の宣言によるスキーマ管理とあわせて紹介いたしました。
ご参考になれば幸いです!
Discussion