ArgoCDでリソースを消さずにrequire pruning状態を解消する
背景
ArgoCDでGitOpsしているとします。ここで誤ってkube-system namespaceリソースを一度管理下に入れてpushし、revertしたとしましょう。このようにArgoCDが同期しているリソースが同期元から消えたとき、設定によって以下の2パターンのどちらかの挙動となります。
prune: trueのとき- 同期していたリソースの削除を自動的に試みる。リソースの削除が完了するとSyncedステータスになる。
prune: falseのとき- 同期していたリソースは削除されず、該当リソースの削除待ち状態となりApplicationリソースは OutOfSyncステータスになる。
- 手動で該当リソースを削除するとSyncedステータスになる。
まず前者の状態になるといきなり詰む可能性があります。基本的に使わない方がよいでしょう*1。
後者の場合、kube-system namespaceは消したくありませんがOutOfSyncは解消したいところです。手動syncをしても(設定を変えなければ)以下の様にfailせずにずっとOutOfSyncなままの状態になります。今回はこれをどうにかする方法を紹介します。

今回検証したのはArgoCD v2.14.11とv3.0.1の2つのバージョンです。
効果が無い方法
ignoreDifferenceに追加する
ArgoCDで差分を無視する有名な方法としてはArgoCDのsyncOptionで差分を無視するリソースを記述する方法です。GitHub Copilot Chatで聞いたところこれをおすすめされました。
しかし一度OutOfSyncとして認識されてしまうとこの方法は効果がありませんでした。
ArgoCDの機能でロールバックする
今回は一度変更をpush→revertしてなかったことにしたリソースでOutOfSyncにしてしまったため、過去のpush前の状態に戻すことは比較的安全であると考えられます。このとき上手くリセットされて管理下から外れないだろうかと考えたのですが効果はありませんでした。
正しい(?)方法
ArgoCDがリソーストラッキングに使っている情報を当該リソースから消すと、ArgoCDの管理下から外れます。そのリソースは既にArgoCDの同期対象ではないため、再度トラッキング情報が付与されることもありません。
ArgoCDがリソーストラッキングする方法が3つあり、自分のクラスタでどれを使っているかによって対処法が異なります。必ず設定を確認するようにしてください。
label の場合
v3.0.0以前のデフォルトのトラッキング手法です。ArgoCDは app.kubernetes.io/instance ラベルを使ってリソースを識別します
今回リソースを同期しているApplicationの名前を foo とすると、同期されているリソースには app.kubernetes.io/name: foo のようなラベルが付与されます。このラベルを削除すると管理かから外れたと認識され、OutOfSync状態からSyncedになります。
ドキュメントには書かれていませんが、自分の環境ではapp.kubernetes.io/part-of ラベルも付与されていたのでこれも消した方が良いでしょう。
# ラベル名の末尾に - を付けると削除するという意味になる
kubectl label ns kube-system app.kubernetes.io/instance-
kubectl label ns kube-system app.kubernetes.io/part-of-
annotation の場合
v3.0.0以降のデフォルトのトラッキング手法です。ArgoCDは対象リソースに argocd.argoproj.io/tracking-id アノテーションを付与します。
今回の場合以下のようなアノテーションが付与されていました。
❯ kubectl get ns kube-system -o yaml
apiVersion: v1
kind: Namespace
metadata:
annotations:
argocd.argoproj.io/tracking-id: require-pruning:/Namespace:require-pruning/kube-system
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"argocd.argoproj.io/tracking-id":"require-pruning:/Namespace:require-pruning/kube-system"},"name":"kube-system"}}
creationTimestamp: "2025-05-18T02:15:00Z"
labels:
kubernetes.io/metadata.name: kube-system
name: kube-system
resourceVersion: "2095"
uid: b4ed1a04-d5f3-4a15-bb19-bf2675660439
spec:
finalizers:
- kubernetes
status:
phase: Active
このアノテーションを削除します。
# アノテーションも末尾に - を付けると削除するという意味になる
kubectl annotate ns kube-system argocd.argoproj.io/tracking-id-
annotation + label の場合
二つの方法の対応をどちらもやりましょう。
結果
Revertコミットに対してsyncedな状態になります。

手動syncするとどうなる?
設定を変更せずsyncするだけなら問題は生じません。 prune: false なApplicationではリソースは削除されないためです。
しかし、よくわからずオプションを有効にして手動syncすると問題が生じる可能性があります。仮にApplicationの設定で prune: false にしていても以下のように PRUNE を有効にしてsyncすると当該リソースを削除しようとします。

既に述べたようにkube-systemは保護されているため単にこの操作は失敗するのですが、何も保護が働いていないリソースに対して行うと削除は成功してしまいます。


