goでmigrationをsingle binaryで提供する

goといえば、シングルバイナリだと思うんですが Databaseのマイグレーションはどのように適用していますでしょうか?

今回はgoでマイグレーションをシングルバイナリで適用する方法を考えてみたのでここに書いておきます。

はじめに

僕はJavaのWebフレームワークであるdropwizardが好きです。 このフレームワークは、アプリケーションの起動コマンドに db migrateみたいなサブコマンドが実装することができて 割とこの思想は好きでした。

アプリケーションと対応するマイグレーションがあるが マイグレーションは別で適用することができる、みたいな柔軟なところが素敵だなぁと思っていました。

モチベーション

モチベーションとしては、端的にいうとアプリケーションとマイグレーションはセットになって管理されて欲しいからです。 Dockerイメージならその要求はすぐ満たせるでしょう しかし、マイグレーションを適用する方法がdocker imageに入って専用のCLIツールで叩く、とかだとどうでしょうか。 想像してみると辛くないですか?

シングルバイナリでマイグレーションファイルもまとめてバイナリに入れておいて アプリケーションの実行ファイルからマイグレーション出来るとどうでしょうか? これならどこでもすぐ動かせるようになりますね。

docker imageがないと起動できない、とかもありません! 素晴らしいですね。 依存するミドルウェアさえあれば、対応するバイナリを落としてきて 実行するだけです!

じゃあどうやるか

サーバを起動する実行ファイルのサブコマンドとして migrateみたいなのを提供して マイグレーションファイルは静的ファイルをバイナリに含めるツールを使ってバイナリに含めてやれば良いです。

goの文脈では、CLIツール用のライブラリはいくつもあるのでサブコマンドを実装するのは簡単です。 また、静的ファイルをバイナリに含めるのもツールがいくつもあるので簡単です。 あとは組み合わせるだけです。

何を使うか

今回は以下のツールを使います。

今回実装する方針の内容は、rubenv/sql-migrateを使っても可能ですし 静的ファイルをバイナリに組み込む方法として、gobuffalo/packrを使っても良いです。 他にも組み合わせは色々あるとは思います。

ちなみに、rakyll/statikはfs.Readdir(0)の挙動が修正された0.17が最近リリースされたようなので 今回のマイグレーションファイルをシングルバイナリに埋め込んで golang-migrate/migrateの実装が可能になりました。 修正は結構前にされてたんですがリリースされてなくてモヤモヤした日々を過ごしていました。

ちなみに、リリースされているのを見つけたツイートがこちらです。

以前試した時は組み合わせが悪くて動かなかったんですが 動くようになって嬉しいですね。

どうやってやるか

こんな感じで出来ます。

import (
    "log"

    _ "statik-migrate/statik"

    "github.com/golang-migrate/migrate/v4"
    _ "github.com/golang-migrate/migrate/v4/database/postgres"
    "github.com/golang-migrate/migrate/v4/source/httpfs"
    _ "github.com/lib/pq"
    "github.com/rakyll/statik/fs"
)

//go:generate statik -src=./migrations
func migration() error {
    f, err := fs.New()
    if err != nil {
        return err
    }
    hfs, err := httpfs.New(f, "/")
    if err != nil {
        return err
    }
    m, err := migrate.NewWithSourceInstance("httpfs", hfs, "postgresql://postgres:postgres@localhost:15432/test?sslmode=disable")
    if err != nil {
        return err
    }
    defer m.Close()
    return m.Up()
}

本番用途で使うなら色んなことを考える必要はある気はしますが ベースはこのような形でマイグレーションを実行することができるはずです。

ちなみに本番用途で使うならどんな対応が必要になるか少し考えてみます。

ざっくりこんなところでしょうか。

まとめ

今回はgolang-migrate/migrateとrakyll/statikで マイグレーションをシングルバイナリで提供してみる方法を書きました。 いくつか課題はありつつも、アプリケーションとマイグレーションが固まって提供されることで アプリケーションとマイグレーションの整合性をうまく担保できるのではと思っています。

また、最近思っていることとして アプリケーションの外にマイグレーションを置いておいて マイグレーションだけ先に適用する、みたいな手法も考えた方が良いのかなと思っていたりもします。

これは、アプリケーションのリファクタリングとデータベースのリファクタリングはライフサイクルが違うので 例えば、先にカラムを追加しておいて、新しいアプリケーションがリリースされるまでトリガーを入れておいてデータベースのリファクタリングをする、みたいなことを考えたときに アプリケーションとセットでマイグレーションを提供している場合に、トリガーは手でメンテナンスすることになります。 もしくは、トリガーだけ先にリリースしておいて後で消す、みたいな対応を考えた場合 その間新しい機能の実装はどうするのでしょうか? git flowみたいなので考えると、hotfixで先にトリガーをリリースしておいて 新しい機能はdevelopブランチで実装していく、みたいなのはできる気はしますが 少し混乱しそうだなぁとか思いました。

追記: データーベースのマイグレーションで思っていること

最近、マイグレーションもgitops出来たらいいなぁみたいな気持ちがあリます。 今回の記事の調査で、golang-migrateを調べたところ、golang-migrateはmigrationのソースとしてgithubリポジトリを読むことができます。 これを使えば、DBのある環境にagentもしくはジョブでmigrationをgitopsできるのかなぁと思っています。

ただ、マイグレーションをgitopsする場合、複数の環境にmigrationを当てる、みたいな関係上 バージョン管理とかをうまくしないといけません。 これはマイグレーションのバージョンと適用するバージョンを別で管理しないといけなさそう。

なんか難しそうな話になってきたので寝る