最近AKSでKubernetesを管理しているのですが、SecretオブジェクトをAzure Key Vaultで管理したくなり、調べた所、Azure Key Vault Provider for Secrets Store CSI Driverにたどり着き、実際にこれを使って実現できたので、メモを残しておきます。
Azure Key Vault Provider for Secrets Store CSI Driverとは
Kubernetes SIGs管理下のSecrets Store CSI Driverプロジェクトのプロバイダ実装で、Key VaultとSecrets Store CSI Driverを連携させる事ができます。メンテはAzureチームが行っています: https://github.com/Azure/secrets-store-csi-driver-provider-azure
Secrets Store CSI Driverは、Kubernetesで秘匿情報を扱うのに特化したCSI Driver統合で、Azure Key Vault等の外部の秘匿情報プロバイダと連携して、KubernetesのPodにこれらの情報をファイルシステムとしてマウントする機能を持っています。CSI Driver自体は各種コンテナオーケストレータ用に抽象化された共通ストレージインターフェースとなっており、Kubernetes特有の機能ではありませんが、Kubernetesも標準でこれを実装しています。 (詳しくはこちら)
登場人物が多くて少々混乱しますが、図にすると以下のような関係になっています:
さて、今回はこのプロバイダを使って、AzureのService Principal権限でAzure Key Vaultに格納されているSecretsをKubernetesのPodから読み込む方法の手順を記します。
今回やる事
Key Vaultに格納されている my-secret1
, my-secret2
(中身はそれぞれ Secret1!!!
, Secret2!!!
) をアプリケーションのPodの環境変数にそれぞれ SECRET1
, SECRET2
として定義する。
尚、今回の検証で使うKubernetesのバージョンは1.17.9となります。また、今回オブジェクトは基本的にNamespace secrets-test
上にデプロイしますので、事前に作っておきます:
kubectl create namespace secrets-test
Service Principalの準備
Key Vaultへのアクセスを行う権限を持つService Principalを用意します:
az ad sp create-for-rbac --skip-assignment
生成されたSPの app-id
と password
は後で使いますので控えておいて下さい。
ここではそれぞれ app-id: 00000000-0000-0000-0000-000000000000
, password: password
とします。
次に、このSPを使ってKey VaultからSecretsを読み出せる様権限を与えます。先程のapp-idとpasswordを使って以下のコマンドを実行します:
az role assignment create \
--role Reader \
--assignee 00000000-0000-0000-0000-000000000000 \
--scope /subscriptions/aaaa1234-12ab-34cd-56ef-1234abcd4567/resourceGroups/my-group/providers/Microsoft.KeyVault/vaults/my-key-vault
az keyvault set-policy \
--name my-key-vault \
--secret-permissions get \
--spn 00000000-0000-0000-0000-000000000000
※ az role assignment create
の --scope
には事前に取得したKey VaultのIDを入れておく必要があります。以下のコマンドで調べる事ができます:
$ az keyvault show -n my-keyvault --query id -o tsv
/subscriptions/aaaa1234-12ab-34cd-56ef-1234abcd4567/resourceGroups/my-group/providers/Microsoft.KeyVault/vaults/my-key-vault
最後に、Secrets Store CSI DriverがKey Vaultにアクセスできるように、先程のSPのapp-idとpasswordを格納したSecretオブジェクトをKubernetesに作ります:
kubectl create secret generic secrets-store-credentials \
--from-literal clientid=00000000-0000-0000-0000-000000000000 \
--from-literal clientsecret=password \
--namespace secrets-test
※この情報は秘匿情報なのでマニフェストには保存しません。また、Namespaceはアプリと同じにする必要があるのでご注意下さい。
Azure Key Vault Provider for Secrets Store CSI Driverのインストール
Azure Key Vault Provider (とSecrets Store CSI Driver) を事前にKubernetes上にインストールする必要があります。
公式ドキュメントにはHelmを使ったインストール方法が紹介されており、以下の手順でSecrets Store CSI DriverとAzure Key Vault Providerを同時にインストールできます。尚、アプリケーションと同じNamespaceにリリースする必要は無いので、今回は default
にデプロイします:
helm repo add csi-secrets-store-provider-azure https://raw.githubusercontent.com/Azure/secrets-store-csi-driver-provider-azure/master/charts
helm install csi-secrets-store-provider-azure/csi-secrets-store-provider-azure --generate-name --namespace default
※Helmのインストールはこちら
インストールが完了するとCSI DriverのDaemonSetが作られます。つまり、全てのノードに共通のPodが作られる事になります。Podの存在は次のようにして行います:
$ kubectl get pods -l app=secrets-store-csi-driver --namespace default
NAME READY STATUS RESTARTS AGE
csi-secrets-store-provider-azure-1599821990-secrets-store-lwpkc 3/3 Running 0 44h
$ kubectl get pods -l app=csi-secrets-store-provider-azure --namespace default
NAME READY STATUS RESTARTS AGE
csi-secrets-store-provider-azure-1599821990-4vs8q 1/1 Running 0 44h
これでインストール完了です。
SecretProviderClassを作成する
SecretProviderClassは、Secrets Store CSI Driverが定義する CustomResourceDefinition
で、このオブジェクトもクラスタにデプロイする必要があります。
マニフェストは以下のような形になります:
# spc.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: my-spc
spec:
provider: azure
parameters:
keyvaultName: my-key-vault
tenantId: aaaaaaaa-12ab-34cd-56ef-bbbbbbbbbbbb # Key VaultのTenant ID。Azure Portal上ではDirectory IDと呼ばれている
objects: |
array:
- |
objectType: secret
objectName: my-secret1 # Key Vaultに格納されているsecretの名前
objectAlias: secret1 # Secrets storageにマウントする際のファイル名。省略可能
- |
objectType: secret
objectName: my-secret2
objectAlias: secret2
secretObjects:
- data:
- key: SECRET1 # Kubernetes SecretsのKey名。今回は環境変数として使うので変数名にしている
objectName: secret1 # ↑で指定したsecretのobjectNameまたはobjectAlias
- key: SECRET2
objectName: secret2
secretName: my-secrets # 同期させたいKubernetesのSecret名
type: Opaque
これを適用します。尚、SecretProviderClassはアプリケーションと同じNamespaceに入れる必要があるのでご注意下さい:
kubectl apply -f spc.yaml --namespace secrets-test
ちなみに、Key Vaultを取得する際の設定はもっと細かく指定する事ができます。 (例えばバージョン指定等) 詳しくは公式ドキュメントをご覧下さい。
アプリケーションのPodで使ってみる
それでは先程作ったSecretProviderClassを使ってKey VaultのSecretをPodの環境変数に埋め込んでみましょう。マニフェストは次のような感じです:
# pod.yaml
kind: Pod
apiVersion: v1
metadata:
name: my-test-pod-with-secrets
spec:
containers:
- image: alpine
name: alpine
command: ['tail']
args: ['-f', '/dev/null']
envFrom:
- secretRef:
name: my-secrets
# 今回は環境変数に埋め込みたいので、ファイルシステムへのマウント機能は不要だが、Kubernetes Secretsへの同期は
# サブ機能であり、ボリュームマウント自体がSecrets Store for CSI Driverの本分なのでこの設定は必須である
volumeMounts:
- name: secrets-store-inline
mountPath: /mnt/secrets-store
readOnly: true
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: my-spc # さっき作ったSecretProviderClass名
nodePublishSecretRef:
name: secrets-store-credentials # さっき作ったKubernetes Secret名
デプロイします:
kubectl apply -f pod.yaml --namespace secrets-test
Podの存在を確認します:
$ kubectl get pods --namespace secrets-test
NAME READY STATUS RESTARTS AGE
my-test-pod-with-secrets 1/1 Running 0 12m
上記の通りデプロイが確認できたら、 exec
でコンテナの中に入ってみましょう:
kubectl exec my-test-pod-with-secrets -it --namespace secrets-test -- sh
入ったら、 env
で中身を見てみます:
# env
(略)
SECRET1=Secret1!!!
SECRET2=Secret2!!!
無事環境変数にセットされました。本来の機能であるボリュームの方も覗いて見ましょう:
# ls -l /mnt/secrets-store/
total 8
-rw-r--r-- 1 root root 10 Sep 14 03:17 secret1
-rw-r--r-- 1 root root 10 Sep 14 03:17 secret2
# cat /mnt/secrets-store/secret1
Secret1!!!
# cat /mnt/secrets-store/secret2
Secret2!!!
しっかりマウントされていますね。無事に実験は成功です。
ちなみに、このPodが削除されると同期先のSecret (今回は my-secrets
) も削除されます。
最後に、検証が終わったので後片付けをしておきます:
kubectl delete namespace secrets-test
主観的な注意点とか感想
1. エラーがそこそこ分かりづらい
設定に誤りがあるとPodが正しく開始されないのですが、登場人物が多い上に記述ミスや、Namespaceの配置ミス等があって一発で動かなかったりします。また、Azure Key VaultのSecretが無い時等以外では、エラーの内容を把握しづらいのも難点です。 (自分が管理するPodじゃなくてCSI Driver側のログを見たりしないといけない)
動かない場合はログを確認したり、設定に誤りが無いか今一度確認してみて下さい。
2. Key Vaultへの更新はKubernetesのSecretには自動適用されない
Kubernetes Secrets側に同期する場合、Key Vaultや、SecretProviderClassオブジェクトへの変更は自動的にSecretには適用されません。
また、このSecret自体のライフサイクルがCSI Driver、すなわちPodのボリュームと同様となっていますので、Podを削除する事によってSecretが削除されるまでは内容が変わる事も無いので、Key VaultのSecretの値を更新したい時等は以下の手順を踏む必要があります:
- Key VaultでSecretの内容を更新する (追加、更新等)
- 必要に応じて (値を追加した時等) SecretProviderClassオブジェクトを更新する
- Podを削除し、再度デプロイする (実際の運用ではDeploymentやReplicaSet経由で行われる)
3. まだ開発中である事を認識する必要がある
KubernetesとしてはCSI Driverは既にStableとなっていますが、Secrets Store for CSI Driver側のAPIのバージョンが v1alpha1
ですので今後大きな仕様の変更は有りえます。現時点でも機能としては十分使えそうですが、こまめに公式の動向を追う必要がありそうです。