リモート開発メインのソフトウェア開発企業のエンジニアブログです

AKS上のKubernetesの秘匿情報をAzure Key Vaultで管理する方法

最近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-secret1my-secret2 (中身はそれぞれ Secret1!!!, Secret2!!!) をアプリケーションのPodの環境変数にそれぞれ SECRET1SECRET2 として定義する。

尚、今回の検証で使う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-000000000000password: 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 ですので今後大きな仕様の変更は有りえます。現時点でも機能としては十分使えそうですが、こまめに公式の動向を追う必要がありそうです。

← 前の投稿

BERTについて勉強したことまとめ (3) 自己教師学習と汎用性について

次の投稿 →

docker-compose.ymlのサービス名にアンダースコアを使うと正しいインターネットホスト名ではなくなる

コメントを残す