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

parallelismcompletions を 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 のような機能もあるので、ゆくゆくはもっといい方法が出てくるかもしれません。