Go製モダンマイグレーションツールのAtlasを使用してみた
GoでDBマイグレーションをどうやるのがいいか検討していたところAtlasというツールが良さそうだったので一通り使ってみたまとめです。
対象読者
- GoプロジェクトのDBマイグレーションに興味がある人
- Atlasの使い方に興味がある人
Atlasとは
Atlasはデータベーススキーマを管理するためのツールおよびwebサービスを提供しています。従来までのバージョンによるマイグレーション管理だけでなくDBの理想状態を宣言的に記述することでスキーマを反映することができます。DBの理想状態はHCL、sql、jsonがサポートされています。
Atlasは非常に高機能でCLIツール、GitHub ActionsのようなCI、Terraformプロバイダーなどを提供しています。
Go製のORMであるentで採用されているマイグレーションツールですがent以外のORMと合わせて使用できますし、go-migrateやgooseのような他のマイグレーションツールとも組み合わせて使用することができます。
install
curl -sSf https://atlasgo.sh | sh
他にもmacであればHomebrewなどでもインストール可能
DBの検査
atlasコマンドを使ってローカル環境のDBを検査し、スキーマファイルを作成してみたいと思います。まずは以下のコマンドを実行してローカルにMySQLコンテナを起動します。
docker run --rm -d --name atlas-demo -p 13306:3306 -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=example mysql
起動できたら以下のコマンドを実行してテーブルを作成します。
docker exec atlas-demo mysql -ppass -e 'CREATE table example.users(id int PRIMARY KEY, name varchar(100))'
実行できたらatlas schema inspect
コマンドを実行してDBを検査し、スキーマファイルを作成します。
atlas schema inspect -u "mysql://root:pass@localhost:13306/example" > schema.hcl
table "users" {
schema = schema.example
column "id" {
null = false
type = int
}
column "name" {
null = true
type = varchar(100)
}
primary_key {
columns = [column.id]
}
}
schema "example" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
出力するファイルは他にSQLとJSON形式で出力することも可能です。
atlas schema inspect -u "mysql://root:pass@localhost:13306/example" --format '{{ sql . }}' | cat
-- Create "users" table
CREATE TABLE `users` (`id` int NOT NULL, `name` varchar(100) NULL, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
DBスキーマの変更を反映する
上記で作成したusersテーブルに紐づくブログ投稿テーブルを追加することを考えてみます。schema.hcl
に以下のようにテーブルを追加します。
table "blog_posts" {
schema = schema.example
column "id" {
null = false
type = int
}
column "title" {
null = true
type = varchar(100)
}
column "body" {
null = true
type = text
}
column "author_id" {
null = true
type = int
}
primary_key {
columns = [column.id]
}
foreign_key "author_fk" {
columns = [column.author_id]
ref_columns = [table.users.column.id]
}
}
宣言的マイグレーション
Atlasのマイグレーション方法は2種類存在し、従来のバージョンによるマイグレーションと理想状態を宣言的に書いて管理する方法があります。宣言的マイグレーションはより現代的で分業化が進んだ現代の開発現場においてアプリケーションエンジニアはDBのあるべき姿だけを考えることができその理想状態を稼働中のインフラ環境に反映する部分はインフラエンジニアが担当することができます。
どちらが優れているということはなく、どちらの方法でもAtlasを使用できるようになっているので所属する組織やチームの状況を見て選択すればいいと思います。
以下は宣言的マイグレーションの実施例です。
atlas schema apply \
-u "mysql://root:pass@localhost:13306/example" \
--to file://schema.hcl
-- Planned Changes:
-- Create "blog_posts" table
CREATE TABLE `blog_posts` (
`id` int NOT NULL,
`title` varchar(100) NULL,
`body` text NULL,
`author_id` int NULL,
PRIMARY KEY (`id`),
CONSTRAINT `author_fk` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`)
);
? Are you sure?:
▸ Apply
Lint and edit
Abort
-
-u
マイグレーション実行対象のDBです。上記の例だとローカルで起動しているDBコンテナを指しています。 -
--to
理想状態。上記の例だとスキーマファイルを指定している。
Applyを選択することでDBにスキーマが反映されているはずです。スキーマが反映されていることをAtlas CloudのWebUIで視覚的に確認することもできます。
% atlas schema inspect \
-u "mysql://root:pass@localhost:13306/example" \
--web
? Where would you like to share your schema visualization?:
▸ Publicly (gh.atlasgo.cloud)
Privately (junichi-yamanaka.atlasgo.cloud)
スキーマをPublicに共有するかPrivateにするか選択できるので好きな方を選択してください。なお、Atlasのフリープランでスキーマの共有はできます。アカウントの作成が必要であれば事前にatlas cloudのアカウントをこちらから作成してください。
参考までにPublicに公開したスキーマがこちらです
バージョンマイグレーション
従来方のバージョンによるマイグレーション方法も使用できます。バージョンによるマイグレーション管理をする場合は以下のようなatlas migrate
コマンドを実行します。
atlas migrate diff create_blog_posts \
--dir "file://migrations" \
--to "file://schema.hcl" \
--dev-url "docker://mysql/8/example"
-
--dir
マイグレーションファイルを配置するディレクトリを指定 -
--to
理想状態。ここで指定したスキーマとの差分をマイグレーションファイルとして生成する。 -
--dev-url
マイグレーションファイルを生成する過程で一時的にdockerを使用します。上記の例ではAtlasが用意している特別なdockerドライバを使用して一時的な環境を用意し使用している。
コマンドが成功すると以下のようにファイルが生成されている。
migrations
├── 20240321134630_create_blog_posts.sql
└── atlas.sum
-- Create "users" table
CREATE TABLE `users` (`id` int NOT NULL, `name` varchar(100) NULL, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Create "blog_posts" table
CREATE TABLE `blog_posts` (`id` int NOT NULL, `title` varchar(100) NULL, `body` text NULL, `author_id` int NULL, PRIMARY KEY (`id`), INDEX `author_fk` (`author_id`), CONSTRAINT `author_fk` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
h1:D890RldtkzxGSTnkN1Cb4+DK8T43H2/5xxJL/CFlDp4=
20240321134630_create_blog_posts.sql h1:I2bENIDZBusIADyoJmXX3Ff/yI6Hvj8uLC5GaBcU67s=
マイグレーションファイルが生成されたので以下のようにコマンドを実行してDBにマイグレーション内容を反映できます。
atlas migrate apply \
--url "mysql://root:pass@localhost:13306/example" \
--dir "file://migrations"
Migrating to version 20240321134630 (1 migrations in total):
-- migrating version 20240321134630
-> CREATE TABLE `users` (`id` int NOT NULL, `name` varchar(100) NULL, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-> CREATE TABLE `blog_posts` (`id` int NOT NULL, `title` varchar(100) NULL, `body` text NULL, `author_id` int NULL, PRIMARY KEY (`id`), INDEX `author_fk` (`author_id`), CONSTRAINT `author_fk` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- ok (50.749209ms)
-------------------------
-- 72.911125ms
-- 1 migration
-- 2 sql statements
(前回までの内容が残っているとうまくいかないかもしれないので、その場合は一旦DBを初期化してから試してみてください。)
宣言的マイグレーション vs バージョン管理型のマイグレーション
宣言的マイグレーションの利点はアプリケーション開発エンジニアとインフラエンジニアの間で作業を完全に分業できることです。アプリケーションエンジニアはDBの理想状態のことだけ考えればよく現在のスキーマからどのようにその状態に近づけようとということは考えなくて済みます。インフラエンジニアはアプリケーションエンジニアが構築したDBスキーマをどう反映するかを考えればよく、そのためのガイドラインをAtlasはドキュメントとして公開してくれています。
バージョン管理型のマイグレーションはRailsやLaravelといったフルスタックフレームワークのワークフローに組み込まれていたり、FlywayといったOSSがあったりと古くからプログラミング言語を問わず使われてきた手法だと思います。この手法のメリットはDBスキーマの移行計画をバージョン管理に含めることができ、アプリケーションコードとともにレビューができることです。DBのスキーマ変更はシステム開発の中でもかなり神経を使う部分なのでこのようにレビューができるようになっていることを好む組織やチームも多いでしょう。しかし、欠点としては移行計画の負担がアプリケーションエンジニアにかかることです。簡単なマイグレーションであればいいですがもしかしたらDBに関する深い知識を求められるかもしれません。
Atlasの公式には第3の手法として上記2つの手法を組み合わせたVersioned Migration Authoring(訳し方がわかなかったので原文そのまま)を提唱している。これは、チーム内でレビューできるようにバージョン管理をするがその移行計画はあるべきDBの理想状態を宣言的に作成し、その理想状態に近づけるような移行計画はAtlasを使い自動で生成するという手法です。公式ドキュメントには以下のように記載されています。
バージョン管理されたマイグレーション・オーサリングでは、ユーザーは希望する状態を宣言し、アトラス・エンジンを使って既存の状態から新しい状態への安全なマイグレーションを計画します。しかし、プランニングと実行を連動させる代わりに、プランは通常のマイグレーションファイルに書き込まれ、ソースコントロールにチェックインし、手動で微調整し、通常のコードレビュープロセスでレビューすることができます。
個人的に使ってみた感想ですがAtlasのCLIは柔軟性がかなりあって、理想状態の指定の仕方はスキーマファイルだけでなく実際のDBを指定することもでき、組織やチーム内にあった使い方を模索する感じがいいんじゃないかなと思いました。ただ、理想状態であるスキーマファイルから移行計画を自動で生成する体験はかなりよかったです。
設定ファイルの作成
Atlasを使用した開発のプロジェクト設定ファイルをatlas.hcl
という形式で作成することができます。
/ Define an environment named "local"
env "local" {
// Declare where the schema definition resides.
// Also supported: ["file://multi.hcl", "file://schema.hcl"].
src = "file://schema.hcl"
// Define the URL of the database which is managed
// in this environment.
url = "mysql://root:pass@localhost:13306/example"
// Define the URL of the Dev Database for this environment
// See: https://atlasgo.io/concepts/dev-database
dev = "docker://mysql/8/dev"
}
env "dev" {
// ... a different env
}
atlas schema inspect --env local
atlas schema apply --env local
上記のようにlocalとdevのような複数環境の設定を書くことができる。
AtlasはHCLを用いてこういった設定やテーブル定義をごりごり書くことができる。ファイル内で宣言した変数や外部からの入力値を使用したり、よりプログラム的にDBを管理することができます。本記事では触れませんがTerraformプロバイダも提供されており、インフラ領域で宣言的に管理することができそうです。
Go SDKの使用
AtlasはGo製のツールですがCLIツールとして提供されているためプログラミング言語を問わず使用することができます。しかし、atlasexec
という薄いラッパーが用意されているためGoのプロジェクトから利用することができるようにもなっています。
以下、簡単にatlasexec
を使用した例です。
mkdir go-sdk-demo
cd go-sdk-demo
go mod init go-sdk-demo
go get ariga.io/atlas-go-sdk/atlasexec
atlas migrate new --edit create_users
エディタが立ち上がるので以下のようにテーブル定義を書きます。
CREATE TABLE users (
id int PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
エディタを閉じると以下のようにマイグレーションファイルが作成されていると思います。
% tree migrations
migrations
├── 20240322135013_create_users.sql
└── atlas.sum
main.goを以下のように作成します。
package main
import (
"context"
"fmt"
"log"
"os"
"ariga.io/atlas-go-sdk/atlasexec"
)
func main() {
// Define the execution context, supplying a migration directory
// and potentially an `atlas.hcl` configuration file using `atlasexec.WithHCL`.
workdir, err := atlasexec.NewWorkingDir(
atlasexec.WithMigrations(
os.DirFS("./migrations"),
),
)
if err != nil {
log.Fatalf("failed to load working directory: %v", err)
}
// atlasexec works on a temporary directory, so we need to close it
defer workdir.Close()
// Initialize the client.
client, err := atlasexec.NewClient(workdir.Path(), "atlas")
if err != nil {
log.Fatalf("failed to initialize client: %v", err)
}
// Run `atlas migrate apply` on a SQLite database under /tmp.
res, err := client.MigrateApply(context.Background(), &atlasexec.MigrateApplyParams{
URL: "sqlite:///tmp/demo.db?_fk=1&cache=shared",
})
if err != nil {
log.Fatalf("failed to apply migrations: %v", err)
}
fmt.Printf("Applied %d migrations\n", len(res.Applied))
}
作成できたら以下のようにして実行します。
go run main.go
Applied 1 migrations
Go SDKの使いどころですが、テストコードを書く時にTestMainにマイグレーション処理を書くことでテストを始める際に完全なスキーマを用意することができます。ORMであるentも内部的にはGoのSDKを使ってentの関数からマイグレーションができるようになっているようです。
docker-composeの使用
Go SDKを紹介しましたがテストコードを書く時にできればまっさらなDB環境を外部依存なく作成したいですよね??
Goではdockertestやtestcontainers-goを利用することでテストコードにコンテナの起動・破棄を簡単に組み込むことができます。
最近Docker社がTestcontainersの開発元を買収したのが関係あるかはわかりませんがtestcontainers-goの開発が盛んなようにも見えるのと、docker-composeの利用ができるようになっていたので今回はtestcontainers-goをAtlasとともに使用する例も紹介してみたいと思います。
ローカル開発用にDBコンテナを用意する
マイグレーションファイルはここまでで作成したものを使いまわします。
tree migrations
migrations
├── 20240321134630_create_blog_posts.sql
└── atlas.sum
次にdocker-composeファイルを以下のように作成します。
version: "3.9"
services:
mysql:
image: mysql:8.0.29
platform: linux/amd64
healthcheck:
test: mysqladmin ping -ppass
environment:
MYSQL_DATABASE: test
MYSQL_ROOT_PASSWORD: pass
ports:
- "13306:3306"
networks:
- db
migrate:
image: arigaio/atlas:latest
command: >
migrate apply
--url mysql://root:pass@mysql:3306/test
networks:
- db
depends_on:
mysql:
condition: service_healthy
volumes:
- ./migrations/:/migrations
networks:
db:
docker compose up -d
...
[+] Running 3/3
✔ Network atlas-testcontainers-demo_db Created 0.3s
✔ Container atlas-testcontainers-demo-mysql-1 Healthy 0.7s
✔ Container atlas-testcontainers-demo-migrate-1 Started
これでローカル開発用にDBを作成できたうえに、DBスキーマもマイグレーションが実行され用意できてるはずです!確認してみます。
docker compose exec mysql mysql -uroot --port 13306 -ppass test -e 'show tables;'
mysql: [Warning] Using a password on the command line interface can be insecure.
+------------------------+
| Tables_in_test |
+------------------------+
| atlas_schema_revisions |
| blog_posts |
| users |
+------------------------+
ちゃんとテーブルが作成されています!
テスト用のコンテナをdocker-composeから用意する
まずは以下のようにGoプロジェクトを作成します。
mkdir atlas-testcontainers-demo
cd atlas-testcontainers-demo
go mod init atlas-testcontainers-demo
testcontainers-goのcomposeモジュールをインストールします。
go get github.com/testcontainers/testcontainers-go/modules/compose
compose.yaml
は前述のものをそのまま使いたいがローカル開発用とは別に起動して使いたいのでもう一つ用意します。ポートやmigreationsファイルの指定だけ変更してます。
version: "3.9"
services:
mysql:
image: mysql:8.0.29
platform: linux/amd64
healthcheck:
test: mysqladmin ping -ppass
environment:
MYSQL_DATABASE: test
MYSQL_ROOT_PASSWORD: pass
ports:
- "23306:3306"
networks:
- db
migrate:
image: arigaio/atlas:latest
command: >
migrate apply
--url mysql://root:pass@mysql:3306/test
networks:
- db
depends_on:
mysql:
condition: service_healthy
volumes:
- ../migrations/:/migrations
networks:
db:
compose.yaml
が用意できたら以下のようなテストファイルを用意します。
package db_test
import (
"context"
"fmt"
"log"
"os"
"testing"
tc "github.com/testcontainers/testcontainers-go/modules/compose"
)
func TestMain(m *testing.M) {
compose, err := tc.NewDockerCompose("testdata/compose.yaml")
if err != nil {
fmt.Println(err.Error())
log.Fatal(err)
}
downFunc := func() error {
return compose.Down(context.Background(), tc.RemoveOrphans(true), tc.RemoveImagesLocal)
}
ctx, cancel := context.WithCancel(context.Background())
// コンテナの起動
if err = compose.Up(ctx, tc.Wait(true)); err != nil {
fmt.Println(err.Error())
cancel()
_ = downFunc() // 手抜き
log.Fatal(err)
}
code := m.Run()
// 後処理
cancel()
if err = downFunc(); err != nil {
log.Fatal(err)
}
os.Exit(code)
}
func TestDB(t *testing.T) {
t.Log("Start test!!")
}
go test ./... -test.v
# github.com/fsnotify/fsevents
cgo-gcc-prolog:454:2: warning: 'FSEventStreamScheduleWithRunLoop' is deprecated: first deprecated in macOS 13.0 - Use FSEventStreamSetDispatchQueue instead. [-Wdeprecated-declarations]
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/CoreServices.framework/Frameworks/FSEvents.framework/Headers/FSEvents.h:1153:1: note: 'FSEventStreamScheduleWithRunLoop' has been explicitly marked deprecated here
...
=== RUN TestDB
db_test.go:46: Start test!!
--- PASS: TestDB (0.00s)
PASS
Container e395fbe6-c9b1-43ac-8711-bc2c04541c6c-migrate-1 Stopping
Container e395fbe6-c9b1-43ac-8711-bc2c04541c6c-migrate-1 Stopped
Container e395fbe6-c9b1-43ac-8711-bc2c04541c6c-migrate-1 Removing
Container e395fbe6-c9b1-43ac-8711-bc2c04541c6c-migrate-1 Removed
Container e395fbe6-c9b1-43ac-8711-bc2c04541c6c-mysql-1 Stopping
Container e395fbe6-c9b1-43ac-8711-bc2c04541c6c-mysql-1 Stopped
Container e395fbe6-c9b1-43ac-8711-bc2c04541c6c-mysql-1 Removing
Container e395fbe6-c9b1-43ac-8711-bc2c04541c6c-mysql-1 Removed
Network e395fbe6-c9b1-43ac-8711-bc2c04541c6c_db Removing
Network e395fbe6-c9b1-43ac-8711-bc2c04541c6c_default Removing
Network e395fbe6-c9b1-43ac-8711-bc2c04541c6c_db Removed
Network e395fbe6-c9b1-43ac-8711-bc2c04541c6c_default Removed
ok atlas-testcontainers-demo 36.589s
このようにTestMainに書くことでテスト実行前にマイグレーションを実行したDBコンテナを起動し、テストの終了時にコンテナを自動で破棄します。この処理をテスト関数の中で書くこともできますが毎回起動と終了を繰り返すとテスト時間がかなり伸びてしまうのでTestMainに書きました。そのためテスト間でDBを共有することになるためそれぞれのテストが影響しないようにデータの後始末は確実にするのが望ましいです。
おわりに
本記事ではGo製のモダンマイグレーションツールのAtlasを紹介しました!けっこう長くなってしまいましたがまだまだ試せていないことが多く、Atlasが非常に高機能なことがわかります。具体的には以下の内容については本記事では触れられていません。
- k8sエコシステムとの統合(ArgoやHelm)
- GitHub ActionsなどのCI環境への組み込み
- TerraformによるDBスキーマの管理
- GORMやgolang-migrate, gooseなどのGo製の各種ツールとの組み合わせ
- スキーマの更新->push->マイグレーション作成->CI/CD->DBスキーマの反映といったDBのパイプライン作成
AtlasはGo製ですがメインはCLIツールとhclおよびsqlファイルによるスキーマ管理のためプログラム言語を問いません!従来のバージョン管理型のマイグレーションから宣言的なスキーマ管理、自動で移行計画を作成してくれるのは非常に開発体験が良かったです。
個人的にはDBを含めた結合テストはtestcontainersのようなライブラリを利用してテスト用のコンテナを起動したいのとローカル開発用にもDBコンテナを用意したく、Atlasのdockerイメージを用いたdocker-composeとtestcontainers-goのcomposeモジュールを使うことで両方とも手に入るのがかなり嬉しいです。
自前でマイグレーション処理を書けばいいのですがテストコードにテスト以外の処理を極力書きたくないのでdocker-composeファイルを指定して起動都愛具レーション実行できるのがいいなと思っています。
ということでテスト体験をよくするためにもAtlasを使ってみてはいかがでしょうか!あとtestcontainers!
今回は以上です🐼
(余談)
本当はsqlcとAtlasの組み合わせの開発体験記事を書こうと思ったのですがAtlasの書くことが多すぎてsqlcのこと書けませんでした。sqlcは多くのマイグレーションツールと合わせて使用することができますがAtlasもサポートしていますよ!
Discussion