ArgoCDで、DBのマイグレーションが完了してからデプロイする


背景

私が所属しているプロジェクトのリリースフローは、大まかに以下の順序で行われます。
1. DBのマイグレーション専用ブランチにマージ
2. CloudBuildで、マイグレーション用のシェルが実行される
3. デプロイ専用ブランチにマージ
4. CloudBuildで、k8sのマニフェストが適用される

また先日、DBの接続情報などをSealedSecretを用いてSecretで管理するようになりました。
そこでマイグレーションの実行もk8sのJobで実行したくなったのですが、マイグレーションの完了を待ってからデプロイするにはどうすればいいかと疑問に思い、調査した結果、ArgoCDでうまいことやれそうだということが分かったので、検証することにしました。
しかもArgoCDでGitOpsを実現しようとしていたので、ちょうどよかったです。

この記事で触れること

  • ArgoCDのHookについて

説明で用いるyamlは以下のリポジトリにコミットしてあります。
istsh/secret-sample-manifests

この記事では触れないこと

  • ArgoCDの導入方法
  • GitOpsとはなにか
  • デプロイ対象のアプリケーションについての諸々

ArgoCDのHook

公式ドキュメント

Hookの定義方法

Hookはアノテーションとして定義します。
ここではJobのyamlを紹介します。

apiVersion: batch/v1
kind: Job
metadata:
  name: sample-job
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
    argocd.argoproj.io/sync-wave: "2"

Hookの種類

ArgoCDのHookは5種類あります。

Hook 説明
PreSync マニフェストの適用前に実行される。
Sync すべてのPreSync Hookが成功として完了した後で、マニフェストの適用と同時に実行される。
Skip マニフェストの適用をスキップする。
PostSync すべてのSync Hookが成功として完了し、すべてのリソースが正常な状態になった後に実行される。
SyncFail PreSyncやSync, PostSyncが失敗したときに実行される。(ver1.2以降)

Hookの削除

Hookを何らかの方法で削除するには、hook-delete-policyをアノテーションに定義します。
Policyは3種類あります。

Policy 説明
HookSucceeded Hookが成功した後に削除される。
HookFailed Hook失敗した後に削除される。
BeforeHookCreation 次の新しいHookが作成される直前に削除される。(ver1.3以降)

同一Hookでの実行順序の指定

sync-waveをアノテーションに定義します。
これを定義すると、その値の昇順にHookが実行されます。
ただし、注意点が2つあります。
1つ目は、値は数値ではなく文字列で定義すること。
2つ目は、公式ドキュメントにもあるように、sync-waveの初期値は0です。同一Hookでsync-waveを設定する場合に、どれかに設定漏れがあると、それが意図せず先に実行されることがあるので注意してください。

マイグレーションのJobを定義

前提として、JobはSecretから環境変数を読み取る、SecretはSealedSecretを使用している、とします。

先にSecret(SealedSecret)とJobのyamlを載せます。
使用しているイメージやその引数は、今回の説明には関係ないので無視してください。

secret.yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: sample-secret
  namespace: default
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
    argocd.argoproj.io/sync-wave: "1"
