⚓️

KubernetesのExternal Secrets Operatorアップグレード時のポイント(CRD・Helm履歴)

に公開

こんにちは、Luup にわです。

Luupでは、Supersetをはじめとする複数のBIツールやデータアプリをKubernetes上で運用しています。

これらのツールは、データベース接続用のパスワードや外部APIのキーといった機密情報を必要としますが、アプリから直接Secret Managerにアクセスするには各アプリでSDK実装や認証設定が必要になります。
そこでExternal Secrets Operatorを使うことで、機密情報をKubernetes標準のSecretとして配布でき、特にこうした既製ツールでは設定だけで利用可能になります。

一方で、External Secrets Operator自体も定期的なアップグレードが必要です。その際、「Operatorを上げれば終わり」ではなく、CRD(CustomResourceDefinition)の更新順やHelmの履歴が原因で思わぬ失敗につながる可能性があります。

この記事では「ExternalSecretのapiVersion更新後にデプロイが落ち続けた」ケースを背景に、実務でつまずきやすいポイントと切り分け観点、アップグレード手順をまとめます。
本記事では、デプロイ時に no matches for kind "ExternalSecret" のようなエラーが出るケースを扱います。同じようなエラーで手が止まったときに、切り分けの足がかりになれば幸いです。

この記事で伝えたいこと

  • apiVersion変更が絡むアップグレードで、どこで失敗しやすいかを把握する
  • no matches for kind "ExternalSecret" のエラーを、CRD側とHelm履歴側に切り分けて考えられるようにする
  • CRD → Operator → 依存チャートの順序を固定し、再デプロイまで通す
  • 失敗時の切り分けを素早く回す(確認コマンドを用意する)

経緯

あるタイミングで、BIツールやデータアプリ側のチャートでExternalSecretapiVersionexternal-secrets.io/v1beta1からexternal-secrets.io/v1に更新しました。

その後、GitHub Actions経由のデプロイが失敗し続け、エラーは概ね以下のようなものでした。

エラー例(要旨)
Error: unable to build kubernetes objects from release manifest:
no matches for kind "ExternalSecret" in version "external-secrets.io/v1"

最初は、テンプレート更新の漏れを疑いました。
次に、クラスターにCRDが入っていない可能性も考えました。
結論としては次の2点が絡んでいました。

  • Operator/CRDの世代が古く、v1の型がクラスター側で期待どおりになっていない
  • Helmのリリース履歴(Secret)に古いv1beta1が残っており、アップグレード時につまずきやすい

この2つは、アプリケーション側のテンプレート更新と、クラスター側(CRD/Operator)の更新が別のタイミングで進むと起きやすい印象です。

概要

External Secrets Operatorがやっていること

External Secrets Operatorは「外部のSecret Store(例:GCP Secret Manager)とKubernetes Secretを同期する仕組み」です。

典型的なデータフローは以下です。

GCP Secret Manager
  ↓ (External Secrets Operatorが取得)
ExternalSecret (宣言)
  ↓ (External Secrets Operatorが reconcile)
Kubernetes Secret (Opaqueなど)
  ↓ (Podが参照/マウント)
データアプリケーション

BIツールやデータアプリの運用では、以下の性質が嬉しいです。

  • Gitには「参照情報(どのSecretを同期するか)」だけ置ける
  • 値の更新はSecret Managerで完結し、一定間隔で自動反映できる
  • 環境(dev/stg/prod)でSecret Storeを分けやすい

アップグレードで壊れやすい3箇所

External Secrets Operatorのアップグレードで壊れやすいのは、だいたい次の3つです。

  1. CRDの更新
  2. Operator本体(controller/webhookなど)の更新
  3. 依存チャート(ExternalSecret/SecretStore/ClusterSecretStoreを含む側)の再デプロイ

特にapiVersionの変更(v1beta1v1など)が入ると、1→2→3の順序が崩れたタイミングで失敗が連鎖しやすくなります。

実際にやったこと(アップグレード手順)

ここからは、今回実際に行った手順です(デプロイはHelm/helmfile想定)。コマンド中のバージョンやnamespaceなどは、環境に合わせて読み替えてください。

1. 現状把握(Operator/CRD/利用状況)

まずは、クラスターに何が入っていて、どこがExternal Secrets Operatorに依存しているかを確認しました。

# CRDの存在確認
kubectl get crd | grep external-secrets

# ExternalSecretなどのリソースが見えているか(見えないならCRDが怪しい)
kubectl get externalsecrets --all-namespaces
kubectl get secretstore --all-namespaces

# CRDが提供しているversion一覧(v1が無ければ当然v1は使えない)
kubectl get crd externalsecrets.external-secrets.io -o jsonpath='{.spec.versions[*].name}'

# External Secrets OperatorのPod/Deploymentの状態(namespaceは環境に合わせる)
kubectl get deploy -n external-secrets
kubectl get pods -n external-secrets

