目的
自宅 k8s の manifest を管理するリポジトリであるwalnuts1018/infraでは、renovate を用いて、HelmRelease の自動更新を行っています。 FluxCD の HelmRelease リソースには、Chart の Version を指定する項目があるのでそこが更新されていきます。
しかし、リポジトリに Commit されているのはあくまでも HelmRelease の Manifest であり、helm install
をするのは Server-Side の helm-controller です。
そのため、Helm Chart 更新の Pull Request を出されても、結局何が変わるのか一目ではわかりません。
そこで、PR の base と head でhelm install
した後に生成されるリソースの差分を出力し、PR にコメントされるような GitHub Action を作成しました。
完成形
さっそく完成形を置いてしまいます。
https://github.com/walnuts1018/infra/pull/307#issuecomment-2039935834
このように、PR にコメントが投稿され、Helm によって生成されるリソースの差分が表示されています。
方針
- base と head、それぞれで kustomize build を行い、一つの Manifest にまとめる
- その Manifest から、HelmRepository と HelmRelease リソースを抽出する
- HelmRepository リソースを yq でごにょごにょして、
helm repo add
を行う - HelmRelease リソースも yq でごにょごにょして、
helm template
を行う - 一番最初の Manifest ファイルに追記する
- 最後に diff を生成し、API 経由でコメントを投稿する
kustomize build
actions/checkout
を使って、PR の base と head のリポジトリをチェックアウトし、azure/k8s-bake
を使って kustomize build を行います。
- uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.base.ref }} - uses: azure/k8s-bake@v3 with: renderEngine: "kustomize" kustomizationPath: "./k8s/clusters/kmc/" kubectl-version: "latest" id: base_bake - run: "mv ${{ steps.base_bake.outputs.manifestsBundle }} /tmp/manifests-base.yaml" - uses: actions/checkout@v4 - uses: azure/k8s-bake@v3 id: base_changed with: renderEngine: "kustomize" kustomizationPath: "./k8s/clusters/kmc/" kubectl-version: "latest" - run: "mv ${{ steps.base_changed.outputs.manifestsBundle }} /tmp/manifests-head.yaml"
この部分はもともと存在していたのでそのまま使うだけです
Dependency のインストール
yq と jq と helm をいれます
- name: install dependencies run: | # Install jq sudo wget https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64 -O /usr/bin/jq sudo chmod +x /usr/bin/jq # Install yq sudo wget https://github.com/mikefarah/yq/releases/download/v4.43.1/yq_linux_amd64 -O /usr/bin/yq sudo chmod +x /usr/bin/yq # install helm curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | sudo bash ```
HelmRepository と HelmRelease の抽出
/tmp/manifests-base.yaml
と /tmp/manifests-head.yaml
から、HelmRepository と HelmRelease のリソースを抽出します。
- name: HelmReleaseとHelmRepoを抽出 run: | cat /tmp/manifests-head.yaml |yq -j '. | select(.kind == "HelmRelease") | sort_keys(.metadata.name .metadata.namespace)' > /tmp/helmreleases-head.json cat /tmp/manifests-head.yaml |yq '. | select(.kind == "HelmRepository") | sort_keys(.metadata.name .metadata.namespace)' > /tmp/helmrepository-head.yaml cat /tmp/manifests-base.yaml |yq -j '. | select(.kind == "HelmRelease") | sort_keys(.metadata.name .metadata.namespace)' > /tmp/helmreleases-base.json cat /tmp/manifests-base.yaml |yq '. | select(.kind == "HelmRepository") | sort_keys(.metadata.name .metadata.namespace)' > /tmp/helmrepository-base.yaml
helm repo の追加
/tmp/helmrepository-[head|base].yaml
から、helm の repository 情報を取得し、helm repo add
を行います。
- name: helm repo add run: | # helm repo add head-<.metadata.name-.metadata.namespace> <.spec.url> cat /tmp/helmrepository-head.yaml | yq -r '"head-" + .metadata.name + "-" + .metadata.namespace + " " + .spec.url' | while read line; do helm repo add $line done # helm repo add base-<.metadata.name-.metadata.namespace> <.spec.url> cat /tmp/helmrepository-base.yaml | yq -r '"base-" + .metadata.name + "-" + .metadata.namespace + " " + .spec.url' | while read line; do helm repo add $line done ```
[head|base]-<.metadata.name-.metadata.namespace>
という名前で helm repository が追加されました。
yq の中で実行コマンド文字列を生成しているので、Parse 処理が一回で済みます。
helm template
/tmp/helmreleases-[head|base].json
と/tmp/helmrepository-[head|base].yaml
をもとに、helm template
コマンドを使って、リソースを生成します。
helm install --dry-run は、k8s cluster にアクセスできる必要がありますが、helm template は helm 単体で実行できるので、こちらを使います。
- name: helm template run: | length=$(cat /tmp/helmreleases-head.json | jq -s length) for i in $( seq 0 $(($length - 1)) ); do cat /tmp/helmreleases-head.json | jq -rs ".[$i] | .spec.values" | tee values.json >> /dev/null command=$(cat /tmp/helmreleases-head.json | jq -rs "\"helm template \" + .[$i].metadata.name + \" --namespace \" + .[$i].metadata.namespace + \" head-\" + .[$i].spec.chart.spec.sourceRef.name + \"-\" + .[$i].metadata.namespace + \"/\" + .[$i].spec.chart.spec.chart + \" --version \" + .[$i].spec.chart.spec.version + \" -f values.json\"") eval $command | yq "sort_keys(..)" >> /tmp/manifests-head.yaml done length=$(cat /tmp/helmreleases-base.json | jq -s length) for i in $( seq 0 $(($length - 1)) ); do cat /tmp/helmreleases-base.json | jq -rs ".[$i] | .spec.values" | tee values.json >> /dev/null command=$(cat /tmp/helmreleases-base.json | jq -rs "\"helm template \" + .[$i].metadata.name + \" --namespace \" + .[$i].metadata.namespace + \" base-\" + .[$i].spec.chart.spec.sourceRef.name + \"-\" + .[$i].metadata.namespace + \"/\" + .[$i].spec.chart.spec.chart + \" --version \" + .[$i].spec.chart.spec.version + \" -f values.json\"") eval $command | yq "sort_keys(..)" >> /tmp/manifests-base.yaml done
helmreleases-[head|base].json
に、それぞれの HelmRelease のリソースが jsonl として入っているので、jq -s
で配列に変換、length
で長さを取得し、for ループで一つずつ処理していきます。
対象の HelmRelease のspec.values
を取得し、values.json として出しておきます。
次に、helm template コマンドを組み立てていきます。これも jq の中で文字列を生成しているので、一回で済みます。
helm template <HelmReleaseのname> --namespace <HelmReleaseのnamespace> <[head|base]-HelmRelease中で指定されているHelmReporisitoryの名前>-<HelmReleaseのnamespace(=HelmRepositoryのnamespace)>/<HelmRelease中で指定されているChartの名前> --version <HelmRelease中で指定されているChartのバージョン> -f values.json
という文字列が最終的に出来上がります。
そして最後にこれを実行し、出力されたリソースを/tmp/manifests-[head|base].yaml
に追記していきます。デフォルトで1行目に---
が入るので、そのまま追記できます。
ただしここで、sort_keys(..)
を使って、リソースの順番を揃えています。完全ではありませんが、ほとんどの項目が head と base で同じ順番になるので余計な差分が出にくくなります。
diff の生成
ただ diff コマンドを実行するだけです
- name: Build markdown comment with manifest diff run: | echo "## Manifest diff <details> <summary>Click to expand</summary> \`\`\`diff $(diff -u manifests-base.yaml manifests-head.yaml) \`\`\` </details>" | tee /tmp/comment.md ```
PR にコメントを投稿
これが少し難しいですが、「Pull request レビュー コメント用 REST API」を使うのではなく、「issue コメント用の REST API」を使います。 「Pull request レビュー コメント用 REST API」は、PR のコミットに対するコメントが付くので、少し探しにくくなってしまいます。
「issue コメント用の REST API」は、issue だけでなく PR にも使うことができ、これが一番皆さんの使う「PR のコメント」となります。
- name: Comment manifest diff to GitHub PR run: | cat /tmp/comment.md | jq -Rs '{ "body": . }' | curl --fail \ -X POST -H 'Accept: application/vnd.github.v3+json' \ --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.number }}/comments \ -d @-
まとめ
もっと複雑になるかと思ったのですが、意外と単純に行けました。 あと、実行時間ももっとかかるかと思ったのですが、1 分ちょいで終わります。
これで少しは PR の中身をちゃんと確認する気が起きそうです。(今までは脳死で Merge していました) renovate の PR だけではなく自分で更新する際にも意図しない変更がないかどうか確認出来てよさそうです。
とりあえず自宅 infra のリポジトリと、KMC のリポジトリに導入してみました。しばらく様子をみて改善していきたいと思います。
また、今後は「kustomize の resources に include していない」「Chart の values.yaml に存在しない項目を指定している」などについてもテストを用意していきたいな~と考えています。