概要
CIでデプロイの自動化はよく行うかと思いますが、データベースのマイグレーション作業も自動化したかったりします。
GCPを使っているとCIはCloud Build
でDBはCloud SQL
を使うケースがあるかと思いますが、同じGCPサービスながらCloud BuildからCloud SQLに接続する過程で色々苦労があったので、備忘録を残します。
ちなみにマイグレーションはgooseというライブラリを用いています。
構成
コードの構成は以下のようになっています。
database/ # データベース関連のファイル置き場。
db/
dbconf.yml
migrations/
2019xxxxxxxxxx_hogehoge.sql
2019yyyyyyyyyy_fugafuga.sql
docker/
Dockerfile.migrate # マイグレーション用のコマンドを記載したDockerfile
Dockerfileの中身は以下の通りです。
FROM golang:1.12-stretch as builder
RUN apt-get update && apt-get install -y \
git \
wait-for-it
ENV GO111MODULE on
RUN go get bitbucket.org/liamstask/goose/cmd/goose
# WORKDIRは自分のプロジェクトパスに設定
WORKDIR /go/src/github.com/kshibata101/sample-project/database
COPY . .
# envはdb/dbconf.ymlに書いているものを指定。今回はcloud_sqlへ接続する設定を用いる。
CMD goose -env cloud_sql up
db/dbconf.ymlに書いた内容を元にDBへの接続を試みます。
cloud_sql:
driver: mysql
open: $CLOUD_SQL_DSN
gooseはgo-sql-driver/mysqlを使っているようなので、openにはdsnの形式で記載します。
dsnにはパスワードなどの設定情報が含まれるため、環境変数としてCloud Buildから入れるようにします。
事前準備
- IAM
- Cloud BuildからCloud SQLへ接続するためにIAMでCloud Buildのサービスアカウントに対して、「Cloud SQL 管理者」を設定しておきます。
- Cloud SQL
- マイグレーション対象となるDBを作成します。
- このとき作成したインスタンスやDBの情報を後ほど使用します。
マイグレーション手順
今回の手順は似たようなことをしている記事があったため、そちらを参考にさせていただきました。
# https://stackoverflow.com/questions/52352103/run-node-js-database-migrations-on-google-cloud-sql-during-google-cloud-build/52366671#
steps:
- id: 'build'
name: 'gcr.io/cloud-builders/docker'
args: ['build', '--no-cache', '-f', 'docker/Dockerfile.migrate',
'-t', 'gcr.io/$PROJECT_ID/migration:$_TAG', 'database/']
- id: 'cloudsql-proxy'
name: 'gcr.io/cloudsql-docker/gce-proxy'
entrypoint: 'sh'
args: ['-c', '/cloud_sql_proxy -dir=/cloudsql -instances=$_CLOUD_SQL_CONNECTION_NAME & while [ ! -f /cloudsql/stop ]; do sleep 2; done']
volumes:
- name: cloudsql
path: /cloudsql
- id: 'migration'
name: 'gcr.io/cloud-builders/docker'
args: ['run', '-e', 'CLOUD_SQL_DSN=$_CLOUD_SQL_DSN',
'-v', 'cloudsql:/cloudsql', # volumeはhost側はpathではなくnameを指定しろとのこと
'gcr.io/$PROJECT_ID/migration:$_TAG']
volumes:
- name: cloudsql
path: /cloudsql
waitFor:
- 'build'
- name: 'gcr.io/cloud-builders/docker'
entrypoint: 'sh'
args: ['-c', 'touch /cloudsql/stop']
volumes:
- name: cloudsql
path: /cloudsql
waitFor:
- 'migration'
substitutions:
_TAG: 'latest'
_CLOUD_SQL_CONNECTION_NAME: '' # required
_CLOUD_SQL_DSN: '' # required
images:
- 'gcr.io/$PROJECT_ID/migration:$_TAG'
要点としては、Cloud SQL Proxyをwhile sleepで立ち上げておき、それと並列でマイグレーションの処理を実行するところ位ですが、順に見ていこうと思います。
ステップ1: docker build
- id: 'build'
name: 'gcr.io/cloud-builders/docker'
args: ['build', '--no-cache', '-f', 'docker/Dockerfile.migrate',
'-t', 'gcr.io/$PROJECT_ID/migration:$_TAG', 'database/']
初めにbuildを行います。
-t
で指定したタグ名については、gcr.io/$PROJECT_ID/以下は好きな名称を指定して問題ありません。
ステップ2: Cloud SQL Proxy
- id: 'cloudsql-proxy'
name: 'gcr.io/cloudsql-docker/gce-proxy'
entrypoint: 'sh'
args: ['-c', '/cloud_sql_proxy -dir=/cloudsql -instances=$_CLOUD_SQL_CONNECTION_NAME & while [ ! -f /cloudsql/stop ]; do sleep 2; done']
volumes:
- name: cloudsql
path: /cloudsql
waitFor:
- '-'
gce-proxyのクラウドビルダーを利用してCloud SQL Proxyを立ち上げます。
$_CLOUD_SQL_CONNECTION_NAME
にはCloud SQLのインスタンス接続名
を使います。
Cloud Buildのトリガー設定で代入変数に埋め込んでおきます。
立ち上げるコマンドがややトリッキーですが、Cloud Buildではステップが移ると立ち上げたプロセスが落ちてしまうので、単純なコマンドでは次のステップでproxy経由での接続ができません。
そこで、whileを使ってcloud_sql_proxyコマンドを立ち上げたままの状態を維持します。
ただし、whileをずっと残しておくと逆にいつまでもビルドが終わらないため、マイグレーションが終わったタイミングで /cloudsql/stop
にファイルを作成(ステップ4)しそれを検知して処理が終わるように仕向けます。
ファイルやディレクトリもステップ間で共有されないのですが、こちらはvolumesのオプションを使うことで共有できるようになります。
/cloudsql/stop
ファイルがあるか判定するため /cloudsql
をvolumesに指定しておきます。
waitForオプションはCloud Buildにおいてビルドの順列化・並列化をできるようにするものです。
通常は書いた順番に処理されていきますが、waitForを指定することで特定のビルドが終わったらこれを実行する、といったことができるようになります。
次のステップ3が始まる前にはproxyを立ち上げておきたいため、waitForに-
を指定します。
-
のみ指定するとビルド開始直後に実行されるようになります。
ステップ3: migration
- id: 'migration'
name: 'gcr.io/cloud-builders/docker'
args: ['run', '-e', 'CLOUD_SQL_DSN=$_CLOUD_SQL_DSN',
'-v', 'cloudsql:/cloudsql', # volumeはhost側はpathではなくnameを指定しろとのこと
'gcr.io/$PROJECT_ID/migration:$_TAG']
volumes:
- name: cloudsql
path: /cloudsql
waitFor:
- 'build'
先程ビルドしたdockerを起動することでマイグレーションを行います。
runの-eオプションで環境変数を渡せるため、DB接続に用いるDSN情報はCloud Buildの代入変数(ここでは $_CLOUD_SQL_DSN
)を利用して埋め込みます。
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
Cloud SQLへの接続は公式でCloud SQL Proxy経由で行うことが推奨されているため、dsnもunix domain socket形式で記述することになります。
dsn上ではprotocolの部分を unix
として記載します。
addressはproxyの立ち上げ場所にもよりますが、 今回はステップ2で/cloudsql
をdirと指定したため、 /cloudsql/{Cloud SQL インスタンス接続名}
が入ります。
最終的には以下のような値を$_CLOUD_SQL_DSN
に設定します。
username:password@unix(/cloudsql/project-id:asia-northeast1:cloud-sql-instance-id)/database?charset=utf8mb4&parseTime=True&loc=UTC
また気をつけなければならないのが-vオプションです。
Cloud SQLに接続するproxyはホスト側の /cloudsql
以下に作られていますが、コンテナ側に渡す際の -v
オプションでホスト側はpath
ではなくname
を指定する必要があります。
(Issueに言及あり)
つまりホスト側を/cloudsql
などのパス形式にしていると、うまく接続ができないので注意してください。
waitFor
のオプションについては、'build'を指定しているためステップ1が終わり次第実行されます。
前の項目で説明した通りステップ2はずっと実行され続けているため、ステップ2に依存する形にするといつまで経っても実行されません。ステップ1の後に実行されるようにします。
ステップ4: touch /cloudsql/stop
- name: 'gcr.io/cloud-builders/docker'
entrypoint: 'sh'
args: ['-c', 'touch /cloudsql/stop']
volumes:
- name: cloudsql
path: /cloudsql
waitFor:
- 'migration'
migrationが終わった後に/cloudsql/stopにファイルを作成します。
これをするとステップ2がファイルの存在を検知して終了し、ビルド全体が完了されます。
まとめ
ということでCloud BuildからCloud SQLのマイグレーションをする手順の紹介でした。
他にも方法はあると思いますが、多少強引でもCloud SQL Proxyを立ち上げてやる方法もあるよ、くらいに思っていただればいいかと思います。