DBマイグレーションを手動コマンド実行からCloud Run Jobsに移行した話

## はじめに
DBマイグレーションをステージングや本番環境に適用すること、またCI/CDに組み込んで自動化するのはバックエンド／サーバーサイド開発で必ずといっていいほど通る道です。
開発体制やフェーズ、アーキテクチャによって最適解は様々変わってきますが、私が現職で適用した方法を紹介したいと思います。
ちなみに今回は「開発者のローカルから手動でコマンド実行」から「Cloud Run Jobs（Cloud Buildトリガー）を手動で実行」に移行できたまでで、完全自動化までは道半ばです。

## マイグレーションツールについて
弊プロダクトではもともとは[sqldef](https://github.com/k0kubun/sqldef)の`mysqldef`コマンドを使ってマイグレーションを適用していました。
もちろん`mysqldef`のみでもマイグレーションを行うことは可能でしたが、下記のような課題があり、別途マイグレーションツールの導入も決めました。
* DBマイグレーションで実行する SQL ファイルをリポジトリで管理できるようにする
  * どのようなSQL/DDLを実行したのかが明示的にわかるようにしたい
* DBマイグレーションの実行状況を管理できるようにする
  * 現状は記録が残せていないので、mysqldef で差分が出たときに実行漏れなのか、あるいは実行したけど想定通りに動いていないのかの区別がつかない

で、上記を満たすマイグレーションツールとして、sql-migrateを採用しました。
Flywayと迷っていたのですが、FlywayはMySQL5.7系は無料のCommunityEditionでは対応不可だったので断念せざるを得ず、、、
まぁsql-migrateは前職でも導入していて、CI/CD(CircleCI)にも組み込んで使っていたので、導入自体は簡単だったのでそこは良かったです🥳

[sql-migrate](https://github.com/rubenv/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ジョブで対処する](https://qiita.com/okonomi/items/b885c22e3dae4153f67d)

Cloud Run Jobsの起動オプション、特にCloud SQLの接続に関わるVPCコネクタやMySQLユーザー／パスワード周りの環境別の設定は、既存のAPIサービス用設定をそのまま使いまわせてめちゃくちゃラクでした。
ENV等書き換えて伏せていますが、CloudBuildからキックするCloud Run Jobsは下記のような設定です。
上記の参考URLにもあったとおり`image`のタグだけ最新のコミットハッシュにして、最新のDockerイメージを使うようにしているだけですね。
```yaml
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が叩ければ良いのでこちらもシンプルに下記のような形にしています。

```Dockerfile
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は下記となっています。
```yaml
# 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`とかになっています

## おわりに
いかがだったでしょうか？

弊プロダクトでは月１〜２回のリリースを行っており、リリースのたびにAPIチームがDBマイグレーションをローカルから行うというストレスがかかる作業に追われていましたが、今ではCloud Buildのトリガーをキックするだけになりました。

本当はリリース用のCloud Buildトリガーに組み込むところをゴールとしていたのですが、リリース前にマイグレーションが適用されるリスクや、リリース失敗による切り戻し等を考えて今は別のトリガーとして設定されています。
最終的にはリリース用トリガーと一体化したいなと思いつつ、まずは開発者体験改善の大きな一歩になったかなと思っています。
プロダクトを良くするのは当然として、プロダクトを開発する人たちの体験も、アーキテクチャの改善やCI/CDツールの活用でもっともっとあげていきたいですよね！

最後までお読みいただきありがとうございました😌

## 参考
- [Cloud RunのDBマイグレーションどうする問題にCloud Runジョブで対処する](https://qiita.com/okonomi/items/b885c22e3dae4153f67d)