まとめ
消したくないのに一度管理下に入れてしまったリソースを安全にArgoCDから見えないようにするには、付与されているリソーストラッキング情報を削除してやれば良いです(たぶん)。
またこのような事態が起き得ることを想定し、 prune: true な設定はできるだけ避けるべきでしょう。開発用で毎日のように消したり作ったりするリソースが含まれる様な環境以外では無効にしたいところです。
慌てて手動syncするのも危険です。仮にApplicationの設定で prune: false になっていても、手動syncでは無理矢理pruneできてしまうためです。
*1:kube-systemは保護されているため削除できませんが、同様に重要なNamespaceの削除はできないように保護を検討した方が良いでしょう。
Access KeyなしでMinIOを使う in Kubernetes with MinIO Operator
[2025-05-04 追記]
minio-operator v7.1.0より、ServiceAccount Tokenのaudienceに sts.min.io を指定しなければならなくなったので記載を修正しました。
github.com
はじめに
以下の記事を読んで、MinIOのSTSを使うとAccess Keyを毎回作って保存してとかしなくても済むことを知りました。
ちょうど家にMinIO Operatorを使って構築したクラスタが居たので、上記の記事では検証されていない MinIO Operator を使ったSTSをやってみました。
STSとは
AWSに存在するSecurity Token Serviceの略です。 OpenID ConnectやSAMLなどの別のIdentity Providerへのフェデレーションを行い、 それらの認証トークンからAWSへアクセスするための短命なトークンを発行する方法が主流です。必要なときに都度リクエストして短命なトークンを生成するので、Access Keyなどを作って保存しておく必要がなくなります。
MinIO Operatorでは、KubernetesのService Account Tokenを使ってAssumeRoleWithWebIdentityを行います。Service AccountのトークンはKubernetesによって署名されており、operatorはその署名を検証してService Account Tokenの真正性を確認します。
事前にMinIO Operatorのカスタムリソースである PolicyBinding を作っておくことで、MinIOクラスタ内のアクセスポリシーとServiceAccountの紐付けを行います。operatorはService Account Tokenから得られたアカウント名に対応するPolicyBindingのアクセスポリシーを知り、MinioのAPIを使って一時的なトークンを発行します。