デプロイ時にno matchesのエラーが出た場合、まずCRDが新しいapiVersionをサポートしているかを確認しました(上記のコマンドでv1が表示されるか)。

今回のケースでは、CRDを更新した後も、アプリ側NamespaceのHelm履歴(Secret)で競合エラーが発生しました。
このエラーの影響で利用に支障の出る状態が続いていたため、詳しい原因を追いきるよりも、まず復旧を優先する判断をしました。
Helmのリリース履歴に古いv1beta1の情報が残っていたことが影響していた可能性が高く、対処としてはHelm履歴を削除して再デプロイすることで解決しました。

2. 先にCRDを更新する(Operatorより前)

External Secrets OperatorはCRDに依存します。Operatorを先に上げてもCRDが古いと、Webhook/Controllerが期待する型とズレてトラブルになりやすいです。

実務上は、CRDを先に“確実に”適用するのが安全だと判断し、先にCRDを適用しました。

External Secrets OperatorのCRDはリポジトリーでbundleとして提供されていることが多いので、バージョンを固定したURLを使いました(以下は例としてvX.Y.Z表記にしています)。

# 例: バージョン固定のCRD bundleを適用
# applyだとannotationサイズなどで失敗することがあるため、replaceを使う方が安定するケースがあります。
kubectl replace -f https://raw.githubusercontent.com/external-secrets/external-secrets/vX.Y.Z/deploy/crds/bundle.yaml

kubectl applyが通らない場合は、Server-Side Apply(SSA)で回避できるケースもあります。
なお、最近のKubernetesで推奨されることが増えています。

# 例: Server-Side Apply(環境によっては衝突解決が必要)
kubectl apply --server-side --force-conflicts -f https://raw.githubusercontent.com/external-secrets/external-secrets/vX.Y.Z/deploy/crds/bundle.yaml

注意しておきたいのは、replaceは「既存を置き換える」ため、CRDが存在しない環境だと失敗する点です。

  • 既にCRDがある: replaceで更新
  • まだCRDがない: applyで作成(初回だけ)

この分岐が面倒な場合でも、まずは手順として定着させ、Runbook化して抜けを減らすのが安全です。

後から調べた補足として、以下のような情報もありました。

OperatorをHelmで導入している場合、チャートによっては installCRDs=true(もしくは同等の仕組み)でCRDも一緒に管理していることがあるようです。
この場合は、CRDを別コマンドで当て直す前に「CRDをどこが管理しているか」を確認した方がよさそうです。

また、External Secrets Operator v0.16.1 以降では、CRD migration 時に status.storedVersions の更新を推奨しているケースがあるようです。
必要に応じて、公式リリースノートも参照してみてください。
https://github.com/external-secrets/external-secrets/releases/tag/v0.16.1

storedVersions 更新のコマンド例
kubectl patch --subresource=status crd externalsecrets.external-secrets.io \
  --type=json \
  -p='[{"op": "replace", "path": "/status/storedVersions", "value": ["v1", "v1beta1"]}]'

3. Operator(Helm release)をアップグレード

CRDを更新したら、Operator自体を上げました。

# helmfileの場合は環境に合わせてapply
helmfile -e <environment> apply

ここで見るべきポイントは、Podが起動していることだけではありません。

  • controller / webhookがReadyになっているか
  • エラーが継続していないか(特にRBAC/Admission/Webhook周り)
kubectl logs -n external-secrets deploy/external-secrets -f

4. 依存チャートの再デプロイ(ここでHelm履歴につまずきやすい)

今回いちばん学びが大きかったのはここです。

テンプレートが既にv1に更新されていても、Helmが**過去のリリース履歴(Secret)**を参照して失敗することがあります。

対処としては、対象リリースのHelm履歴(Secret)を削除し、再デプロイしました。

なお、HelmのストレージドライバーがSecretの場合の話です(Helm v3のデフォルト)。

# 例: ロールバック用の履歴(status=superseded)だけ削除して再デプロイ
kubectl get secret -n <namespace> -l owner=helm,name=<release-name>,status=superseded
kubectl delete secret -n <namespace> -l owner=helm,name=<release-name>,status=superseded

helmfile -e <environment> apply

この操作は「ロールバック履歴が消える」デメリットがあります。
必要なら、削除前に対象Secretをバックアップしておくと復旧の選択肢が増えます。

# 例: 削除前にバックアップ(ファイル名は任意)
kubectl get secret -n <namespace> -l owner=helm,name=<release-name>,status=superseded -o yaml > helm-history-backup.yaml

また、状況によってはHelmの挙動が「新規インストール扱い」に寄る可能性もあります。
一方で、アプリケーション用のSecretや、ExternalSecretリソース自体を消すわけではありません(ラベルで正しく絞れている前提)。

削除対象のラベルが想定どおりか(type: helm.sh/release.v1 など)は、実行前に必ず確認してください。できれば、検証環境で先に試すのがおすすめです。

