Cert Manager アップグレード (v1.5 to v1.8)

以前、以下の記事で古い Cert Manager を v1.5 まで一気に(段階的に)アップグレードしました。

[Kubernetes] cert-manager のアップグレード方法 (v0.12 to v1.5.5)
[Kubernetes] cert-manager のアップグレード方法 (v0.12 to v1.5.5)
https://fand.jp/upgrade-cert-manager-form-0-12/
kubernetes の証明書管理機能 cert-manager のアップグレード手順の整理

本記事ではその後のアップグレード(v1.5 to v1.8)と、途中で起きたトラブルを記載します。

Cert Manager アップグレード作業の基本動作

基本的な作業パターンは、前記事でも記載しているように以下の手順です。

Backup and Restore Resources - cert-manager Documentation

# 基本的なアップグレード作業
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.8.0/cert-manager.yaml

# v1.8.0 の部分を適当なバージョンにする

v1.7へのアップグレード時に発生したエラー

$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.7.2/cert-manager.yaml
namespace/cert-manager unchanged
serviceaccount/cert-manager-cainjector configured
serviceaccount/cert-manager configured
serviceaccount/cert-manager-webhook configured
configmap/cert-manager-webhook created
clusterrole.rbac.authorization.k8s.io/cert-manager-cainjector configured
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-issuers configured
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-clusterissuers configured
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-certificates configured
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-orders configured
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-challenges configured
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-ingress-shim configured
clusterrole.rbac.authorization.k8s.io/cert-manager-view configured
clusterrole.rbac.authorization.k8s.io/cert-manager-edit configured
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-approve:cert-manager-io configured
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-certificatesigningrequests configured
clusterrole.rbac.authorization.k8s.io/cert-manager-webhook:subjectaccessreviews configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-cainjector configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-issuers configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-clusterissuers configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-certificates configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-orders configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-challenges configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-ingress-shim configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-approve:cert-manager-io configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-certificatesigningrequests configured
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-webhook:subjectaccessreviews configured
role.rbac.authorization.k8s.io/cert-manager-cainjector:leaderelection configured
role.rbac.authorization.k8s.io/cert-manager:leaderelection configured
role.rbac.authorization.k8s.io/cert-manager-webhook:dynamic-serving configured
rolebinding.rbac.authorization.k8s.io/cert-manager-cainjector:leaderelection configured
rolebinding.rbac.authorization.k8s.io/cert-manager:leaderelection configured
rolebinding.rbac.authorization.k8s.io/cert-manager-webhook:dynamic-serving configured
service/cert-manager configured
service/cert-manager-webhook configured
deployment.apps/cert-manager-cainjector configured
deployment.apps/cert-manager configured
deployment.apps/cert-manager-webhook configured
mutatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook configured
validatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook configured

# この直後、アップグレード作業時に初めてエラーが発生
Error from server (Invalid): error when applying patch:

# ....manifest の中身が一気に表示されるが、長いので省略

to:
Resource: "apiextensions.k8s.io/v1, Resource=customresourcedefinitions", GroupVersionKind: "apiextensions.k8s.io/v1, Kind=CustomResourceDefinition"
Name: "certificaterequests.cert-manager.io", Namespace: ""
for: "https://github.com/cert-manager/cert-manager/releases/download/v1.7.2/cert-manager.yaml": CustomResourceDefinition.apiextensions.k8s.io "certificaterequests.cert-manager.io" is invalid: status.storedVersions[0]: Invalid value: "v1alpha2": must appear in spec.versions

このエラーが6回ほど発生します。

6回発生する理由は、具体的にはCRD (Custom Resource Definition) の適用で発生していました。

  • certificaterequests.cert-manager.io
  • certificates.cert-manager.io
  • challenges.acme.cert-manager.io
  • clusterissuers.cert-manager.io
  • issuers.cert-manager.io
  • orders.acme.cert-manager.io

分解して Apply してみると、シンプルに以下のメッセージが6回出ているだけです。

The CustomResourceDefinition "orders.acme.cert-manager.io" is invalid: status.storedVersions[0]: Invalid value: "v1alpha2": must appear in spec.versions