spec:
  encryptedData:
    MYSQL_HOST: AgBPhntyTvUOYPtjJrdZk0fbVyljZFQExAVKAHYjE0rtTNsqaNXygninGs5CPPALfIf/BgmONK35xepHnH7kqc+WNm2nt4MueAMHAtrHQ/8SCkzXbQGLuiyUUds2Q2xVu/rBAVwGfWaKkwi9Db8Y7u6UOLE2wjgk2hpfqnf0KCgR4+u4q936tszBlqQrzk6+ZCqn7oPkOHI3iHJIi4Myo+Xh51Cx0ItEcq18huAK04tTgPoNDJgjLZZnWt83eOJcdqs6mNuI2khsi+/sXizA/naRRDJjiZat5//v/e73ejPRYLJkLCxmbiqBkZUmzBcTgj9PNvOPd+Be/VlKV0I4oO23e4f4CTp+xsq/hxje1qo38zL5UEalbYvtWVCAQCHyA5zcacpD7Sbl0hDSn4JCmXgktrtmgp4iADw2dGDqj7yGyvgQt18VUyWwqIzdhdih9a9LLvqK+gEf9pHs6p6j+rSlvIhKWM9k+vp44h9GLv5ViXB0jFD6Ig5kuyrne6OPceJgj1kuoGQbGdwCvd6thjNytWwBuPujKo5AK4VepLUUh7q7zZwqBv0yXMwSkmFDpJMTmMuy6W32HZvmJw1m/ajaAqlltDKl2pbyDClRLJ/K1n9QDiOEgIdLX3t9C5tJL1G9b1bMTpTfqqYcQbTQnDBEJJ7du4eYQRoT9EP4vI0eyab8beuvBvwIdPvsPWwY+uf7TzuyX0ve6/8=
    MYSQL_PASSWORD: AgAV4O7rOmyqL2JtPZ/XtUIMNW5GPgSMhc8zU0zU7LyaNfu5PPFfZrqmyvV4u0XjiNL0nH0rxCEnbm8ZlYtDQCDTMAZMgQkDSWVq7RGnL0txkE3mtsgzctNvIk54lvoVi7fzsBUHXLWsXu58NgQsGhc/VG25zMIq2ycEKd0nEFoa3EK1EoNsb3gKNS4qGM7hqkS5y7ufOcUR5mVuvXlihrfn9nGcucFsRsMbxsTYWUauM8x0wqxrnw7o74EkA0Gyhu2FI43n4mleubC8pzOEYvurfyfd1UmS7H6j/Cm4VuPtiX/uynvKyb2iVJX3vkeyKnmvhP6AcegfI1UYSD1nLg6taDOUFDdScCRsdI97lF0ehYauinLLp94rRQIXOFWYaSqq8VNbQzApXTDAgSzjazdx17p1IVrspEc7xCzD1Dcn3RiiiSpGnYEbG4rF4pFu8AcKH5j/MSydKN4iJgQ2utOgJ7pUXNeShgRrNf/BI67uus44fgeIXnkG8+LG7/WGhsjnYTRsECZD4g+q2vB3tYeBSFQ7ojUFVN5ChsZgTS+uziIp3jzxu3sWfSt4puGx+BlFp+dxv0Zp1YrRKuje+dBWlCkrf9Sa16SR7DCqKdX+1G0W6rwYYEyXwRW9Y0s6lUZwfpd0kOVcYZABohfH3RkWmxgKjF76xFlyFfHILVtdGJbe1nz1144wAMQeE3jvk/ypeAGy+I8lbg==
    MYSQL_USER: AgDjq7qBJR1DDOWapYF4AhtQi1sQgeglSRhu6tZ+JHPTPPdNFBV+zQk3xzKKRldZeeIlXQ5ZquEdc3lMWYKqg5xvL2e21YhgPv4oWADDvsWxWdklZVLDl0SGPQT64v6K+lPVR5Gia6zIpocvv4v3GbMcCXFkFK/b5HeSU6FcxrAhrQ+vdxSWKfkezO8vrS1wJ3MYLgLSwtiRDsibjpYwVTg0sLDcpTlrDk95IEnmcY8bE6qGJ3BO8mUBPhsBnk5yLJO2MZeOofA9IbpUDIpUJSFcaaXc+PK3C+vKxxJ2Ia94Y07UshUOrl72dTrp9aJv1mdkVfzZ7EHQpHh+2k7b2U1nMFVFwK2QV193vJVSQDM21IzIttE4QOxllv0ei48FVMVw3pJ0ABJMKiYta6KAZvKQ5EJkEIVVHtVqRH6plpBgU72dAPVuJAmdlAQ3I844lj1AKwfydylFRJP/olkHegB5Lu3ScwblicPZt4Q+f+IV79YefKR+Bf8zwLB5bd5uFOVx5Jj3dkiSfTN9P60FE6uA1axxyECafqwcgEmxafODtRpP+94rbAUboWCCXTAqjL+KyclCkXg0lKKLDHBmIzhsV3+eDwlApHWfvs1/x2dyW17Rjs7Nqb6vi/xX+YXT0d67TzNEUFfYFABRN9ABN/YSCWsVxIJXvMulg4RpsGD4g5E4GG5gwj3Jjjfr0yEtsu9fS5Zn
  template:
    metadata:
      creationTimestamp: null
      name: sample-secret
      namespace: default


