ArgoCD の Config Management Plugin (CMP)を理解し、Plugin でマニフェストの変数置換を行う

概要

前職の同僚がずっと、PullRequest ごとにプレビュー環境でアプリケーションをデプロイしたいと言っていた。

確かにそれができれば便利ではあるけど、たとえ k8s の力を借りても実現するまでの手順は多く、遠い夢かと思っていた。

でも ArgoCD で頑張ればその夢は近くなるかもしれない。

これは、ArgoCDの Config Management Plugin (CMP) と呼ばれる機能を使って、動的なマニフェスト生成を行い、さらにPullRequestごとの固有の情報をマニフェストに柔軟に埋め込むための仕組みを考えてみたという話。

想定読者

  • k8s にある程度詳しい
  • ArgoCD にもある程度詳しい
  • ArgoCD の ApplicationSet や Generator の機能を知っている、あるいは調べればわかる方

参考資料

GitブランチやPullRequestごとにプレビューを作ることを試している先人の方の知見を抜きにこの内容は語れない。感謝です。

speakerdeck.com

made.livesense.co.jp

speakerdeck.com

qiita.com

何をやりたいか

PullRequestごとにプレビュー環境を作る場合、PullRequestごとに固有の情報を埋め込む必要が出てくる。 特に、ドメインが顕著な例で、 PullRequestの ID が 3 だとする場合、 https://preview-3.example.com みたいな固有のドメインでプレビューアプリケーションを公開したい。

ドメイン以外にも、データベースなどの外部リソースもPullRequestごとに作る場合は、外部リソースのIDも Podの環境変数などに埋め込みたい。 (その外部リソースをどうやって準備するのかは、また別の課題)

実際に参考資料の多くも Ingress などのドメイン名を可変にするような工夫をされている。

Pull Requestをすぐ動作確認! マイクロサービスでのプレビュー環境の作り方 - LIVESENSE ENGINEER BLOG では、 Helm の template を使った方法があり、Kustomizeの拡張機能でIngressの環境管理を実現する - Qiita では、 ArgoCDの plugin と kustomize の replacementRule を使った方法が紹介されている。

どちらの方法も マニフェスト中のドメインの値だけ置換するのなら十分だと思ったが、色々な場所で置換をかけたい場合は扱いが難しい。 前者の方式だと Helm 化が必須だし、後者の方法は置換する値の分だけ環境変数を作り込まないといけない。

理想的な形は、 マニフェスト中に、 ${環境変数} という変数を書いておいて、ArgoCDが置換してくれる形式、 次のようなマニフェストがあるといい感じにしてくれるものだ。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: sample
spec:
  rules:
  - host: preview-${PULL_REQUST_NUMBER}.example.com

要は、kustomize でビルドしたマニフェストに対して、 enbsubst で環境変数による変数置換を行いたい。 このやり方なら Replacement Rule では対応できない置換、ConfigMapの nginx の設定ファイルのホスト名を置き換えるとか、 にも対応できる。

どうやってやるか?

Kustomizeの拡張機能でIngressの環境管理を実現する - Qiita で紹介されている Transformer (KRM function exec plugin) を使うことを考えてみる。

Transformer は kustomize の仕組みで、ビルド後のマニフェストに編集を加える。 KRM function exec plugin は Transformer の仕組みとして任意のコマンドを実行する方法で、 マニフェストを標準入力としてコマンドに渡し、コマンドの標準出力の結果を最終結果として扱う。

ただし、 exec plugin を使うには現時点では kustomize コマンドのオプションに --enable-alpha-plugins --enable-exec を追加する必要があり、通常の ArgoCDから実行される kustomize では実行できない。

そこで、 ArgoCD の Plugin の仕組み、 Config Management Plugin (CMP) を使ってその制約を突破する。

Config Management Plugin (CMP)

ArgoCDは kustomize や Helm などのビルドに対応しているが、Plugin を使うことで任意の処理でマニフェストのビルドを行うことができる。

注意点として、 Plugin を有効化した Application では KustomizeやHelm オプションは使えなくなる。

実際の手順や使い方はここに詳しく書いてある。

argo-cd.readthedocs.io

ArgoCD2.5の時点では CMPの設定方法は二つある

  1. argo-cd ConfigMapに configManagementPlugins を追加する方法
  2. argocd-repo-server pod に plugin を sidecar コンテナとして追加する方法

1の方法は手軽だが実行可能なコマンドが argocd イメージあるもの(helm, kustomizeなど) に限られる。また ArgoCD2.5時点で非推奨になり2.6で消える。

argo-cd.readthedocs.io

よって、これから適用するには 2の方法しかない。

2の方法は マニフェスト生成に関するコマンド実行をsidecar コンテナ内で実行するため、1より安全でかつ必要に応じて任意のコマンドを実行できる。 ただし、sidecarコンテナの中に必要なコマンド類を全て入れておく必要がある。

このことを抑えておかないと、当初は kustomize すら実行できなくて結構困った。1の方式とは名前は同じだがほぼ別物だと思った方が良い。