そのため、Kubernetesクラスタ内でAccess KeyやAccess SecretをSecretに保存する必要はなくなります。MinioのAPIへのアクセスをするアプリケーションをデプロイする前に、対応するPolicyとPolicyBinding・ServiceAccountを用意してアプリケーションの環境変数として与えるだけでアクセスできるようになります。 正しいService Account以外からのリクエストに対してはトークンが発行されず、MinIOへのアクセスはできません。
準備
今回はkindで作ったクラスタで試します。STSのためにはTLSが必須となり、署名検証を無効にせずに使うためにはcert-managerおよびtrust-managerを用いるのがよいです。いずれもhelmで簡単にインストールできます。
k8sクラスタをセットアップ
kindで建てたクラスタにhelmを使ってcert-managerとtrust-managerをインストールします。trust-managerについて詳しくは過去記事をご覧下さい。 簡単に言うとクラスタ内にCA証明書をばらまく機能を持ったカスタムコントローラです。
kind create cluster --name minio-sts helm repo add jetstack https://charts.jetstack.io --force-update helm upgrade cert-manager jetstack/cert-manager \ --install \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true \ --wait # テストのために secretTargets.authorizedSecretsAll=true を付けているが、 # 本来は secretTargets.authorizedSecrets で許可するsecretを指定した方が良い helm upgrade trust-manager jetstack/trust-manager \ --install \ --namespace cert-manager \ --wait --set secretTargets.enabled=true \ --set secretTargets.authorizedSecretsAll=true
CAとClusterIssuerをセットアップ
このk8sクラスタ内で使えるCAをセットアップし、各種コンポーネントの証明書はこのCAを使って発行します。tls/issuer.yaml に記述します。
--- apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsign spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: internal-ca-2025 spec: isCA: true commonName: internal-ca-2025 secretName: internal-ca-2025-secret duration: 87600h renewBefore: 336h # 14d privateKey: algorithm: ECDSA size: 256 issuerRef: name: selfsign kind: ClusterIssuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: internal spec: ca: secretName: internal-ca-2025-secret
kubectl apply -f tls/issuer.yaml -n cert-manager
Operator用の証明書とCA証明書をセットアップ
先にMinIO OperatorのSTSエンドポイント用証明書を発行しておきます。このとき、secretNameは sts-tls とする必要があります*1。minio-operator/cert.yaml として記述します。
--- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: sts-certmanager-cert namespace: minio-operator spec: dnsNames: - sts - sts.minio-operator.svc - sts.minio-operator.svc.cluster.local secretName: sts-tls # MUST issuerRef: kind: ClusterIssuer name: internal
kubectl create ns minio-operator
kubectl apply -f minio-operator/cert.yaml
今回MinIO本体でもcert-managerが発行した証明書を使うのですが、operatorからMinIOへのリクエストでも署名検証できるようにするためにCA証明書をマウントします。Operatorには特定の名前パターンでsecretを作ると自動で検知してくれる仕組み*2があるのでこれを使います。
CA証明書はtrust-managerに配布させます。tls/bundle-for-minio.yaml として記述します。
--- apiVersion: trust.cert-manager.io/v1alpha1 kind: Bundle metadata: name: operator-ca-tls-internal # operator-ca-tls-*という名前にする namespace: cert-manager spec: sources: # 作成したクラスタ内CAの証明書を同梱する - secret: name: "internal-ca-2025-secret" key: "ca.crt" target: secret: key: "ca.crt" namespaceSelector: # minio-operatorのnamespaceだけにインストールする matchLabels: kubernetes.io/metadata.name: minio-operator
kubectl apply -f tls/bundle-for-minio.yaml
MinIO Operatorをセットアップ
MinIO Operatorはアップストリームの手順に従ってインストールします。具体的にはリポジトリのアーカイブをダウンロードし、kustomizeでマニフェストをビルド・一部パッチを当ててクラスタにapplyします。
MINIO_OPERATOR_VERSION="7.1.1" mkdir -p minio-operator/upstream # minio-operatorのupstreamのマニフェストをダウンロードする wget -O minio-operator/upstream/v${MINIO_OPERATOR_VERSION}.tar.gz \ https://github.com/minio/operator/archive/refs/tags/v${MINIO_OPERATOR_VERSION}.tar.gz cd "minio-operator/upstream" tar xf "v${MINIO_OPERATOR_VERSION}.tar.gz" # リポジトリ丸ごとは要らないので、一旦素のマニフェストをビルドして書き出しておく kustomize build operator-${MINIO_OPERATOR_VERSION}/ > "operator.yaml" # 不要なファイルを削除する rm -r operator-${MINIO_OPERATOR_VERSION} v${MINIO_OPERATOR_VERSION}.tar.gz cd ../../
通常MinIO Operatorは自己署名証明書を自動的に作成しますが、これはあらゆる署名検証を無効にする必要があり面倒です。そのため、自動作成する機能を切ってcert-managerで発行した証明書を使わせます。前のセクションで証明書は既に用意したので、ここでは自動作成する機能をオフにします。minio-operator/kustomization.yaml でパッチを当てます。
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: minio-operator resources: - upstream/operator.yaml - cert.yaml patches: - patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: minio-operator namespace: minio-operator spec: template: spec: containers: - name: minio-operator env: - name: OPERATOR_STS_AUTO_TLS_ENABLED value: "off" - name: OPERATOR_STS_ENABLED value: "on"
kustomize build minio-operator/ | kubectl apply -f -
MinIO用の証明書をセットアップ
MinIOのWebコンソールおよびS3互換APIのエンドポイントで利用される証明書を発行します。
--- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: minio-tenant-cert namespace: objectstorage spec: dnsNames: - "minio.objectstorage" - "minio.objectstorage.svc" - "minio.objectstorage.svc.cluster.local" - "*.minio-hl.objectstorage.svc.cluster.local" - "*.objectstorage.svc.cluster.local" - "*.minio.objectstorage.svc.cluster.local" secretName: minio-tenant-tls issuerRef: kind: ClusterIssuer name: internal
kubectl create ns objectstorage
kubectl apply -f objectstorage/cert.yaml
MinIOをセットアップ
MinIO Operatorに対して、MinIO本体はTenantと呼ばれます。カスタムリソースであるTenantを作ることでOpertorによってMinIOがセットアップされます。 今回はテスト用にErasure Codingの設定を限界まで緩めているので冗長性がありません。本番環境でそのまま利用しないでください。
--- apiVersion: minio.min.io/v2 kind: Tenant metadata: name: minio namespace: objectstorage spec: image: 'minio/minio:RELEASE.2025-03-12T18-04-18Z' # Disable default tls certificates. requestAutoCert: false # Use certificates generated by cert-manager. externalCertSecret: # 先に作っておいた証明書 - name: minio-tenant-tls type: cert-manager.io/v1 configuration: # 後で作る name: storage-configuration pools: - servers: 1 name: pool-0 volumesPerServer: 1 volumeClaimTemplate: apiVersion: v1 kind: persistentvolumeclaims spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi
いくつかの追加設定をsecretを使って設定します。 MINIO_STORAGE_CLASS_STANDARD でErasure Codingの設定をしています。本番環境では正しい設定をしてください*3。パスワードも同様にランダムかつ十分長い文字列を設定するようにし、リポジトリにはpushしないようにしてください。
--- apiVersion: v1 kind: Secret metadata: name: storage-configuration namespace: objectstorage stringData: config.env: | export MINIO_ROOT_USER="minio" export MINIO_ROOT_PASSWORD="password" export MINIO_STORAGE_CLASS_STANDARD="EC:0" export MINIO_BROWSER="on"
これらを適用します。
cat << 'EOF' > objectstorage/base/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: objectstorage resources: - storage-configuration.yaml - tenant.yaml - tenant-cert.yaml EOF kustomize build objectstorage/ | kubectl apply -f -
最終的に以下の様にPodが立っていればOKです。
❯ kubectl get po -n objectstorage NAME READY STATUS RESTARTS AGE minio-pool-0-0 2/2 Running 0 6h24m
BucketとPolicyを作る
ポートフォワードしてコンソールにアクセスします。証明書のエラーが出ますが、これはクラスタ内CAの証明書がブラウザ側にはインストールされていないためです。本番環境ではコンソールを公開するためのIngressリソースなどを用意し、クラスタ外でもvalidな証明書を使いましょう。今回は単に警告を無視してアクセスします。
kubectl port-forward svc/minio-console -n objectstorage 9443:9443
https://localhost:9443 にアクセスすると設定したユーザ名・パスワードでログインできます。