job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: sample-job
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
    argocd.argoproj.io/sync-wave: "2"
spec:
  backoffLimit: 0
  template:
    spec:
      containers:
        - name: sample-job
          image: istsh/secret-sample-app-job:1.0.0
          args: ["success"]
          envFrom:
            - secretRef:
                name: sample-secret
      restartPolicy: Never

ここで重要なのは、Jobだけでなく、Secret(SealedSecret)にもPreSyncを定義していることです。
マイグレーションを実行する場合、SecretからDBの接続情報を取得することになると思いますが、JobだけにPreSyncを定義すると、Jobの実行時にSecretが作成/更新されていないため、実行に失敗します。
SecretにもPreSyncを定義することで、同一HookでSecretも作成されます。

しかし、Secretが作成されるよりも前にJobが実行されてしまうと、DBの接続情報が取得できずに失敗してしまいます。
ここで使えるのがsync-waveです。Secretのsync-waveを"1"、Jobを"2"とすることで、Secretが必ず先に作成されます。

また、hook-delete-policyを定義しないと、次回の実行時にHookが残ったままなので、PreSyncそのものが失敗してしまいます。必ず定義しましょう。
HookSucceededは失敗した場合に削除されずに残ってしまいます。HookFailedも同様なので、BeforeHookCreationが有用なのではないでしょうか。

Hookの状態を確認

Hookの状態は以下のコマンドで確認できます。

$ argocd app get sample-app --show-operation
Name:               sample-app
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          default
URL:                https://localhost/applications/sample-app
Repo:               https://github.com/istsh/secret-sample-manifests.git
Target:             candidate-local
Path:               k8s/overlays/local
SyncWindow:         Sync Allowed
Sync Policy:        Automated
Sync Status:        Synced to candidate-local (2d128f4)
Health Status:      Healthy

Operation:          Sync
Sync Revision:      2d128f4f1a2ee8a773a0505d827c34e28ac13593
Phase:              Succeeded
Start:              2020-09-28 01:24:56 +0900 JST
Finished:           2020-09-28 01:25:09 +0900 JST
Duration:           13s
Message:            successfully synced (all tasks run)

GROUP        KIND          NAMESPACE  NAME                               STATUS     HEALTH   HOOK     MESSAGE
bitnami.com  SealedSecret  default    sample-secret                      Succeeded           PreSync  sample-secret created
batch        Job           default    sample-job                         Succeeded           PreSync  job.batch/sample-job created
             ConfigMap     default    envoy-secret-sample-server-config  Synced                       configmap/envoy-secret-sample-server-config unchanged
             ConfigMap     default    envoy-secret-sample-proxy-config   Synced                       configmap/envoy-secret-sample-proxy-config unchanged
             Service       default    secret-sample-server-preview       Synced     Healthy           service/secret-sample-server-preview unchanged
             Service       default    secret-sample-proxy-active         Synced     Healthy           service/secret-sample-proxy-active unchanged
             Service       default    secret-sample-server-active        Synced     Healthy           service/secret-sample-server-active unchanged
             Service       default    secret-sample-proxy-preview        Synced     Healthy           service/secret-sample-proxy-preview unchanged
argoproj.io  Rollout       default    secret-sample-server               Synced     Healthy           rollout.argoproj.io/secret-sample-server unchanged
argoproj.io  Rollout       default    secret-sample-proxy                Synced     Healthy           rollout.argoproj.io/secret-sample-proxy unchanged

まとめ

ArgoCDのPreSyncを用いることで、アプリケーションのデプロイ前にマイグレーションを実行することが可能になりました。
マイグレーションに限らず、PreSyncで「デプロイを開始します」、PostSyncで「デプロイが完了しました」、SyncFailで「デプロイが失敗しました」と通知することも可能です。
これ以外におすすめの使い方を知っている方は、コメント等いただけると幸いです。