はじめに
DBマイグレーションをステージングや本番環境に適用すること、またCI/CDに組み込んで自動化するのはバックエンド/サーバーサイド開発で必ずといっていいほど通る道です。
開発体制やフェーズ、アーキテクチャによって最適解は様々変わってきますが、私が現職で適用した方法を紹介したいと思います。
ちなみに今回は「開発者のローカルから手動でコマンド実行」から「Cloud Run Jobs(Cloud Buildトリガー)を手動で実行」に移行できたまでで、完全自動化までは道半ばです。
マイグレーションツールについて
弊プロダクトではもともとはsqldefのmysqldef
コマンドを使ってマイグレーションを適用していました。
もちろんmysqldef
のみでもマイグレーションを行うことは可能でしたが、下記のような課題があり、別途マイグレーションツールの導入も決めました。
- DBマイグレーションで実行する SQL ファイルをリポジトリで管理できるようにする
- どのようなSQL/DDLを実行したのかが明示的にわかるようにしたい
- DBマイグレーションの実行状況を管理できるようにする
- 現状は記録が残せていないので、mysqldef で差分が出たときに実行漏れなのか、あるいは実行したけど想定通りに動いていないのかの区別がつかない
で、上記を満たすマイグレーションツールとして、sql-migrateを採用しました。
Flywayと迷っていたのですが、FlywayはMySQL5.7系は無料のCommunityEditionでは対応不可だったので断念せざるを得ず、、、
まぁsql-migrateは前職でも導入していて、CI/CD(CircleCI)にも組み込んで使っていたので、導入自体は簡単だったのでそこは良かったです🥳
sql-migrateについて詳しい説明や使用感はここでは割愛しますが、Go製なこともあり、私が関わったGoのバックエンドのプロジェクトでは必ずといっていいほど採用されていましたね。
気になるかたは公式見てみると良いと思います!
Cloud BuildからCloud Run Jobsでの実行
前述の通り今までのDBマイグレーションは開発者がローカル環境からCloud SQL Proxyで各環境のCloud SQLに接続してmake
経由でmysqldef
コマンドを叩いていました。
手動だとどうしてもヒューマンエラーがあり、また作業する開発者のストレスもそれなりにあるので、できるだけCIツールなりで半自動化したいところです。
弊プロダクトのAPIサーバーはCI/CDでCloudBuildを利用してビルド&デプロイを行っていたので、APIサーバーのデプロイステップの手前にDBマイグレーションステップを差し込めないかなぁーと考えていたところ、下記のような記事を見て、なるほどCloudRunJobsをキックすれば良いのか!とこちらを採用しました。
Cloud RunのDBマイグレーションどうする問題にCloud Runジョブで対処する
Cloud Run Jobsの起動オプション、特にCloud SQLの接続に関わるVPCコネクタやMySQLユーザー/パスワード周りの環境別の設定は、既存のAPIサービス用設定をそのまま使いまわせてめちゃくちゃラクでした。
ENV等書き換えて伏せていますが、CloudBuildからキックするCloud Run Jobsは下記のような設定です。
上記の参考URLにもあったとおりimage
のタグだけ最新のコミットハッシュにして、最新のDockerイメージを使うようにしているだけですね。
gcloud beta run jobs update db-migration \
--region=$REGION \
--vpc-connector=$VPC_CONNECTOR \
--vpc-egress=$VPC_EGRESS \
--service-account=$SERVICE_ACCOUNT \
--image=gcr.io/prj-id/schema:$COMMIT_SHA \
--task-timeout=$TASK_TIMEOUT \
--tasks=1 \
--parallelism=1 \
--max-retries=0 \
--execute-now \
あとはDockerfileですが、sql-migrateが叩ければ良いのでこちらもシンプルに下記のような形にしています。
FROM golang:1.18-alpine
RUN apk add --update --no-cache ca-certificates gcc musl-dev make
WORKDIR /go/project-dir/schema
RUN go install github.com/rubenv/sql-migrate/...@latest
CMD ["sql-migrate", "up", "-config=migration/config.yml", "-env=cloudrunjobs"]
project-dir/schema
配下にあるmigration/config.yml
を指定してsql−mirate up
をかけているだけですね。
sql-migrate
のenv指定はcloudrunjobs
としています。
sql-migrateのconfig.ymlは下記となっています。
# Cloud Run Jobsでの実行用
cloudrunjobs:
dialect: mysql
datasource: ${SERVER_MYSQL_USER}:${SERVER_MYSQL_PASSWORD}@tcp(${SERVER_MYSQL_HOST}:${SERVER_MYSQL_PORT})/${SERVER_MYSQL_DATABASE}?parseTime=true&charset=utf8mb4
dir: migration/sql
table: migration_history
# 開発者ローカルでの実行用
development:
dialect: mysql
datasource: ${USER}:${PASSWORD}@tcp(${HOST}:${PORT})/${DB}?parseTime=true&charset=utf8mb4
dir: migration/sql
table: migration_history
ローカルとClour Run Jobsでは利用しているENV名が違うためにそれぞれ別の定義としていますが、sql-migrateの便利な点として、実行時に-env
オプションを指定しない場合はデフォルトでconfig.yml
内のdevelopment
ブロックの設定を採用してくれるところです。
これにより、開発者ローカル実行時はきわめて簡素なsql-migrate up
だけで済むようにしています。
※ほんとはconfigファイル名もデフォルトから変えているのでsql-migate up -config=migration/config.yml
になります
※make経由で叩いているのでさらに簡素にmake schema.migrate
とかになっています
おわりに
いかがだったでしょうか?
弊プロダクトでは月1〜2回のリリースを行っており、リリースのたびにAPIチームがDBマイグレーションをローカルから行うというストレスがかかる作業に追われていましたが、今ではCloud Buildのトリガーをキックするだけになりました。
本当はリリース用のCloud Buildトリガーに組み込むところをゴールとしていたのですが、リリース前にマイグレーションが適用されるリスクや、リリース失敗による切り戻し等を考えて今は別のトリガーとして設定されています。
最終的にはリリース用トリガーと一体化したいなと思いつつ、まずは開発者体験改善の大きな一歩になったかなと思っています。
プロダクトを良くするのは当然として、プロダクトを開発する人たちの体験も、アーキテクチャの改善やCI/CDツールの活用でもっともっとあげていきたいですよね!
最後までお読みいただきありがとうございました😌