後から調べたところ、Helmのリポジトリーで提供されている helm-mapkubeapis プラグインを使うと、履歴内のapiVersionを書き換えることもできるようです。
今回は復旧速度を優先して履歴削除の方法を選びましたが、より安全に対処したい場合はこちらの方法も検討してみてください。

5. 同期の成否を確認("Secretがある"だけでは不十分)

最後に、ExternalSecretが正常にreconcileできているかを確認しました。

kubectl get externalsecrets -n <namespace>
kubectl describe externalsecret -n <namespace> <name>

# 作られるはずのKubernetes Secretが作成/更新されているか
kubectl get secret -n <namespace> <target-secret-name>

BIツールやデータアプリ運用だと、ここで確認したいのは以下です。

  • Secretのキー名がアプリの期待どおりか(DATABASE_URLなど)
  • refresh間隔やローテーションの要件を満たせるか
  • 失敗時に“気付ける”状態か(イベント/メトリクス/アラート)

今回のハマりどころ

今回の事象は、見え方の似た原因は2つ(CRD側 / Helm履歴側)あり、切り分けに時間を要しやすいことがハマりどころでした。

CRD更新の順序を崩さない

エラー例:

no matches for kind "ExternalSecret" in version "external-secrets.io/v1"

原因:

  • apiVersionv1に上げたが、クラスター側のCRDが追随していない
  • CRDが古いため、v1の型が認識されない

今回やったこと:

  • CRD → Operator → 依存チャートの順番で更新と再デプロイを進めました
  • CRD/Operatorを更新してから、v1に更新済みの依存チャートを再デプロイしました
`kubectl apply`で失敗することがある

今回の事象とは直接関係しませんが、CRD適用で詰まりやすい点としてメモしておきます。

エラー例:

Error: metadata.annotations: Too long: must have at most 262144 bytes

原因:

  • CRDのサイズが大きく、kubectl applyがannotationのサイズ制限で失敗する

対処法:

# 既存CRDがある場合は kubectl replace を使用
kubectl replace -f https://raw.githubusercontent.com/external-secrets/external-secrets/vX.Y.Z/deploy/crds/bundle.yaml

# または Server-Side Apply を使用
kubectl apply --server-side -f https://raw.githubusercontent.com/external-secrets/external-secrets/vX.Y.Z/deploy/crds/bundle.yaml

Helm履歴(Secret)が原因で、テンプレート更新済みでも失敗する

今回の本題です。

エラー例:

Error: unable to build kubernetes objects from release manifest:
no matches for kind "ExternalSecret" in version "external-secrets.io/v1"

(CRDは既にv1をサポートしているにもかかわらずエラーになる)

原因:

  • 「いまのテンプレート」ではなく「過去の履歴」まで見に行く
  • Helmのリリース履歴(Secret)に古いv1beta1が残っており、新旧のapiVersionが混在
  • その結果、no matches for kind ...のようなエラーに見える

今回やったこと:

# Helm履歴(status=superseded)を削除して再デプロイ
kubectl get secret -n <namespace> -l owner=helm,name=<release-name>,status=superseded
kubectl delete secret -n <namespace> -l owner=helm,name=<release-name>,status=superseded

helmfile -e <environment> apply

より安全な方法:

後から調べたところ、helm-mapkubeapis プラグインを使うと、履歴内のapiVersionを書き換えることもできるようです(履歴を残したい場合)。

この切り分けができると、Operator/CRDではなく対象リリースの履歴の問題として切り分けられます。

依存関係の境界(今回の学び)

症状:

  • アプリ側のマニフェストをv1に更新したが、クラスター側のCRDが古いままでデプロイが失敗し続ける

原因:

  • アプリ側のapiVersion変更と、クラスター側(CRD/Operator)の更新タイミングがずれている

次からの方針:

  • できるだけ同じリリースで束ねる(アプリとインフラの更新を同時に実施)
  • CRD/Operatorを先に更新してから、アプリ側のマニフェストを変更する

結果

  • External Secrets Operator側(CRD/Operator)を更新し、依存チャートを再デプロイできる状態になりました
  • apiVersion更新後に落ち続けていたデプロイが通り、Secret同期も正常に戻りました

最後に

External Secrets Operatorのアップグレードで気を付けることは、Operator単体ではなく「CRD」「Helm履歴」「依存チャート再デプロイ」をセットで扱うことでした。

データエンジニアリングの観点でも、Secrets運用はアプリの信頼性・変更の追跡(誰がいつ更新したか)・開発/運用のスピードに影響します。
できる範囲から、仕組みとして安全に回せる形に近づけていければと思っています。
この記事が、同じようなエラーで困ったときの助けになればうれしいです。

Luupでは、データエンジニアリングの観点でプロダクトを前に進める仲間を探しています。興味がある方は、ぜひご応募ください。

https://recruit.luup.sc/

参考

Luup Developers Blog

Discussion