sidecar の設定

sidecar コンテナは次の条件を満たす必要がある

  1. argocd-repo-server pod の中に sidecar イメージを差し込むこと
  2. /home/argocd/cmp-server/config/plugin.yaml ファイルがあり、init , generate などの項目が書かれていること
  3. 必要なコマンドやバイナリ、スクリプト類をimageに埋め込むこと

2,3については自分のイメージを作ってその中に入れ込んでもいいし、 ConfigMapや initContainer などを使ってVolume mount で実現してもよい。 このサンプルは後者の方法で行う。

まずは、 plugin.yaml の ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: cmp-plugin
data:
  plugin.yaml: |
    apiVersion: argoproj.io/v1alpha1
    kind: ConfigManagementPlugin
    metadata:
      name: kustomize-exec-plugin
    spec:
      version: v1.0
      init:
        command: ["sh", "-euxc", "plugin init for ${ARGOCD_APP_NAME} PR-${ARGOCD_ENV_PR_NUMBER}"]
      generate:
        command: ["kustomize","--enable-alpha-plugins", "--enable-exec", "build"]
      discover:
        find:
          command: ["sh", "-c", "echo $ARGOCD_APP_SOURCE_PATH | grep preview"]

Plugin では https://argo-cd.readthedocs.io/en/stable/user-guide/config-management-plugins/#environment にある通り、ARGOCD_APPで始まる環境変数のほか、 Application リソースで定義した任意の環境変数が、 ARGOCD_ENV_ の prefix が付与されて参照できる。

init は generate の前に実行されるコマンドで checkout した git repo の内容を変更したりするときに使う。サンプルとして単に変数を echo しているだけだが、 Application の kustomize オプションが使用できなくなるので、 kustomize suffix や commonLabels を使いたい場合はここで設定するといいだろう。

generate でマニフェストを出力する。 後述する exec plugin を有効化するためのオプションを指定して kustomize build を行う。

discover が一風変わっていて、 plugin を適用する条件を記述する。 fileName でglobパターンでファイルを指定するか find.command で実行するコマンドの return code が 0 の場合に plugin が実行されるという動作になる。

(正直なところ、こんな判定方法より 従来通り、plugin の名前で指定させて欲しいという気もするが、多分ArgoCDが KustomizeやHelmを判定する方法もこれに近いやり方なんだろう。)

ここでは環境変数 ARGOCD_APP_SOURCE_PATH (kustomize の実行パス) に preview が含まれているか、という条件としている。

次に sidecar イメージの準備。 sidecar で実行するコマンドは、任意のシェルスクリプトや envsubset, kustomize になる。 今回はカスタムイメージを作らずに用意するので、既存のイメージや ConfigMapなどを使って用意する。

まずはシェルスクリプト

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cmp-script
data:
  patch.sh: |
    #!/bin/sh
    cat - | /home/argocd/scripts/envsubst

後で sidecar コンテナに入れ込むもので、標準入力を envsubst に渡している。

今更になって気づいたがこれくらいなら、そのままenvsubst を呼び出せば良かった気がする。。。

kustomize は ArgoCD イメージに含まれているのでそれを使わせてもらう。 envsubst は野良イメージよりはマシだろうということで nginx イメージを使ってみた。

上記をまとめると、次のようなマニフェストを argocd-repo-serverのdeploymentに埋め込むことになる。 kustomize で ArgoCD をビルドするなら patch で下記マニフェストをあてれば良い。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: argocd-repo-server
spec:
  template:
    spec:
      initContainers:
      ## ConfigMapのスクリプト、 nginx image 内部の envsubst を /home/argocd/scripts/ にコピーして、 sidecar イメージで使えるようにする。
      - name: envsubst
        image: nginx:stable # this is relyable image that includes envsubst. 
        command: ["/bin/sh","-c", "cp /usr/bin/envsubst /home/argocd/scripts/envsubst && cp /home/argocd/scripts_temp/*.sh /home/argocd/scripts && chmod +x /home/argocd/scripts/*"]
        volumeMounts:
          - mountPath: /home/argocd/scripts
            name: cmp-script-exe
          - mountPath: /home/argocd/scripts_temp
            name: cmp-script
      containers:
      # この辺りの書き方は https://argo-cd.readthedocs.io/en/stable/user-guide/config-management-plugins/#2-place-the-plugin-configuration-file-in-the-sidecar を参照。
      - name: cmp
        command: ["/var/run/argocd/argocd-cmp-server"] # Entrypoint should be Argo CD lightweight CMP server i.e. argocd-cmp-server
        image: quay.io/argoproj/argocd:v2.5.2 # kustomize を使いたいので argocd イメージを使っている。
        securityContext:
          runAsNonRoot: true
          runAsUser: 999
        volumeMounts:
          - mountPath: /var/run/argocd
            name: var-files
          - mountPath: /home/argocd/cmp-server/plugins
            name: plugins
          # Remove this volumeMount if you've chosen to bake the config file into the sidecar image.
          - mountPath: /home/argocd/cmp-server/config/plugin.yaml
            subPath: plugin.yaml
            name: cmp-plugin
          # Starting with v2.4, do NOT mount the same tmp volume as the repo-server container. The filesystem separation helps 
          # mitigate path traversal attacks.
          - mountPath: /tmp
            name: cmp-tmp
          - mountPath: /home/argocd/scripts
            name: cmp-script-exe
      volumes:
        - configMap:
            name: cmp-plugin
          name: cmp-plugin
        - emptyDir: {}
          name: cmp-tmp
        - emptyDir: {}
          name: cmp-script-exe
        - configMap:
            name: cmp-script
          name: cmp-script

