アプリ開発がチーム化してくると、「誰がいつどんな SQL を流したのか分からない」という問題が必ず起きる。手元では動くのに、同僚の環境では users テーブルにカラムが足りない。ステージングに反映したつもりが一部漏れている。こうした事故の多くは、スキーマ変更を「手順書と口伝」で運用していることが原因になっている。
解決策はシンプルで、スキーマの変更履歴をファイルとしてコード管理し、ツールに順番通り適用させればいい。その用途で扱いやすいツールの一つが golang-migrate で、公式が Docker イメージを配布しているため Go のインストールすら不要で始められる。
マイグレーションという考え方
マイグレーションとは、DB のスキーマ(テーブルやカラムの定義)の差分を、一つずつファイルとして積み上げていく運用のことを指す。000001_create_users.up.sql、000002_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 コンテナへ接続する」というスタイルになる。
ホスト側の 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 lsdocker ps と docker 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" \
versionerror: 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-up、make migrate-down、make migrate-version といったターゲットを用意しておけば、チームの誰でも同じ手順で流せるようになり、口伝運用から抜け出せる。
さらに進めるなら、CI 上でプルリクエスト時に「マイグレーションファイルの構文チェック」を走らせる、ステージング反映を自動化する、といった発展の余地がある。まずは手元で up と down を何度か往復させて、壊して戻せる感覚をつかむところから始めるのが近道になる。