Docker で golang-migrate を使う DB マイグレーション入門

アプリ開発がチーム化してくると、「誰がいつどんな SQL を流したのか分からない」という問題が必ず起きる。手元では動くのに、同僚の環境では users テーブルにカラムが足りない。ステージングに反映したつもりが一部漏れている。こうした事故の多くは、スキーマ変更を「手順書と口伝」で運用していることが原因になっている。

解決策はシンプルで、スキーマの変更履歴をファイルとしてコード管理し、ツールに順番通り適用させればいい。その用途で扱いやすいツールの一つが golang-migrate で、公式が Docker イメージを配布しているため Go のインストールすら不要で始められる。

目次

マイグレーションという考え方

マイグレーションとは、DB のスキーマ(テーブルやカラムの定義)の差分を、一つずつファイルとして積み上げていく運用のことを指す。000001_create_users.up.sql000002_add_email_to_users.up.sql といった連番ファイルを Git で管理し、ツールが「どこまで適用済みか」を DB 側の管理テーブルで記録してくれる。

up と down の役割

マイグレーションファイルは基本的に二本セットで作る。適用用の up.sql と、巻き戻し用の down.sql だ。

ファイル役割典型的な中身
xxx.up.sql変更を適用するCREATE TABLE, ALTER TABLE ADD COLUMN
xxx.down.sql変更を取り消すDROP TABLE, ALTER TABLE DROP COLUMN

down を書いておくと、間違えた変更を一段階戻せる。開発中に「このカラム名やっぱり変えたい」となったときの試行錯誤コストが大きく下がる。

golang-migrate を Docker で動かす仕組み

golang-migrate の Docker 版を使う場合、発想の切り替えが一つ必要になる。ツール本体を常駐させるのではなく、「SQL を流すたびに使い捨てコンテナを起動し、同じ Docker ネットワーク上の DB コンテナへ接続する」というスタイルになる。

Docker Network (example-app_default) migrate コンテナ –rm で使い捨て SQL を流して終了 DB コンテナ 常駐 example-app-db-1 SQL 適用 db/migrations/*.sql (ホスト)

ホスト側の db/migrations/ ディレクトリをコンテナ内の /migrations にマウントし、接続先に DB コンテナの名前(docker ps の NAMES に出る文字列)をそのままホスト名として指定する。コンテナ同士が同じネットワークにいれば、この名前で名前解決できる。

覚えておくべき二つの名前

Docker で動かすときに最初につまずくのがここなので、先に整理しておく。

  • ネットワーク名: docker network ls の結果に _default が付いた名前。Compose で起動した場合 プロジェクト名_default の形式になる
  • DB コンテナ名: docker ps の NAMES 列の値。Compose の場合 プロジェクト名-db-1 のような形式になる

この二つを接続文字列と --network オプションに正しく渡せれば、あとはコマンドの末尾を変えるだけで up も down も version 確認もできる。

実際に動かす

マイグレーションファイルの準備

プロジェクト直下に db/migrations/ を作り、連番でファイルを置く。

db/migrations/
├── 000001_create_users_table.up.sql
└── 000001_create_users_table.down.sql

中身は普通の SQL でよい。

-- 000001_create_users_table.up.sql
CREATE TABLE users (
  id    BIGINT AUTO_INCREMENT PRIMARY KEY,
  name  VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE
);
-- 000001_create_users_table.down.sql
DROP TABLE users;

DB コンテナを先に起動しておく

docker compose up db -d
docker ps
docker network ls

docker psdocker network ls で、前述した「DB コンテナ名」と「ネットワーク名」を確認する。

まずは接続確認

いきなり up を流す前に、末尾を version にして接続だけ試すのが安全だ。

docker run --rm \
  --network example-app_default \
  -v "$(pwd)/db/migrations:/migrations" \
  migrate/migrate \
  -path=/migrations/ \
  -database "mysql://appuser:apppass@tcp(example-app-db-1:3306)/appdb" \
  version

error: no migration と表示されれば成功で、「接続はできていて、まだ何も適用されていない」状態を意味する。ここでネットワーク名やホスト名の typo があると、接続エラーや名前解決エラーが返るので原因の切り分けがしやすい。

マイグレーションを適用する

接続確認が取れたら、末尾を up に変えるだけでいい。

docker run --rm \
  --network example-app_default \
  -v "$(pwd)/db/migrations:/migrations" \
  migrate/migrate \
  -path=/migrations/ \
  -database "mysql://appuser:apppass@tcp(example-app-db-1:3306)/appdb" \
  up

未適用の SQL がすべて順番に流れる。適用後にもう一度 version を実行すると、現在のバージョン番号が返ってくる。一つ戻したい場合は末尾を down 1 にする(引数なしの down は全削除になるので注意)。

次の一歩

このコマンドは毎回書くには長すぎるので、実務では Makefile やシェルスクリプトにまとめるのが定番になる。たとえば make migrate-upmake migrate-downmake migrate-version といったターゲットを用意しておけば、チームの誰でも同じ手順で流せるようになり、口伝運用から抜け出せる。

さらに進めるなら、CI 上でプルリクエスト時に「マイグレーションファイルの構文チェック」を走らせる、ステージング反映を自動化する、といった発展の余地がある。まずは手元で updown を何度か往復させて、壊して戻せる感覚をつかむところから始めるのが近道になる。

目次