Create Bucket してテスト用のbucketを作ります。

最後にこの test bucketのためのPolicyを作成します。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:DeleteObject", "s3:GetObject", "s3:ListBucket", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::test/*" ] } ] }

MinIOのクライアント用CA証明書を配布する
MinIOの証明書の署名検証のため、CA証明書をクラスタ内の全namespaceに配布します。各Podはこれをマウントすることで正しく署名検証できます。
--- apiVersion: trust.cert-manager.io/v1alpha1 kind: Bundle metadata: name: internal-ca-bundle spec: sources: - useDefaultCAs: true - secret: name: "internal-ca-2025-secret" key: "tls.crt" target: configMap: key: "trust-bundle.pem"
kubectl apply -f tls/bundle.yaml
全namespaceで internal-ca-bundle というConfigMapが作られているはずです。
❯ kubectl get cm internal-ca-bundle NAME DATA AGE internal-ca-bundle 1 6h4m
STSを使ってMinioにアクセスする
ようやく長いセットアップが終わったので実際にSTSを使ってMinIOのAPIを呼び出します。
PolicyBindingを作る
MinIO Operatorはこのカスタムリソースを元にService Accountとそれに許可するPolicyを識別します。このPolicy BindingはMinIO Tenantと同じnamespaceに配置する必要があります。今回はdefault namespaceの test というService Accountに test-rw Policyを許可します。
--- apiVersion: sts.min.io/v1alpha1 kind: PolicyBinding metadata: name: default-test namespace: objectstorage spec: application: namespace: default serviceaccount: test policies: - test-rw
kubectl apply -f objectstorage/policybindings.yaml
awscliで操作する
基本的には環境変数を使ってアクセス先や認証方法を設定します。awscliやAWS SDKはいずれも同じ環境変数で設定を変更できるようになっています。
awscliの入ったPodを立ち上げます。
--- apiVersion: v1 kind: ServiceAccount metadata: name: test --- apiVersion: v1 kind: Pod metadata: name: bastion spec: containers: - name: bastion # 古いバージョンでは環境変数からの設定変更をサポートしていないので注意 image: amazon/aws-cli:latest command: - sleep - infinity volumeMounts: - name: internal-ca mountPath: /tls/internal readOnly: true # audienceを指定したトークンをマウント - name: minio-sts-token mountPath: /var/run/secrets/sts.min.io/serviceaccount readOnly: true volumes: - name: internal-ca configMap: name: internal-ca-bundle - name: minio-sts-token projected: sources: - serviceAccountToken: # audienceを指定しなければならない audience: "sts.min.io" expirationSeconds: 86400 path: token serviceAccountName: test
kubectl apply -f default/bastion.yaml
kubectl exec を使って上記コンテナ内のbashを立ち上げます。
kubectl exec -it bastion -- bash
コンテナの中で以下の環境変数を設定します。
# マウントしたCA証明書のパス。STSとMinioのエンドポイントの署名検証に必要 export AWS_CA_BUNDLE=/tls/internal/trust-bundle.pem # ServiceAccountのトークンを認証キーとして使う export AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/sts.min.io/serviceaccount/token # consoleではなくMinioのAPIエンドポイント export AWS_ENDPOINT_URL_S3=https://minio.objectstorage.svc:443 # STSのエンドポイントはサービス名の末尾に `sts/テナントのnamesapce` を入れる export AWS_ENDPOINT_URL_STS=https://sts.minio-operator.svc:4223/sts/objectstorage # Minioでは使われないのだが、awscliはそれっぽい値を入れないとリクエストを蹴るので適当に入れる export AWS_ROLE_ARN=arn:aws:iam::dummy:role/test
これだけでawscliを使ってMinIOのS3互換APIを利用できます。
bash-4.2# aws s3 ls s3://test/ bash-4.2# echo hello > hello.txt bash-4.2# aws s3 cp hello.txt s3://test/ upload: ./hello.txt to s3://test/hello.txt bash-4.2# aws s3 ls s3://test/ 2025-03-21 06:14:38 6 hello.txt bash-4.2#
AWS SDKから操作する
同じ環境変数を用いると、以下の様なコードでMinIOへの操作を記述できます。今回はGoのAWS SDK v2を使っています。
package main import ( "context" "flag" "fmt" "log/slog" "os" "os/signal" "syscall" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" ) var ( bucketName string pathToList string ) func listBucketItems() error { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer cancel() cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return fmt.Errorf("failed to load configuration, %w", err) } bucket := s3.NewFromConfig(cfg, func(o *s3.Options) { o.UsePathStyle = true }) outputs, err := bucket.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ Bucket: &bucketName, Prefix: &pathToList, }) if err != nil { return fmt.Errorf("failed to list bucket items, %w", err) } for _, item := range outputs.Contents { slog.Info("get", "path", item.Key, "size", item.Size, "bucket", bucketName) } return nil } func main() { flag.StringVar(&bucketName, "bucket", "", "bucket name") flag.StringVar(&pathToList, "path", "", "path to list") flag.Parse() if err := listBucketItems(); err != nil { slog.Error("failed to list bucket items", "error", err) os.Exit(1) } }
同じようにAWS SDKを使っているアプリケーションでは同様の環境変数の設定でアクセス出来るようになるため非常に便利です。
例えば筆者のクラスタにはmocoというMySQLのオペレータがインストールされており、これはAWS SDKを使ってMySQLクラスタのバックアップをS3にアップロードする仕組みを持っています。MySQLBackupというカスタムリソースでその設定を書くのですが、以下の様に書いて適切なBucket・Policy・PolicyBindingを用意するだけでバックアップのデータをMinIOにアップロードできます。
apiVersion: moco.cybozu.com/v1beta2 kind: BackupPolicy metadata: name: daily spec: schedule: "5 0 * * *" concurrencyPolicy: Forbid jobConfig: serviceAccountName: moco-mysql env: - name: AWS_CA_BUNDLE value: /tls/internal/trust-bundle.pem - name: AWS_WEB_IDENTITY_TOKEN_FILE value: /var/run/secrets/sts.min.io/serviceaccount/token - name: AWS_ENDPOINT_URL_STS value: https://sts.minio-operator.svc:4223/sts/objectstorage # AWS_ROLE_ARN is required by the AWS SDK, but it is not mandatory in Minio. - name: AWS_ROLE_ARN value: arn:aws:iam::dummy:role/test bucketConfig: bucketName: moco-mysql-backup region: us-east-1 endpointURL: https://minio.objectstorage.svc:443 usePathStyle: true workVolume: ephemeral: volumeClaimTemplate: spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 10Gi volumeMounts: - name: internal-ca mountPath: /tls/internal readOnly: true - name: minio-sts-token mountPath: /var/run/secrets/sts.min.io/serviceaccount readOnly: true volumes: - name: internal-ca configMap: name: internal-ca-bundle - name: minio-sts-token projected: sources: - serviceAccountToken: audience: "sts.min.io" expirationSeconds: 86400 path: token
気になったところ
STSのエンドポイントが /sts/{{Tenantのnamespace}} となるのですが、 Tenant リソース自体は一つのnamespaceに複数作成できそうな気がします。
すると、あるPolicyBindingがどのTenantのものなのか判別できない気がします。実用的にこれで困ることがあるかは不明です。
今回は自分しか管理する人が居ないクラスタなのでよいのですが、複数の開発者が同じクラスタを共有するマルチテナント構成の場合、PolicyBindingリソースの操作権限をどう与えるか悩ましいなと思いました。 いくらでも作成・更新できてしまうと他チームのbucketを勝手に操作する権限を付与できてしまいますし、逆に全く作れないようにしてMinIO Tenantの管理チームだけに絞ってしまうと都度依頼対応が必要になったりして手間が増えそうです。
また、Policyを宣言的に管理する方法がないのもつらいところです。どのように権限分担をするのかが悩ましい仕組みだなと思いました。
まとめ
Kubernetesのin clusterなMinIOへのアクセスでは、MinIO OperatorのSTSを利用することで面倒なAccess Keyの管理なしにMinIOへのアクセス権を付与出来ます。
MinIO OperatorのSTSはリクエストに付与されたKubernetesのService Account Tokenを元に、PolicyBindingカスタムリソースを用いてMinIO上のPolicyを識別し、MinIOから短命なトークンを発行します。
awscliやAWS SDKは環境変数経由で認証方法やアクセス先をカスタマイズでき、他者のアプリケーションであってもAWS SDKを使っていればSTSを使ってMinIOにアクセス出来るます(たぶん)。
*1:https://min.io/docs/minio/kubernetes/upstream/operations/cert-manager/cert-manager-operator.html#id4
*2:https://min.io/docs/minio/kubernetes/gke/operations/cert-manager/cert-manager-tenants.html#trust-the-tenant-s-ca-in-minio-operator
*3:https://min.io/docs/minio/linux/operations/concepts/erasure-coding.html
Cilium 1.16.5への更新でnode-local-dnsが壊れた話
何が起きた?
私物のKubernetesクラスタ(kubesprayで構築)のCiliumを1.15.11から1.16.5へ更新したところ、クラスタ内の名前解決が一斉にタイムアウトするようになりました。
CoreDNSサービス自体は正常に応答を返しているようでした。デバッグした結果、各ノードのkubeletが配る resolv.conf にはnode-local-dnsがリッスンしているリンクローカルアドレスが書かれており、このアドレスへの接続に問題が発生したことが分かりました。
悲しいことにこれによってArgoCDが機能不能になり、Ciliumの簡単なロールバックはできない状態になったためどうにか現状で復旧するしかありませんでした*1。最終的にnode-local-dnsを無効にしてノードを順番にdrain→uncordonして復旧しました。
再現手順
node-local-dns自体のマニフェストはkubernetesのドキュメントにも載っています。
しかしこれを適用するだけでは配られる resolv.conf にリンクローカルアドレスが記載される状態にはなりません。kubesprayでnode-local-dnsのアドオンを有効化するとこの状態を作るようになります。
このような構成は上記の公式ドキュメントでも推奨されているものであり、特段に不審な構成ではありません。
NodeLocal DNSキャッシュのローカルリッスン用のIPアドレスは、クラスター内の既存のIPと衝突しないことが保証できるものであれば、どのようなアドレスでもかまいません。例えば、IPv4のリンクローカル範囲169.254.0.0/16やIPv6のユニークローカルアドレス範囲fd00::/8から、ローカルスコープのアドレスを使用することが推奨されています。
1. VMを作る
kubesprayでkubernetesクラスタを構築するため、VMを用意します。ここではmultipassを使っていますが、何でも良いです。
OSはUbuntu 24.04を問題になったクラスタで利用していたため、ここでも採用しました。他のOSの挙動は検証していませんが、おそらく同じだと思います。
GITHUB_USER=pddg # cloud-initのconfigを記述する cat << EOF > cloud-config.yaml allow_public_ssh_keys: true ssh_import_id: ["gh:${GITHUB_USER}"] EOF # VMを起動する multipass launch \ --name k8s-master \ --memory 2G \ --disk 30G \ --cloud-init cloud-config.yaml \ 24.04 multipass launch \ --name k8s-worker \ --memory 2G \ --disk 30G \ --cloud-init cloud-config.yaml \ 24.04
2. kubesprayでクラスタを構築する
作成したクラスタとホストOSは普通にIPアドレスで通信できるようになっているので、これを元にkubesprayで使うためのAnsible Inventoryファイルを用意します。ユーザが ubuntu であることに注意してください(一敗)。
mkdir -p inventory/multipass MASTER_IPADDR=$(multipass info --format json| jq -r '.info | to_entries[] | select(.key == "k8s-master") | .value.ipv4[]') WORKER_IPADDR=$(multipass info --format json| jq -r '.info | to_entries[] | select(.key == "k8s-worker") | .value.ipv4[]') cat << EOF > inventory/multipass/hosts.yaml all: hosts: k8s-master: ansible_user: ubuntu ansible_host: ${MASTER_IPADDR} ip: ${MASTER_IPADDR} access_ip: ${MASTER_IPADDR} k8s-worker: ansible_user: ubuntu ansible_host: ${WORKER_IPADDR} ip: ${WORKER_IPADDR} access_ip: ${WORKER_IPADDR} children: kube_control_plane: hosts: k8s-master: kube_node: hosts: k8s-master: k8s-worker: etcd: hosts: k8s-master: k8s_cluster: children: kube_control_plane: kube_node: EOF
このファイルを使って対象のホストがアクセス可能であることを確かめます。ここでは簡単のためdockerコンテナを利用しています。
# pingが成功することを確認する docker run --rm -it \ --mount type=tmpfs,dst=/.ansible \ --mount type=bind,source=$(pwd)/inventory,dst=/kubeproxy/inventory \ --mount type=bind,source=${HOME}/.ssh/id_ed25519,dst=/root/.ssh/id_rsa \ quay.io/kubespray/kubespray:v2.26.0 \ ansible -i /kubeproxy/inventory/multipass/hosts.yaml -m ping all
うまくいけば各ホストからpongが返ってくるはずです*2。
❯ docker run --rm \
--mount type=tmpfs,dst=/.ansible \
--mount type=bind,source=$(pwd)/inventory,dst=/kubeproxy/inventory \
--mount type=bind,source=${HOME}/.ssh/id_ed25519,dst=/root/.ssh/id_rsa \
quay.io/kubespray/kubespray:v2.26.0 \
ansible -i /kubeproxy/inventory/multipass/hosts.yaml -m ping --private-key /root/.ssh/id_rsa all
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
[WARNING]: Skipping callback plugin 'ara_default', unable to load
k8s-worker | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
k8s-master | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
kubesprayの設定ファイルを書き、クラスタを構築します。
mkdir -p inventory/multipass/group_vars cat << EOF > inventory/multipass/group_vars/all.yaml --- kube_version: v1.30.4 container_manager: containerd # 後からciliumを自分で入れる kube_network_plugin: cni # kube-proxyは削除しておく kube_proxy_remove: true # 構築したクラスタへアクセスするためのKUBECONFIGをローカルに配置する kubeconfig_localhost: true dns_mode: coredns # node-local-dnsを有効化 enable_nodelocaldns: true # node-local-dnsがリッスンするアドレス(デフォルト) nodelocaldns_ip: 169.254.25.10 EOF docker run --rm \ --mount type=tmpfs,dst=/.ansible \ --mount type=bind,source=$(pwd)/inventory,dst=/kubeproxy/inventory \ --mount type=bind,source=${HOME}/.ssh/id_ed25519,dst=/root/.ssh/id_rsa \ quay.io/kubespray/kubespray:v2.26.0 \ ansible-playbook -i /kubeproxy/inventory/multipass/hosts.yaml --become cluster.yml
しばらく待つと構築が完了するので、 .envrc に KUBECONFIG の設定を書いてここで使うkubernetesクラスタの情報を設定します。
cat << 'EOF' > .envrc export KUBECONFIG=$KUBECONFIG:inventory/multipass/artifacts/admin.conf EOF direnv allow .
いくつかのpodはpendingですが、Kubernetesクラスタが構築出来たことを確認します。
❯ kubectl get po -n kube-system NAME READY STATUS RESTARTS AGE coredns-776bb9db5d-jsnjw 0/1 Pending 0 2m36s dns-autoscaler-6ffb84bd6-9xt7x 0/1 Pending 0 2m34s kube-apiserver-k8s-master 1/1 Running 0 3m46s kube-controller-manager-k8s-master 1/1 Running 1 3m45s kube-scheduler-k8s-master 1/1 Running 1 3m45s nginx-proxy-k8s-worker 1/1 Running 0 2m49s nodelocaldns-75h97 1/1 Running 0 2m33s nodelocaldns-jhpmt 1/1 Running 0 2m33s
3. helmでCilium 1.16.4をインストール
まずはCIlium 1.16.4をインストールします。実際に壊れた環境では1.15.12→1.16.5への更新で壊れたのですが、再現環境を作って試していると1.16.4まではこの構成でも普通に動作していました。
helm repo add cilium https://helm.cilium.io/ cat << EOF > cilium-values.yaml # kube-proxyがない環境でmasterにアクセスできる必要がある k8sServiceHost: ${MASTER_IPADDR} k8sServicePort: 6443 # kube-proxyレスモードでインストールする kubeProxyReplacement: true ipam: mode: kubernetes securityContext: privileged: true # この設定を入れると再現する bpf: masquerade: true EOF helm install cilium cilium/cilium \ --version 1.16.4 \ --values cilium-values.yaml \ --namespace kube-system # cilium agentがreadyになるまで待つ kubectl wait --timeout=90s --for=condition=Ready -n kube-system \ pods -l k8s-app=cilium # ciliumインストール前から存在し、hostNetwork以外で動くpodをdeleteして作り直す kubectl get pods \ --all-namespaces \ -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,HOSTNETWORK:.spec.hostNetwork \ --no-headers=true \ | grep '<none>' \ | awk '{print "-n "$1" "$2}' \ | xargs -L 1 -r kubectl delete pod
4. 名前解決出来ることを確認する
公式ドキュメントの以下で紹介されているイメージを使うことにします。 kubernetes.io
cat << EOF > dnsutil.yaml apiVersion: v1 kind: Pod metadata: name: dnsutils namespace: default spec: containers: - name: dnsutils image: registry.k8s.io/e2e-test-images/agnhost:2.39 imagePullPolicy: IfNotPresent restartPolicy: Always EOF kubectl apply -f dnsutil.yaml # PodがReadyになるまで待つ kubectl wait --timeout=90s --for=condition=Ready pods/dnsutils
1.16.4ではこの状態で名前解決に成功します。
❯ kubectl exec -i -t dnsutils -- nslookup kubernetes.default Server: 169.254.25.10 Address: 169.254.25.10#53 Name: kubernetes.default.svc.cluster.local Address: 10.233.0.1
resolv.confにはnode-local-dnsのリッスンするリンクローカルアドレスが書かれています。
❯ kubectl exec -i -t dnsutils -- cat /etc/resolv.conf search default.svc.cluster.local svc.cluster.local cluster.local flets-east.jp iptvf.jp nameserver 169.254.25.10 options ndots:5
各kubernetesノードには以下の様なデバイスが生えるようです。
❯ ssh ubuntu@$WORKER_IPADDR ip a show dev nodelocaldns
3: nodelocaldns: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 6e:a5:52:ac:8a:b2 brd ff:ff:ff:ff:ff:ff
inet 169.254.25.10/32 scope global nodelocaldns
valid_lft forever preferred_lft forever
hubbleを使って通信を観測してみます。
# 対象のpodが乗っているノードのcilium-agentを出力する便利スクリプト wget -O k8s-get-cilium-pod.sh \ https://raw.githubusercontent.com/cilium/cilium/refs/tags/v1.16.4/contrib/k8s/k8s-get-cilium-pod.sh chmod +x k8s-get-cilium-pod.sh kubectl exec -ti pod/$(./k8s-get-cilium-pod.sh dnsutils default) -n kube-system -c cilium-agent \ -- hubble observe --since 1m --pod default/dnsutils
手元で実行した記録
❯ kubectl exec -ti pod/$(./k8s-get-cilium-pod.sh dnsutils default) -n kube-system -c cilium-agent \ -- hubble observe --since 1m --pod default/dnsutils Dec 22 07:51:41.852: 127.0.0.1:39065 (world) <> default/dnsutils (ID:32060) pre-xlate-rev TRACED (UDP) Dec 22 07:51:41.852: default/dnsutils:50642 (ID:32060) -> 169.254.25.10:53 (world) to-stack FORWARDED (UDP) Dec 22 07:51:41.853: default/dnsutils:50642 (ID:32060) <- 169.254.25.10:53 (world) to-endpoint FORWARDED (UDP) Dec 22 07:51:41.853: 169.254.25.10:53 (world) <> default/dnsutils (ID:32060) pre-xlate-rev TRACED (UDP) Dec 22 07:51:41.853: default/dnsutils:60221 (ID:32060) -> 169.254.25.10:53 (world) to-stack FORWARDED (UDP) Dec 22 07:51:41.853: default/dnsutils:60221 (ID:32060) <- 169.254.25.10:53 (world) to-endpoint FORWARDED (UDP) Dec 22 07:51:41.853: 169.254.25.10:53 (world) <> default/dnsutils (ID:32060) pre-xlate-rev TRACED (UDP) Dec 22 07:51:41.855: default/dnsutils:51283 (ID:32060) -> 169.254.25.10:53 (world) to-stack FORWARDED (UDP) Dec 22 07:51:41.856: default/dnsutils:51283 (ID:32060) <- 169.254.25.10:53 (world) to-endpoint FORWARDED (UDP) Dec 22 07:51:41.856: 169.254.25.10:53 (world) <> default/dnsutils (ID:32060) pre-xlate-rev TRACED (UDP)
5. Cilium 1.16.5に更新する
helm upgrade cilium cilium/cilium \ --version 1.16.5 \ --namespace kube-system \ --values cilium-values.yaml # cilium agentがreadyになるまで待つ kubectl wait --timeout=90s --for=condition=Ready -n kube-system \ pods -l k8s-app=cilium
dnsutils Podでのnslookupがタイムアウトするようになります。
❯ kubectl exec -i -t dnsutils -- nslookup kubernetes.default ;; connection timed out; no servers could be reached command terminated with exit code 1
corednsのClusterIPを指定すると通るので、node-local-dnsとの通信だけが途絶えている事が分かります。
❯ kubectl get svc -n kube-system NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE cilium-envoy ClusterIP None <none> 9964/TCP 6m56s coredns ClusterIP 10.233.0.3 <none> 53/UDP,53/TCP,9153/TCP 56m hubble-peer ClusterIP 10.233.15.182 <none> 443/TCP 6m56s ❯ kubectl exec -i -t dnsutils -- nslookup kubernetes.default 10.233.0.3 Server: 10.233.0.3 Address: 10.233.0.3#53 Name: kubernetes.default.svc.cluster.local Address: 10.233.0.1
hubble observeすると虚空に消えて返って来ないようです。
❯ kubectl exec -ti pod/$(./k8s-get-cilium-pod.sh dnsutils default) -n kube-system -c cilium-agent \ -- hubble observe --since 1m --pod default/dnsutils Dec 22 07:45:18.830: 127.0.0.1:45439 (world) <> default/dnsutils (ID:32060) pre-xlate-rev TRACED (UDP) Dec 22 07:45:18.830: default/dnsutils:55914 (ID:32060) -> 169.254.25.10:53 (world) to-network FORWARDED (UDP) Dec 22 07:45:28.849: default/dnsutils:55914 (ID:32060) -> 169.254.25.10:53 (world) to-network FORWARDED (UDP)
試したこと
masqLinkLocal: false
bpf.masquerade: true にすると問題が発生することから、IPマスカレードの仕組みのどこかでおかしくなっているのではと考えました。
In addition, if the
masqLinkLocalis not set or set to false, then169.254.0.0/16is appended to the non-masquerade CIDRs list.
node-local-dnsのリンクローカルアドレスは169.254.25.10なので、なにかの設定変更でIPマスカレードの対象になりおかしくなったのかもしれません。 下記の設定を追加してみましたが効果はありませんでした。
ipMasqAgent: enabled: true masqLinkLocal: false
hostLegacyRouting: true
以下のissueは起きた現象としては似ているなと思ったので参考にし、この設定を入れてcilium agentを再起動したところ接続出来るようになりました。
メンテナの見解では、これはBPF Host Routingが正しく動作するようになった結果なので、このオプションを有効化するのが正しいとのこと(できればChangelogに書いて欲しかったなぁ)。
https://github.com/cilium/cilium/issues/36761#issuecomment-2559123347
Ciliumにおける正しい(?)node-local-dnsのアーキテクチャ
node-local-dnsと言えばhost networkで動作させるものだと思っていました。しかしCiliumではpod networkで動作させ、 CiliumLocalRedirectPolicy リソースでCoreDNS Cluster IPあての通信をnode-local-dnsサービスにルーティングするという構成が想定されているようです。
自分のクラスタはこの構成に移行することにし、kubesprayのaddonとしてのnode-local-dnsは無効にしました。
まとめ
stagingとかdevみたいなクラスタは用意しないポリシーだったんですが、やっぱりあった方が良いかもと言う気持ちになりました。
通常のKubernetesでのデファクトな構成と、自分が利用しているコンポーネントの想定する構成が一致するとは限りません。ちゃんとドキュメントは確認しましょう。