こんにちは!スタンドアロンと聞くとどうしても攻殻機動隊を連想してしまう、しゅん(@MxShun)です。
今回は、Go 製のスタンドアロン DB マイグレーションツール bitbucket.org/liamstask/goose
から github.com/pressly/goose
へスムーズに移行するハウツーを書きます。
目次
はじめに
goose は、Go で作られた DB マイグレーションツールです。Go でできたスタンドアロンな DB マイグレーションツールとしては第一に候補として上がる、いわばデファクトスタンダードと言っても差し支えないでしょう。
簡単な例をあげます。
-- +goose Up
CREATE TABLE post (
id int NOT NULL,
title text,
body text,
PRIMARY KEY(id)
);
-- +goose Down
DROP TABLE post;
のような SQL ファイルを用意し、
$ goose up
$ goose down
と実行することで、それぞれ DB の更新と切戻しを適用できます。DB マイグレーションの適用状況はテーブルで管理、共有されます。
+---+--------------+----------+-------------------+
|id |version_id |is_applied|tstamp |
+---+--------------+----------+-------------------+
|1 |0 |1 |2018-09-15 12:00:00|
|2 |20180915120000|1 |2018-09-15 12:00:00|
|3 |20181001120000|1 |2018-10-01 12:00:00|
+---+--------------+----------+-------------------+
goose の主要なサプライヤには bitbucket.org/liamstask/goose
と、それをフォークした github.com/pressly/goose
があります。それぞれのプロジェクト指標は下記の通りです(執筆時点)。あくまで DB マイグレーションツールなのでベンチマークはありません;)
プロジェクト | スター数 | フォーク数 | 最終更新 |
---|---|---|---|
bitbucket.org/liamstask/goose | – (Bitbucket なのでスターなし) | 107 | 2018-03-01 |
github.com/pressly/goose | 3,558 | 388 | 2023-01-27 |
問題
上に書いたプロジェクト指標から分かる通り、bitbucket.org/liamstask/goose
は2018年3月1日の更新を最後に開発がストップしています。これに伴う問題の一つが、Apple silicon での非動作です。
$ goose -version
segmentation fault goose -version
Apple silicon 標準化の潮流において、bitbucket.org/liamstask/goose
利用者は漏れなく向き合わなければならない問題と言えます。幸いにもフォークプロジェクトの github.com/pressly/goose
はこの問題をクリアしており、移行コストの観点でも bitbucket.org/liamstask/goose
から github.com/pressly/goose
へ移行するのが自然な流れかと思います。
移行
github.com/pressly/goose
は、bitbucket.org/liamstask/goose
との違いを次のように整理しています。
- No config files
- Default goose binary can migrate SQL files only
- Go migrations:
- We don’t
go build
Go migrations functions on-the-fly
from within the goose binary- Instead, we let you create your own custom goose binary, register your Go migration functions explicitly and run complex migrations with your own
*sql.DB
connection- Go migration functions let you run your code within an SQL transaction, if you use the
*sql.Tx
argument- The goose pkg is decoupled from the binary:
- goose pkg doesn’t register any SQL drivers anymore, thus no driver
panic()
conflict within your codebase!- goose pkg doesn’t have any vendor dependencies anymore
- We use timestamped migrations by default but recommend a hybrid approach of using timestamps in the development process and sequential versions in production.
- Supports missing (out-of-order) migrations with the
-allow-missing
flag, or if using as a library supply the functional optiongoose.WithAllowMissing()
to Up, UpTo or UpByOne.- Supports applying ad-hoc migrations without tracking them in the schema table. Useful for seeding a database after migrations have been applied. Use
-no-versioning
flag or the functional optiongoose.WithNoVersioning()
.
取り立てて考えなければいけないのは、次の 3 つでしょう。
1. No config files
bitbucket.org/liamstask/goose
では慣例的に DB 設定郡を dbconf.yml
に外部定義し、これを利用します。
development:
driver: postgres
open: user=liam dbname=tester sslmode=disable
github.com/pressly/goose
では、文字通りこの設定ファイルが使えません。代わりに、これらを宣言的に実行する必要があります。
# Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND
$ goose postgres "user=liam dbname=tester sslmode=disable" up
これに対するスムーズな移行のアイデアとして、「シェルスクリプトをかませる」があります。具体的には、次のようなシェルファイルを用意します。なお、簡易的な例なので DB 認証情報はハードコーディングしていますが、実際には env ファイル等へ逃がすべきです。
DIR="./db/migrations"
DRIVER="postgres"
DBSTRING_LOCAL="user=liam dbname=tester sslmode=disable"
DBSTRING_DEVELOPMENT="user=liam dbname=tester_dev sslmode=disable"
DBSTRING_STAGING="user=liam dbname=tester_stg sslmode=disable"
DBSTRING_PRODUCTION="user=liam dbname=tester_prd sslmode=disable"
# https://github.com/pressly/goose
# NOTE: Macの利用しか想定していないので他プロセッサで利用する場合はバイナリと分岐の追加が必要
function up() {
if [ "$(uname -m)" == "arm64" ]; then
./db/bin/goose_darwin_arm64 -dir=${DIR} ${DRIVER} "${DBSTRING}" up
else
./db/bin/goose_darwin_x86_64 -dir=${DIR} ${DRIVER} "${DBSTRING}" up
fi
}
function down() {
if [ "$(uname -m)" == "arm64" ]; then
./db/bin/goose_darwin_arm64 -dir=${DIR} ${DRIVER} "${DBSTRING}" down
else
./db/bin/goose_darwin_x86_64 -dir=${DIR} ${DRIVER} "${DBSTRING}" down
fi
}
ENV=$(echo "$2" | tr '[:lower:]' '[:upper:]')
DBSTRING=$(eval echo '${DBSTRING_'"$ENV"'}')
$1
私が goose を移行したプロジェクトでは、もともと Makefile を利用してオペレーションのマニュアル・共通化をしていました。それも相まってユーザインタフェースを変更することなく移行できました。
goose-up:
- goose -env local up
+ @sh ./goose.sh up local
$ make goose-up
2. Go migrations
bitbucket.org/liamstask/goose
は上で述べたような SQL ファイルの他に、Go ファイルで定義したマイグレーションを実行することもできます。
package main
import (
"database/sql"
"fmt"
)
func Up_20130106222315(txn *sql.Tx) {
fmt.Println("Hello from migration 20130106222315 Up!")
}
func Down_20130106222315(txn *sql.Tx) {
fmt.Println("Hello from migration 20130106222315 Down!")
}
github.com/pressly/goose
ではデフォルトでこれができません。代わりに、カスタム goose アプリケーションのバイナリファイルを生成する必要があります。
- Move main.go into your
cmd/
directory- Rename package name in all
*_.go
migration files frommain
tomigrations
.- Import this
migrations
package from your custom cmd/main.go file:import ( // Invoke init() functions within migrations pkg. _ "github.com/pressly/goose/example/migrations-go" )
難点は、カスタム goose アプリケーションの生成とバージョン管理のコストが発生することでしょう。特殊なユースケースを除いて Go ファイルでマイグレーションを管理するべき理由はないように思えます。これを機に「SQL ファイルでマイグレーションを管理する方式に変更する」ことを検討してもよいかもしれません。
以上の 2 点を踏まえ、各環境で検証をすれば比較的スムーズに移行が可能かと思います。
3. Hybrid versioning
github.com/pressly/goose
が推奨している Hybrid versioning にも触れておきます。要約すると、そのコンセプトは「タイムスタンプと連番の両方を利用したバージョニング」です。
goose ではタイムスタンプベースのバージョニングが基本となっています。これは goose create
でマイグレーションファイルを生成した際、プレフィックスとしてタイムスタンプが付与されることから明らかです。
$ goose create add_some_column sql
Created new file: 20170506082420_add_some_column.sql
goose はマイグレーションファイル名の辞書順、つまりタイムスタンプが若いものから見てどこまで処理して、どこから処理していないかを管理しています。一見理にかなったバージョニング方式に思えますが、課題が指摘されています。
https://github.com/pressly/goose/issues/63#issuecomment-428681694
指摘となる例示は次のようなものです。
1. 次のような 2 つのブランチがあるとします
joe
ブランチ
migrations/2018-09-15-12:00:00_joe_1.sql
migrations/2018-10-10-12:00:00_joe_2.sql
alice
ブランチ
migrations/2018-10-01-12:00:00_alice_1.sql
migrations/2018-10-18-12:00:00_alice_2.sql
2. joe
ブランチを master
ブランチに先にマージします
+ migrations/2018-09-15-12:00:00_joe_1.sql
+ migrations/2018-10-10-12:00:00_joe_2.sql
3. goose up
を実行して master
ブランチから staging
環境に対してマイグレーションを実施します
$ goose up
goose: migrating db environment 'staging', current version: 0, target: 2
OK 2018-09-15-12:00:00_joe_1.sql
OK 2018-10-10-12:00:00_joe_2.sql
4. alice
ブランチを master
ブランチにマージします
migrations/2018-09-15-12:00:00_joe_1.sql
+ migrations/2018-10-01-12:00:00_alice_1.sql
migrations/2018-10-10-12:00:00_joe_2.sql
+ migrations/2018-10-18-12:00:00_alice_2.sql
5. goose up
を実行して master
ブランチから staging
環境に対してマイグレーションを実施します
ここが問題のポイントです。2018-10-10-12:00:00_joe_2.sql
までマイグレーションを適用した環境において、それよりも辞書順で先の 2018-10-01-12:00:00_alice_1.sql
はこのときの goose up
対象外となってしまいます。
$ goose up
goose: migrating db environment 'staging', current version: 0, target: 0
OK 2018-10-02-12:00:00_alice_2.sql
6. goose up
を実行して master
ブランチから production
環境に対してマイグレーションを実施します
$ goose up
goose: migrating db environment 'production', current version: 0, target: 2
OK 2018-09-15-12:00:00_joe_1.sql
OK 2018-10-01-12:00:00_alice_1.sql
OK 2018-10-02-12:00:00_joe_1.sql
OK 2018-10-18-12:00:00_alice_2.sql
7. staging
環境で未検証の 2018-10-01-12:00:00_alice_1.sql
が production
環境へ適用されたためアプリケーションがダウンしてしまいました
これを避けるため開発ブランチではタイムスタンプを利用したバージョニングを実施し、リリースブランチにマージされたタイミングで連番を振ってその連番に基づいてバージョニングをする手法が Hybrid versioning です。 先の例に Hybrid versioning を当てはめてみます。
2.
joe
ブランチをmaster
ブランチに先にマージします
のタイミングで、マイグレーションファイルに連番を振ります。
migrations/2018-09-15-12:00:00_joe_1.sql -> migrations/0001_2018-09-15-12:00:00_joe_1.sql
migrations/2018-10-10-12:00:00_joe_2.sql -> migrations/0002_2018-10-10-12:00:00_joe_2.sql
4.
alice
ブランチをmaster
ブランチにマージします
のタイミングでも、同じ要領でマイグレーションファイルに連番を振ります。
migrations/2018-10-01-12:00:00_alice_1.sql -> migrations/0003_2018-10-01-12:00:00_alice_1.sql
migrations/2018-10-18-12:00:00_alice_2.sql -> migrations/0004_2018-10-18-12:00:00_alice_2.sql
最終的には次のようになりますね。
migrations/0001_2018-09-15-12:00:00_joe_1.sql
migrations/0002_2018-10-10-12:00:00_joe_2.sql
migrations/0003_2018-10-01-12:00:00_alice_1.sql
migrations/0004_2018-10-18-12:00:00_alice_2.sql
これにより、
5.
goose up
を実行してmaster
ブランチからstaging
環境に対してマイグレーションを実施します
のタイミングでアプリケーションダウンを検知できるようになるという訳です。
上記はあくまで Hybrid versioning のアプローチの一つで、具体的な方式はチームやプロダクト単位で意思決定をする必要がありますね。また、goose blog Adding support for out-of-order migrations に描かれたブランチモデルを参考にするとより理解が深まると思います。
未適用マイグレーションの取り扱い
最後に github.com/pressly/goose
では言及のない、未適用マイグレーションの取り扱いの違いについても触れます。
冒頭で述べたとおり、goose では DB マイグレーションの適用状況をテーブルで管理します。これは bitbucket.org/liamstask/goose
と github.com/pressly/goose
共通の仕様です。
+---+--------------+----------+-------------------+
|id |version_id |is_applied|tstamp |
+---+--------------+----------+-------------------+
|1 |0 |1 |2018-09-15 12:00:00|
|2 |20180915120000|1 |2018-09-15 12:00:00|
|3 |20181001120000|1 |2018-10-01 12:00:00|
+---+--------------+----------+-------------------+
この状態から goose down
を実行して直前のマイグレーションが未適用の状態にロールバックしてみます。
bitbucket.org/liamstask/goose
では is_applied = false
となります。
+---+--------------+----------+-------------------+
|id |version_id |is_applied|tstamp |
+---+--------------+----------+-------------------+
|1 |0 |1 |2018-09-15 12:00:00|
|2 |20180915120000|1 |2018-09-15 12:00:00|
|3 |20181001120000|0 |2018-10-01 12:00:00|
+---+--------------+----------+-------------------+
github.com/pressly/goose
ではどうでしょう。レコード自体が削除されます。
+---+--------------+----------+-------------------+
|id |version_id |is_applied|tstamp |
+---+--------------+----------+-------------------+
|1 |0 |1 |2018-09-15 12:00:00|
|2 |20180915120000|1 |2018-09-15 12:00:00|
+---+--------------+----------+-------------------+
これに対応して goose up
は is_applied = false
のレコードを無視するようになっています。goose status
上は Pending となっているにも関わらず、です。
+---+--------------+----------+-------------------+
|id |version_id |is_applied|tstamp |
+---+--------------+----------+-------------------+
|1 |0 |1 |2018-09-15 12:00:00|
|2 |20180915120000|1 |2018-09-15 12:00:00|
|3 |20181001120000|0 |2018-10-01 12:00:00|
+---+--------------+----------+-------------------+
$ goose status
Applied At Migration
=======================================
Sat Sep 15 12:00:00 2018 -- 001_2018-09-15-12:00:00_joe_1.sql
Pending -- 2018-10-01-12:00:00_alice_1.sql
$ goose up
goose: no migrations to run. current version: 0
したがって、未適用マイグレーションがある状態で bitbucket.org/liamstask/goose
から github.com/pressly/goose
へ移行すると、適用されなかったマイグレーションがゾンビのように残存することになります。一度 is_applied = false
となったレコードを削除してからマイグレーションを実施しても構いませんが、すべてのマイグレーションを適用したクリーンな状態で bitbucket.org/liamstask/goose
から github.com/pressly/goose
へ移行したいものですね。
おわりに
今回は Go 製のスタンドアロン DB マイグレーションツール bitbucket.org/liamstask/goose
から github.com/pressly/goose
へスムーズに移行するハウツーを書きました。少しでも皆さんのツール移行の手助けとなっていれば幸いです。
サイバーエージェントには Go の資産がたくさんあり、より多くの Go エンジニアと一緒に働けることを期待しています。興味がある方はぜひ採用ページをご覧ください!