# これがCRDの数だけ出る。

なおこの時点の状態は、大部分のリソースが v1.7 用になっているが、CRDを含む若干数が v1.6系のままであるという複合状態です。

エラーの原因

結局のところ、この問題にあたってしまった直接の原因は特定しきれていませんが、心当たりがあるのは v1.7 での API の廃止(Breaking Changes)です。以下の記事に記載されている通り、 v1alpha1|2v1beta のAPI廃止に伴って事前に Cert Manager のリソースをアップグレードしておくように注意されていますが、今回のわたしの環境では、一部リソースでは v1alpha2 が残ったまま Cert Manager のアップグレードを行ってしまいました。

本記事では最終的に解決方法まで示しますが、Kubernetes の仕様理解が不十分な点もありますので、いち解決事例の参考程度に参照してください。一応、調査の過程で勉強担った部分も多数あります。

解決方法

解決の糸口はここにあります。

status.storedVersions[0]: Invalid value: “v1alpha2”: must appear in spec.versions

v1alpha2 が邪魔だと理解。

確かに、適用されている CRD を見てみると値があります。

kubectl get crd orders.acme.cert-manager.io --show-managed-fields --output json の実行結果)

修正するために、Kubernetes クラスタに対して、管理用のREST API 通信を直接試みます。

Accessing Clusters | Kubernetes

注意

だめだったからすぐにパッチ作業をしたのではなく、既存リソースの v1 化など事前にやれることは一通り実施したうえで、やはり status.Versions の状態が変わらなかったことから、パッチ充て作業をすることにしました。作業漏れがないか、Release Notes の確認など再確認をしましょう。

kubectl proxy --port=8080

# これでレスポンスが返ってくれば接続確認はOK
curl http://localhost:8080/api/

そして、以下のパターンで storedVersions から v1alpha2 を消してしまいます(正確には、 v1 のみで置き換える)

curl -d '[{ "op": "replace", "path":"/status/storedVersions", "value": ["v1"] }]' \
  -H "Content-Type: application/json-patch+json" \
  -X PATCH \
 http://localhost:8080/apis/.......

実行した REST パッチ

# まずバックアップ
kubectl get crd "certificaterequests.cert-manager.io" --output yaml > certificaterequests.yaml
kubectl get crd "certificates.cert-manager.io" --output yaml > certificates.yaml
kubectl get crd "challenges.acme.cert-manager.io" --output yaml > challenges.yaml
kubectl get crd "clusterissuers.cert-manager.io" --output yaml > clusterissuers.yaml
kubectl get crd "issuers.cert-manager.io" --output yaml > issuers.yaml
kubectl get crd "orders.acme.cert-manager.io" --output yaml > orders.yaml
# proxy は別のターミナルで実行
kubectl proxy

# 1個ずつ、合計6回のCRDに対してパッチを適用
curl --header "Content-Type: application/json-patch+json" \
  --request PATCH http://localhost:8001/apis/apiextensions.k8s.io/v1/customresourcedefinitions/certificaterequests.cert-manager.io/status \
  --data '[{"op": "replace", "path": "/status/storedVersions", "value":["v1"]}]'

curl --header "Content-Type: application/json-patch+json" \
  --request PATCH http://localhost:8001/apis/apiextensions.k8s.io/v1/customresourcedefinitions/certificates.cert-manager.io/status \
  --data '[{"op": "replace", "path": "/status/storedVersions", "value":["v1"]}]'

curl --header "Content-Type: application/json-patch+json" \
  --request PATCH http://localhost:8001/apis/apiextensions.k8s.io/v1/customresourcedefinitions/challenges.acme.cert-manager.io/status \
  --data '[{"op": "replace", "path": "/status/storedVersions", "value":["v1"]}]'

curl --header "Content-Type: application/json-patch+json" \
  --request PATCH http://localhost:8001/apis/apiextensions.k8s.io/v1/customresourcedefinitions/clusterissuers.cert-manager.io/status \
  --data '[{"op": "replace", "path": "/status/storedVersions", "value":["v1"]}]'