これでインストール周りの設定は完了。うまくいけば argocd-repo-server Pod に cmp コンテナが sidecar として入っているはずだ。

ApplicationSet の作成

PullRequest に対応して プレビュー用の Application を動的に作成するための ApplicationSet を用意する。

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: preview-app
  namespace: argocd
spec:
  generators:
      - pullRequest:
          github:
            owner: <your org>
            repo: <your repo>
            tokenRef:
              secretName: <secret name>
              key: <API token( e.g. PAT)  secret key>
          filters:
          - branchMatch: "f*"
  template:
    metadata:
      name: 'app-sample-pr-{{number}}'
      labels:\
        preview: pr-{{number}}
    spec:
      project: sample-proj
      destination:
        namespace: sample
        server: https://kubernetes.default.svc
      source:
        path: application/sample/overlays/preview
        repoURL: https://github.com/<your org>/<your repo>.git
        targetRevision: '{{branch}}'
        plugin:
          env:
            - name: PR_NUMBER
              value: '{{number}}'
            - name: PR_BRANCH
              value: '{{branch}}'

PullRequest Generator は PullRequest に応じて Application を生成する仕組みで、 number, branch といった変数を使ってテンプレート化できる。

plugin の指定がポイントで、ここで Plugin の適用を設定し、 env で環境変数を設定している。 PullRequestのIDやブランチを環境変数で渡すことができる。

注意点としてここで設定した 環境変数は plugin側では 'ARGOCD_ENV_' prefixがつく、つまり PR_NUMBERARGOCD_ENG_PR_NUMBER になる。

preview用 manifest の設定

ここまで来れば 完成までもう少し。

上記の ApplicationSet に記述した application/sample/overlays/preview の下の kustomizaton.yaml には transformers を設定する。

transformers:
  - plugin.yaml

plugin.yaml の内容は次の通り。

apiVersion: kustomize-krm/v1alpha1
kind: Patch
metadata:
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: /home/argocd/scripts/patch.sh
  name: preview_patch

ここで重要なのは、 アノテーションの config.kubernetes.io/function.exec.path に記載するスクリプトのパスのみで、 apiVersionやkind, name はk8sマニフェストの仕様を満たすためのダミーの値となっている。CRDなどは不要でどんな値でもよい。

pathに記載した コマンドに、 kustomize build で生成したマニフェストが標準入力経由で渡され、 コマンドが標準出力に出した内容が最終結果となる。 今回コマンドで行っているのは enbsubst の実行となる。

あとは、プレビュー向けのmanifestは ${環境変数} で置換したい値を書いておけばよい。

例えば Ingressの場合は、

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test
spec:
  rules:
  - host: preview-${ARGOCD_ENV_PR_NUMBER}.example.com

のように書いておけば PullRequest のIDが host名に差し込まれるし、 nginx config なども、

apiVersion: v1
kind: ConfigMap
metadata:
  name: nignx-conf
data:
  nginx.conf: |
    http {
      server {
          listen       80;
          location /api/ {
            proxy_pass  http://sample-app2-${ARGOCD_ENV_PR_NUMBER}.${ARGOCD_APP_NAMESPACE}.cluster.svc.local:80;
          }

のように書くことができる。

まとめ

sidecar 型 plugin の仕組みを理解するまでは苦労したが、k8s の範囲内では PullRequestに応じた 動的なマニフェストがいい感じにできる予感がしてきた。 今回はenvsubst だが、やろうと思えば go template などの高度なテンプレート機能を差し込むこともできるはずだ。

k8s の範囲外の部分 (例えば、データベースやメッセージキューのようなクラウドリソースもPullRequestごとに作りたい) をどうするかという課題は残るが、結局のところこれらの外部リソースもプレビュー環境の生成・破棄とライフサイクルを合わせるために、 k8sマニフェストとして外部リソースを表現できないかということを考えている。 要は Operator として外部リソースを管理して、CRDでリソースの定義を設定すれば、今回の仕組みを使ってCRDにPullRequestの情報が埋め込める。

パブリッククラウドではすでにそういったOperator が存在しているので、いい線いってそうなアイデアではあるなと感じている。

github.com

cloud.google.com

これでも足りない部分は、 自分でOperatorを作るか、 ArgoCD の Resource Hook を使って外部リソースを作成する Job を作ればいいのではと思っている。

argo-cd.readthedocs.io

というわけで、夢に一歩近づけたと思うことにする。