Kubernetes の Job でマイグレーションを実行する
Posted on
Kubernetes にコンテナをデプロイするのは manifest ファイルを kubectl apply
で適用するだけなので、Capistrano や Fabric に比べればとてもシンプルです。ですが、データベースのマイグレーションまで自動化しようとすると途端に難しくなります。
今回は Kubernetes の Job でマイグレーションを実行する方法をご紹介します。他にもっといい方法があれば、ぜひ教えてください!
Kubernetes の Job
Kubernetes の公式サイト「Jobs - Run to Completion」から引用します。
A job creates one or more pods and ensures that a specified number of them successfully terminate. As pods successfully complete, the job tracks the successful completions. When a specified number of successful completions is reached, the job itself is complete. Deleting a Job will cleanup the pods it created.
Web アプリケーションのように常に動かし続けるのではなく、マイグレーションのように 1 回限りで終わる処理のためのオブジェクトです。 command
で指定した処理が exit 0
で終わると成功と見なして、その Pod を削除してくれます。
manifest ファイルは Pod とだいたい同じで、こんな感じになります。
apiVersion: batch/v1
kind: Job
metadata:
name: app
spec:
backoffLimit: 3
parallelism: 1
completions: 1
template:
spec:
containers:
- name: app
image: (snip)
command: ["rake", "db:migrate"]
restartPolicy: Never
parallelism
と completions
を 1 にすることで「1 回成功するまで実行」となります。ただし backoffLimit
を指定しているので、何らかの理由で command
が成功しなかった場合は 3 回までリトライします。なので、冪等性を持った処理でなければなりません。
マイグレーションが終わるまで待つ
通常のデプロイでは、アプリケーションを更新する前に確実にマイグレーションを終わらせる必要があります。また、マイグレーションが失敗した場合はすぐさまデプロイを中断させなければなりません。
ですが、Kubernetes の Job はコマンドを実行するだけなので、マイグレーションのステータスは自分たちでチェックする必要があります。この部分の実装が地味に面倒でした。
具体的には kubectl get job
で Job のステータスを取ってきて .status.succeeded
をチェックしています。今回はデプロイに CircleCI を使っているので bash で書きました。
#!/bin/bash
set -eux
job_name='xxxxx'
# Stop if job remain.
if kubectl get job ${job_name}; then
exit 1
fi
# Apply the job.
kubectl apply -f ./kubernetes/migration.yml
# Wait for the job to run.
while true; do
phase=$(kubectl get pod --selector="job-name=${job_name}" -o 'jsonpath={.items[0].status.phase}')
if [ "${phase}" != 'Pending' ]; then
break
fi
sleep 2
done
# Check the status of the job.
while true; do
is_active=$(kubectl get job ${job_name} -o 'jsonpath={.status.active}')
if [ "${is_active}" != '' ]; then
sleep 2
continue
fi
succeeded=$(kubectl get job ${job_name} -o 'jsonpath={.status.succeeded}')
if [ "${succeeded}" -eq 1 ]; then
break
else
exit 1
fi
done
# Delete the job.
kubectl delete -f ./kubernetes/migration.yml
.status.succeeded
が 1 以外のときは exit 1
になるので、それを受けて CircleCI のジョブが止まります。
実際のデプロイフローではこのスクリプトが終わるのを待ってから Deployment や Service の manifest ファイルを適用しています。こうすることで Capistrano と同じようにアプリケーションを更新する前に確実にマイグレーションを終わらせることができます。
まとめ
Kubernetes へのデプロイフローの中でマイグレーションを実行するには少し工夫が必要です。 Kubernetes には Init Containers のような機能もあるので、ゆくゆくはもっといい方法が出てくるかもしれません。