curl --header "Content-Type: application/json-patch+json" \
  --request PATCH http://localhost:8001/apis/apiextensions.k8s.io/v1/customresourcedefinitions/issuers.cert-manager.io/status \
  --data '[{"op": "replace", "path": "/status/storedVersions", "value":["v1"]}]'

curl --header "Content-Type: application/json-patch+json" \
  --request PATCH http://localhost:8001/apis/apiextensions.k8s.io/v1/customresourcedefinitions/orders.acme.cert-manager.io/status \
  --data '[{"op": "replace", "path": "/status/storedVersions", "value":["v1"]}]'

これを実行後はそれぞれ差分が以下のようになっています。

spec:
   conversion:
@@ -939,5 +939,4 @@
     status: "True"
     type: Established
   storedVersions:
-  - v1alpha2
   - v1

以降、問題なく最新の v1.8 までアップグレードできました。

まとめ

今回のトラブル対応と通じて以下の点を学びました。

  • Custom Resource Definition の中身や挙動(雰囲気)の理解
    • どのように複数バージョンのAPIを定義しているのか
    • バージョン定義とは別に、ステータスという存在もある
  • Server Side Apply という挙動について
  • kubectl proxy および REST API によるパッチ作業
    • 完全削除をして新規生成してしまえば同じなのではないかと思いましたが、Kubernetes マスターとしてはマスター側でしか持っていないメタ情報もあるので、差分で適用していくのが重要だということの理解

この記事は前提的に言葉を端折ってしまっていたり、表面的な雰囲気の理解でしかない部分など粗さも多く雑ですが、メモ程度の位置づけとしてご容赦ください。

[参考情報] status.storedVersions とは

どのような周期で定義されるのかは理解できていませんが、以下の記載が真であるならなんとなく理解。

All served versions in the history of the CRD end up in the Status. Any update to the CRD Status is impossible. Any update to the CRD versions that would consist in removing a version that exists in the status is impossible.

関連記事

Cannot remove v1alpha1 from CRD storedVersions with kubectl · Issue #2196 · elastic/cloud-on-k8s

履歴にあれば載るし、そして載ってしまったら消えないので、手動で削除するしかない?(ただ、ここでの議論ではプログラムを用いて修正したというやりとりでもあるので、完全に一致なのかは不明)

とりあえず私のケースではプログラムを使うことなく、 REST API でのパッチ作業でかいけつしました。

[参考情報] Server Side Apply

Cert Manager 1.7 のリリースノートには、以下のパッチ作業の言及があります。

Release 1.7 - cert-manager Documentation

crds=("certificaterequests.cert-manager.io" "certificates.cert-manager.io" "challenges.acme.cert-manager.io" "clusterissuers.cert-manager.io" "issuers.cert-manager.io" "orders.acme.cert-manager.io")

for crd in "${crds[@]}"; do
  manager_index="$(kubectl get crd "${crd}" --show-managed-fields --output json | jq -r '.metadata.managedFields | map(.manager == "cainjector") | index(true)')"
  kubectl patch crd "${crd}" --type=json -p="[{\"op\": \"remove\", \"path\": \"/metadata/managedFields/${manager_index}\"}]"
done

結局今回の問題解決には全く関係がなかったのですが、patch を適用するという意味ではかなり理解を深める参考になりました。

Server Side Apply (SSA) とは何か

Kubernetes のマニフェストを宣言的に適用するにあたって、共同作業時などにデグレードを起こさないように内容の差分管理をサーバー側で実施することを目的とした仕組みです。

記事がわかりやすかったです。

Kubernetes 1.14: Server-side Apply (alpha) - Qiita

SSA の優位性が理解できましたが、個人作業をしているときは普通に Client Side Apply でやったほうがシンプルとも言えそうです。

managedFields とは何か

上記パッチ作業を適用するために --show-managed-fields というオプションがありますが、これを実行してもJSONデータには .metadata.managedFields が存在しません。おかしいぞ?と思ったところで調査していたところ、 Server Side Apply をした時に登場する管理データであることを理解しました。