こんにちは、タカサオです!
EKSクラスタ上のアプリケーションPodが使うシークレットをAWS Secret Managerで管理したい場合、ASCP (AWS Secrets and Config Provider) が使えます。
ASCPを用いる場合、AWS Secrets Manager にシークレットを作成して、さらにそれを参照する SecretProviderClass も定義して…という作業を手作業で行っていました。
今回は Kro と ACK を組み合わせることで、「1 つのカスタムリソースを apply するだけで AWS Secrets Manager のシークレットと SecretProviderClass を同時に作成できるか」を EKS Auto Mode で検証してみたいと思います!
検証したのは以下の 3 点です。
-
kind: ManagedSecretという独自リソースを apply すると、ACK が SM シークレットを、Kro が SecretProviderClass を自動作成する - ユーザーが SM に実値を手動で設定した後、Pod 起動時に ASCP が k8s Secret を自動作成する
- Pod の環境変数にシークレット値が注入される
使用技術の概要
Kro (Kube Resource Orchestrator) は Kubernetes のカスタムリソースを組み合わせて、複数のリソースを 1 つの独自 API として定義できるオーケストレーターです。ResourceGraphDefinition という CRD でリソース間の依存関係を定義し、CEL 式でリソース同士の値を参照できます。
ACK (AWS Controllers for Kubernetes) は AWS リソースを Kubernetes の CRD として管理するコントローラー群です。Secret リソースを apply すると、ACK が AWS Secrets Manager に実際のシークレットを作成します。
ASCP (AWS Secrets and Config Provider) は Secrets Store CSI Driver の AWS 向けプロバイダーで、CSI ボリュームをマウントしたタイミングで SM のシークレットを取得して k8s Secret を作成できます。
今回は Kro・ACK それぞれを EKS Capabilities として使います。EKS Capabilities はコントローラーをクラスター外の AWS マネージドインフラ上で動かす仕組みで、クラスター内に Helm でコントローラーを入れなくて済みます。
検証環境
| 項目 | 内容 |
|---|---|
| Kubernetes バージョン | v1.36 |
| クラスタータイプ | EKS Auto Mode |
| Kro | v0.9.2-eks-3(EKS Capability) |
| ACK SecretsManager | v46.108.1-eks-1(EKS Capability) |
| ASCP | 3.1.1(IRSA 認証) |
| Secrets Store CSI Driver | 1.6.0 |
| リージョン | ap-northeast-1 |
アーキテクチャ
今回検証する構成の流れは以下のとおりです。
Kro が起点となり、ACK 経由で SM シークレットを、直接 SecretProviderClass を作成します。その後ユーザーが実値を設定し、Pod 起動時に ASCP が k8s Secret を組み立てる流れです。
前提(構築済みの環境)
この記事では以下は構築済みとして、Kro の ResourceGraphDefinition の定義から解説します。
- EKS Auto Mode クラスター(v1.36)
- EKS Capabilities(Kro / ACK SecretsManager) の有効化
- Secrets Store CSI Driver + ASCP のインストール(Helm)と IRSA の設定
検証1: ResourceGraphDefinition を定義する
Kro が解釈する ResourceGraphDefinition を定義します。EKS Capability の Kro は apiVersion: kro.run/v1alpha1 / kind: ResourceGraphDefinition を使います。
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: managed-secret
spec:
schema:
apiVersion: v1alpha1
kind: ManagedSecret
spec:
# SM シークレット名(k8s リソース名のプレフィックスにも使う)
secretName: string | required=true
status:
smArn: ${awsSecret.status.ackResourceMetadata.arn}
smSynced: ${awsSecret.status.conditions[0].type}
resources:
# 1. ACK で AWS Secrets Manager にシークレットを作成(初期値なし)
- id: awsSecret
readyWhen:
- ${has(awsSecret.status.ackResourceMetadata.arn)}
template:
apiVersion: secretsmanager.services.k8s.aws/v1alpha1
kind: Secret
metadata:
name: ${schema.spec.secretName}
namespace: ${schema.metadata.namespace}
spec:
name: ${schema.spec.secretName}
description: "Managed by kro + ACK (EKS Capability)"
# 2. SecretProviderClass(ASCP 用)
- id: secretProviderClass
template:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: ${schema.spec.secretName + "-spc"}
namespace: ${schema.metadata.namespace}
spec:
provider: aws
parameters:
region: ap-northeast-1
objects: |
- objectName: "${schema.spec.secretName}"
objectType: "secretsmanager"
objectAlias: "db-password"
secretObjects:
- secretName: ${schema.spec.secretName + "-k8s-secret"}
type: Opaque
data:
- objectName: "db-password"
key: db-password
ポイントをいくつか補足します。
namespace は ${schema.metadata.namespace} を参照する
当初は schema の spec に namespace フィールドを定義していましたが、kro は ${schema.metadata.namespace} でインスタンスの Namespace を直接参照できます(公式ドキュメントでも namespace: ${schema.metadata.namespace} が使われています)。そのため spec 側で namespace を持たせる必要はなく、重複を避けられます。
readyWhen は ${...} で囲む
readyWhen は ${has(awsSecret.status.ackResourceMetadata.arn)} の形式で書きます。"has(...)" のようにダブルクォートだけで囲む形式は「only standalone expressions are allowed」というエラーになります。
リソースの依存関係は CEL 式から自動推定される
secretProviderClass は ACK Secret の情報を参照していませんが、awsSecret が Ready(ARN 確定)になるのを待たせたい場合は CEL 式で参照を持たせます。kro は ${} の参照から DAG を構築し、作成順序を自動で決めます。
apply すると ManagedSecret という CRD が生成されます。
$ kubectl get crd | grep managedsecret
managedsecrets.kro.run 2026-07-09T13:19:09Z
検証2: ManagedSecret を apply する
生成された独自 API を使ってインスタンスを作成します。metadata.namespace から Namespace が、spec.secretName から SM シークレット名が決まります。
apiVersion: kro.run/v1alpha1
kind: ManagedSecret
metadata:
name: app-db-secret
namespace: default
spec:
secretName: eks-kro-ack-secrets-app-secret
kubectl apply -f managed-secret.yaml
しばらくすると ManagedSecret のステータスが ACTIVE になります。
$ kubectl get managedsecret app-db-secret -o yaml
...
status:
smArn: arn:aws:secretsmanager:ap-northeast-1:xxxx:secret:eks-kro-ack-secrets-app-secret-fqPnAw
smSynced: ACK.ResourceSynced
state: ACTIVE
status.smArn に SM シークレットの ARN が入りました。SecretProviderClass も自動作成されています。
$ kubectl get secretproviderclass
NAME AGE
eks-kro-ack-secrets-app-secret-spc 113s
1 つの ManagedSecret を apply しただけで、ACK が SM シークレットを、Kro が SecretProviderClass を作成してくれました。
検証3: SM に実値を設定して Pod から参照する
この時点では SM シークレットは初期値なしで作成されています。ここでユーザーが実際の値を設定します。
aws secretsmanager put-secret-value \
--secret-id eks-kro-ack-secrets-app-secret \
--secret-string '{"db-password":"SuperSecretPassword123!"}' \
--region ap-northeast-1
シークレット値を ManagedSecret のマニフェストに書かず、SM 側で直接設定できるので、値が Git や k8s マニフェストに残りません。
続いて、CSI ボリュームマウントと環境変数参照を組み合わせた Pod を起動します。
apiVersion: v1
kind: Pod
metadata:
name: secret-test-pod
spec:
serviceAccountName: secret-test-sa # IRSA アノテーション付き SA
containers:
- name: app
image: public.ecr.aws/docker/library/busybox:stable
command: ["sh", "-c", "echo DB_PASSWORD=$DB_PASSWORD && sleep 3600"]
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: eks-kro-ack-secrets-app-secret-k8s-secret
key: db-password
volumeMounts:
- name: secrets-store
mountPath: /mnt/secrets
readOnly: true
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: eks-kro-ack-secrets-app-secret-spc
ここで重要なのは、env var で k8s Secret を参照する場合でも CSI ボリュームのマウントは必須 という点です。ASCP は CSI ボリュームがマウントされたタイミングで SM からシークレットを取得し、k8s Secret を作成するためです。
Pod が起動すると k8s Secret が自動作成され、env var にシークレット値が注入されました。
$ kubectl exec secret-test-pod -- sh -c "echo DB_PASSWORD=$DB_PASSWORD"
DB_PASSWORD={"db-password":"SuperSecretPassword123!"}
$ kubectl get secret eks-kro-ack-secrets-app-secret-k8s-secret
NAME TYPE DATA AGE
eks-kro-ack-secrets-app-secret-k8s-secret Opaque 1 10s
SM シークレットを JSON 形式で格納した場合、デフォルトでは JSON 文字列がそのまま env var に入ります。特定のキーだけを取り出したい場合は SecretProviderClass の jmesPath を使います。
ハマりポイント
EKS Capabilities は k8s RBAC を手動で追加する
EKS Capabilities はクラスター外の AWS マネージドインフラで動作します。Capability を作成すると Access Entry は自動作成されますが、それだけでは Kubernetes リソースを操作する RBAC が不足します。Kro や ACK が k8s リソースを作成できるよう、ClusterRoleBinding を手動で追加する必要があります。
SM シークレット名は強制削除後すぐには再利用できない
検証をやり直す際、aws secretsmanager delete-secret --force-delete-without-recovery で削除したシークレットと同名で再作成しようとして、以下のエラーになりました。
InvalidRequestException: You can't create this secret because a secret
with this name is already scheduled for deletion.
通常の削除では 7〜30 日の recovery window がありますが、--force-delete-without-recovery を使うと即時削除されます。ただし 公式ドキュメント にあるとおり、実際の削除は非同期のバックグラウンド処理なので、削除直後の同名再作成には少し待ち時間(back-off)が必要でした。
初期値は SM 側で設定する設計にした
当初は ACK の spec.secretString で k8s Secret を参照し、SM シークレットに初期値を入れる構成を検討しました。しかし今回の環境では ACK(EKS Capability)が参照先の k8s Secret を読み取れず、この方式を断念しました(RBAC 周りの追加検証が必要そうです)。
結果的に「SM シークレットを初期値なしで作成 → ユーザーが後から実値を設定」という設計にしましたが、これはシークレット値をマニフェストに書かずに済むため、セキュリティ的にはむしろ好ましい形になりました。
まとめ
今回は Kro + ACK(EKS Capabilities)と ASCP を組み合わせて、シークレット管理を自動化してみました。
-
kind: ManagedSecret→ SM シークレット + SecretProviderClass の同時作成 → 成功。Kro の ResourceGraphDefinition でリソースを宣言するだけで、1 つの独自リソースから両方を作成できました - Pod 起動時の k8s Secret 自動作成と env var 注入 → 成功。CSI ボリュームのマウントをトリガーに ASCP が SM からシークレットを取得し、k8s Secret を作成します
いったん ManagedSecret という抽象を用意してしまえば、アプリ開発者は「シークレット名を 1 つ指定するだけ」でシークレット基盤を使えるようになるので、プラットフォームチームが用意する抽象化としては便利だなと感じました。
それではまた!