Kubernetes v1.22 では結構多くのAPIが廃止1され、慎重なアップグレードが求められる。 その補助として、最近出たGKEの新機能 Deprecation Insights を使うと便利だ。 v1.21 等のGKE clusterで廃止予定のAPIが使われていないかチェックしてくれる。
あるGKEクラスターで確認してみると、Argo CD controller から2種類の廃止予定APIが呼ばれていることが判明した。
/apis/extensions/v1beta1/ingresses
/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions
古い Ingress API の呼び出し
これに関してはArgo CDのissueに詳しい解説があった。
ArgoCD使っているとGKEのdeprecated insightsが消えない……と色々調べてたら @toversus26 さんの解説が出てきてとても助かりました https://t.co/AqyrMnP0Vo
— castaneai (@castanea) 2022年8月4日
- GKE では延命措置(?)として v1.22 でも廃止されるはずの Ingress API が残されている
- よって Argo CD が管理対象の GKE cluster に対して API resourceの一覧を取得すると古い Ingress API もチェックしてしまう
つまり GKE 側が古い API resource を返さないようにすれば Argo CD も状態を取得しないはずで、Argo CDで管理しているアプリ内に古いタイプの Ingress がなければそのまま v1.22 にアップグレードして問題ない2。(ただし、廃止予定APIの警告機能のせいで自動アップグレードは止められるので、手動アップグレードが必要になる3。)
古い CRD API の呼び出し
2つ目の /apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions
が厄介だった。
これはArgo CDのissuesを探しても見つからず、Argo CDのどの機能が呼び出しているのか最初わからなかった。
Deprecation Insights はAPI呼び出し元の User-Agent は教えてくれるが、一体 Argo CDのどの箇所から呼ばれたのかは流石にわからない。それならいっそArgo CDにパッチを当てて、k8s APIの呼び出しをすべてフックしてStack Traceを出力してしまえば呼び出し箇所がわかるのでは!?と考え、
Argo CD内の共通のclient-go REST API Client設定に http.RoundTripper を差し込んで customresource
をURLに含む場合のみStack Trace (Caller)を出すことにした。
使っていた Argo CD は v2.0.5 だったので、Argo CDのソースをcloneし、v2.0.5 のタグをcheckoutしたあとに以下のパッチを適用した。
diff --git a/pkg/apis/application/v1alpha1/types.go b/pkg/apis/application/v1alpha1/types.go index 31fad8a57..e3d3ae583 100644 --- a/pkg/apis/application/v1alpha1/types.go +++ b/pkg/apis/application/v1alpha1/types.go @@ -3,6 +3,7 @@ package v1alpha1 import ( "encoding/json" "fmt" + "k8s.io/klog/v2" math "math" "net" "net/http" @@ -11,6 +12,7 @@ import ( "path/filepath" "reflect" "regexp" + "runtime" "sort" "strconv" "strings" @@ -2850,10 +2852,34 @@ func (proj AppProject) IsDestinationPermitted(dst ApplicationDestination) bool { return false } +type customRT struct { + base http.RoundTripper +} + +func (c *customRT) RoundTrip(req *http.Request) (*http.Response, error) { + if strings.Contains(req.URL.Path, "customresource") { + var callers []string + for i := 0; i < 50; i++ { + _, file, line, ok := runtime.Caller(i) + if ok { + callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + } else { + break + } + } + j, _ := json.Marshal(callers) + klog.Warningf("============== %s %s caller: %s", req.Method, req.URL, string(j)) + } + return c.base.RoundTrip(req) +} + // SetK8SConfigDefaults sets Kubernetes REST config default settings func SetK8SConfigDefaults(config *rest.Config) error { config.QPS = common.K8sClientConfigQPS config.Burst = common.K8sClientConfigBurst + config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + return &customRT{base: rt} + } tlsConfig, err := rest.TLSConfigFor(config) if err != nil { return err
改造したArgo CDをKubernetes内で動作させるには改造済Argo CDのビルドとコンテナイメージが必要となる。
公式リポジトリ内に Dockerfile も用意されていたのだが、手元のMacではすんなり動かず、今回は argocd-application-controller
のバイナリさえ差し替えればよかったので無理やり自前で Dockerfile を用意して、次のように kubectl patch
でイメージだけを別物に差し替えるというHACKなやり方をした。
FROM golang:1.17-alpine as builder WORKDIR /go/src/argo-cd COPY ./argo-cd/go.* ./ RUN --mount=type=cache,target=/go/pkg/mod set -eux && go mod download COPY ./argo-cd . RUN --mount=type=cache,target=/root/.cache/go-build set -eux && CGO_ENABLED=0 go build -o /bin/argocd-application-controller ./cmd/main.go FROM ubuntu:20.04 ENV USER=argocd USER 999 WORKDIR /go/src/argo-cd COPY ./argo-cd/assets ./assets COPY --from=builder /bin/argocd-application-controller /usr/local/bin/argocd-application-controller
IMAGE="argocd-patched:$(date +%s)" docker build -t "${IMAGE}" -f ./Dockerfile . minikube image load "${IMAGE}" kubectl -n argocd patch deploy argocd-application-controller -p "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"application-controller\",\"image\":\"${IMAGE}\"}]}}}}"
このやり方は非常に無理やり感があるが、過去にAgonesやExternal SecretsのControllerをデバッグした際にも役立っており、意外といろいろな場所で使えるのでは…と思っている。
便利技
— castaneai (@castanea) 2021年6月8日
kubectl patch deployment foo-controller -p "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"foo-controller\",\"image\":\"$(ko publish --local ./cmd/foo-controller)\"}]}}}}"
改造版Argo CDを適用後、CRD を管理下に含むアプリケーション をSyncしてみると次のログが得られた。
argocd-application-controller-f8bbf44db-c6gsr application-controller W0804 17:33:46.409455 1 types.go:2871] ============== GET https://10.96.0.1:443/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/sealedsecrets.bitnami.com caller: ["/go/src/argo-cd/pkg/apis/application/v1alpha1/types.go:2863","/go/pkg/mod/k8s.io/client-go@v0.20.4/transport/round_trippers.go:287","/go/src/argo-cd/pkg/apis/application/v1alpha1/types.go:2873","/go/pkg/mod/github.com/argoproj/pkg@v0.9.1/kubeclientmetrics/metric.go:127","/go/pkg/mod/k8s.io/client-go@v0.20.4/transport/round_trippers.go:298","/go/pkg/mod/k8s.io/client-go@v0.20.4/transport/round_trippers.go:160","/usr/local/go/src/net/http/client.go:252","/usr/local/go/src/net/http/client.go:176","/usr/local/go/src/net/http/client.go:725","/usr/local/go/src/net/http/client.go:593","/go/pkg/mod/k8s.io/client-go@v0.20.4/rest/request.go:891","/go/pkg/mod/k8s.io/client-go@v0.20.4/rest/request.go:964","/go/pkg/mod/k8s.io/apiextensions-apiserver@v0.20.4/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/customresourcedefinition.go:72","/go/src/gitops-engine/pkg/sync/sync_context.go:815","/go/pkg/mod/k8s.io/apimachinery@v0.20.4/pkg/util/wait/wait.go:211","/go/pkg/mod/k8s.io/apimachinery@v0.20.4/pkg/util/wait/wait.go:445","/go/pkg/mod/k8s.io/apimachinery@v0.20.4/pkg/util/wait/wait.go:441","/go/src/gitops-engine/pkg/sync/sync_context.go:814","/go/src/gitops-engine/pkg/sync/sync_context.go:869","/go/src/gitops-engine/pkg/sync/sync_context.go:1113","/go/src/gitops-engine/pkg/sync/sync_context.go:1198","/usr/local/go/src/runtime/asm_arm64.s:1133"]
少し見づらいが、 /go/src/gitops-engine/pkg/sync/sync_context.go:815
という箇所がヒットした。
ここの ensureCRDReady という関数で apiextensions/v1beta1/customresourcedefinitions
を呼んでいることがわかる。
crd, err := sc.extensionsclientset.ApiextensionsV1beta1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{})
この ensureCRDReady 関数の歴史をたどってみると、Argo CDが利用している gitops-engine のPull Request にたどり着いた。この修正で古いAPIを呼ぶのをやめ新しい v1 を呼ぶようになったらしい。
その後 Argo CD v2.3 で上記のgitops-engineの修正が取り込まれたようだ。 よって、Argo CDを最新化すると直りそうだとわかった!
さらに言ってしまうと この修正が入る Argo CD v2.3 以前では ensureCRDReady でエラーが出てもスルーされるようになっている。
なので実はAPIが急に呼べなくなっても障害にはならなさそうだ。該当箇所のコメントを見ると Method is best effort
と書かれているし、PollImmediate
の結果も捨てられている。
// ensureCRDReady waits until specified CRD is ready (established condition is true). Method is best effort - it does not fail even if CRD is not ready without timeout. func (sc *syncContext) ensureCRDReady(name string) { _ = wait.PollImmediate(time.Duration(100)*time.Millisecond, crdReadinessTimeout, func() (bool, error) {
つまり、Argo CDのバージョンを上げないまま Kubernetes v1.22 に上げても実稼働には問題ないとみて良さそうだ。
まとめ
GKEのDeprecation Insightsは便利だが、Argo CDくらいの大規模なソフトウェアになると、なぜ、どこで古いAPIを呼んでいるのかは一見ではわからない。
しかし、Argo CDはOSSなのでやろうと思えば改造して動作確認が可能である。 また、 http.RoundTripper や実行時のスタック情報を持つGoの利点を生かすことで、むりやり感はあるものの呼び出し箇所を特定できた。