Azure Key Vault の値を AKS (Azure Kubernetes Service) 上の Secret として設定する（ Managed Identity を利用）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/ca52f051-03d2-3280-ae61-d5f73e0184a3.png" width=100%>

_イメージとしては上のような感じです。（イメージなので厳密にはちがうかもしれません。。。）_

+ Kubernetes を利用した CIOps や GitOps 等をいざ実践しようと思うと、証明書やアクセスキーやパスワードといった秘匿情報である Secret の管理をどうすればよいのか課題となってくると思います。
  + 通常 Kubernetes の Secret を YAML ファイル等で定義する場合、秘匿情報部分については、暗号化ではなく、Base64 エンコードによる難読化のみのため、コードの管理については、何かしらの対策が必要となります。
  + このような課題を解決する、各種 OSS や ソリューション はいくつかありますが、いずれも一長一短がある状況です。

+ Azure では Key Vault(キー コンテナー) という秘匿情報を管理するサービスがあり、 Key Vault より、秘匿情報を取得し、AKS（ Kubernetes ） 上の Pod へ Secret として 受け渡すことが可能です。
  + 今回は AKS シークレット ストア CSI ドライバーの Azure Key Vault プロバイダーを利用し、Service Principal よりも、セキュア（ Client Secret が不要）な Azure Managed Identity によるアクセス認証のパターンを紹介します。
  + なお、本記事は、主に以下公式ドキュメントの内容をベースにカスタマイズしたものとなります。
 
https://docs.microsoft.com/ja-jp/azure/aks/csi-secrets-store-driver 

https://docs.microsoft.com/ja-jp/azure/aks/csi-secrets-store-identity-access

## AKS (Azure Kubernetes Service) クラスタの準備
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/b19afb18-edad-be10-47eb-32988127b7ff.png" width=60%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/d8a70ce4-6435-9522-c6ad-288f21a00a16.png" width=15%>

+ リソースグループ名： demo_aks_key_vault
+ クラスタ名：　my-aks-key-vault01
+ Kuberenetes version 1.21.9
+ 以下の通り`シークレット ストア CSI ドライバーの有効化`をチェックした状態で、新規に AKS クラスタを作成します。
![ScreenShot 2022-04-06 17.44.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/b537ffe3-7014-1d51-a5ca-4420dddb4ef2.png)

+ 既存クラスタの有効化方法等については以下公式ドキュメントを参照ください。

https://docs.microsoft.com/ja-jp/azure/aks/csi-secrets-store-driver#upgrade-an-existing-aks-cluster-with-azure-key-vault-provider-for-secrets-store-csi-driver-support

AKS クラスタ作成後に kubeconfig のクレデンシャルを取得し

`az aks get-credentials --resource-group [YOUR_RESOUCE_GROUP] --name [YOUR_AKS_CLUSTER]`

以下コマンドで シークレット ストア CSI ドライバー が正常に有効化されていることを確認します。

`kubectl get pods -n kube-system -l 'app in (secrets-store-csi-driver, secrets-store-provider-azure)'`

`aks-secrets-store-csi-driver` 及び `aks-secrets-store-provider-azure`が以下のような感じで Pod が Running となっていることを確認（  以下はNode pool のvmssが1台の例）
```bash:"実行結果（例）"
$az aks get-credentials --resource-group demo_aks_key_vault --name my-aks-key-vault01
Merged "my-aks-key-vault01" as current context in /home/tbuchi888/.kube/config
$kubectl get pods -n kube-system -l 'app in (secrets-store-csi-driver, secrets-store-provider-azure)'
NAME                                     READY   STATUS    RESTARTS   AGE
aks-secrets-store-csi-driver-j25dd       3/3     Running   0          2d
aks-secrets-store-provider-azure-f85q8   1/1     Running   0          2d
```

## Azure Key Vault （キー コンテナー）の準備
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/c0d863bc-db85-d960-593b-d157e919c795.png" width=100%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/d8a70ce4-6435-9522-c6ad-288f21a00a16.png" width=15%>

今回は、Azure Key Vault（`キー コンテナー`）を作成後に以下のような 秘匿情報を
`キー コンテナー` > `シークレット`  > `+ 生成／インポート`より、シークレットとして作成しておきます。

| キーコンテナー名 | シークレット名 | 値 | 
|:-|:-|:-|
| demo-csi-secret| demo-secret01 | This is the value of the secret "demo-secret01" included in the Azure key vault "demo-csi-secret". |
|| demo-secret02 | This is the value of the secret "demo-secret02" included in the Azure key vault "demo-csi-secret". This is "demo-secret02". |

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/35b21ac8-f8f7-28d4-27d7-9b3ecd7506a2.png" width=50%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/ad8d5c0b-576d-af9a-94dc-b0172bc6ace2.png" width=70%>

また、後ほど AKS 側作業で利用するため、`概要`より ディレクトリ ID（テナント ID）を控えておきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/e45e9b34-e167-1a15-fe24-b6a80ee49a3e.png" width=70%>


## Azure Key Vault （キー コンテナー）のアクセスポリシーへ AKS の Managed Identity(マネージド ID) を設定する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/ba7f3c11-2598-b730-5349-b1cbd0a21f02.png" width=100%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/d8a70ce4-6435-9522-c6ad-288f21a00a16.png" width=15%>

Azure Key Vault （キー コンテナー）へのアクセスを許可するために、アクセスポリシーへ`マネージド ID`を割り当てます。
割り当て可能な `マネージド ID` は、システムやユーザー定義など、いくつかありますが、今回はシークレット ストア CSI ドライバー の有効化の際に、自動的に作成され、AKSのノードプール（vmss/仮想マシンスケール セット）へ、`ユーザー割り当て済み`として登録される　`マネージド ID`　`azurekeyvaultsecretsprovider-*`を流用します
その他の（システム割当 や ユーザー割り当て、Podなど ）`マネージド ID`の設定方法については、以下公式ドキュメントを参照してください。

https://docs.microsoft.com/ja-jp/azure/aks/csi-secrets-store-identity-access

まずは、`マネージド ID`を確認します。
AKS 作成時に自動的作成される リソースグループ MC_[AKSリソースグループ名]_[AKSクラスタ名]_[リージョン名]
の中から、以下のような名称の`マネージド ID`が作成されていることを確認します。
`azurekeyvaultsecretsprovider-[AKSクラスタ名]`

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/90945fb8-2e06-e2df-21c4-6692a36c1716.png" width=70%>

次に、同じくMC_からはじまるリソースグループの中から、ノードプール（ vmss /仮想マシンスケール セット）を選択し、
`ID` > `ユーザー割り当て済み` へ、`マネージド ID`が割り当てられていることを確認の上、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/9cf4946a-f1ee-fe4c-5782-70179458c466.png" width=70%>

`マネージド ID`をクリックして詳細を表示します。
この後の作業で利用するため、以下を控えておきます。
+ クライアント ID （＊： AKS側作業で利用）
+ オブジェクト (プリンシパル) ID 

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/d07b56aa-73c2-522c-26fc-e7eda6e53aeb.png" width=70%>

次に Portal より  先ほど作成した Azure Key Vault （キー コンテナー）を表示し、`キー コンテナー` > `アクセス ポリシー`より`+ アクセス ポリシーの追加`をクリックします。

+ テンプレートからの構成
  + プルダウンより`キー、シークレット、証明書の管理`（用途にあったものを選択してください）
+ プリンシパルの選択
  + `選択されていません`をクリックし、右側のペインで、さきほど、確認した`マネージド ID`の`オブジェクト (プリンシパル) ID`を検索欄へ入力
  + 検索結果の`マネージド ID`をクリックして選択したアイテムへ表示されていることを確認後
  + `選択`ボタンを押下して、元（左）のペインのプリンシパルの選択へ`マネージド ID`が反映されていることを確認後
+ 最後に`追加`ボタンをクリックします
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/3ed7a545-0327-61bd-a5f5-4dceb2de17a2.png" width=70%>

+ アクセス ポリシーの画面に戻り、画面上の`保存`をクリックして、AKSへ割り当てられた`マネージド ID`へのアクセスポリシーが追加されたことを確認します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/37dc8045-c259-54fc-991d-17c0279accdb.png" width=70%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/cc05289b-93bc-87d4-6920-e1c296187612.png" width=70%>

これで、Azure Key Vault （キー コンテナー）へ`マネージド ID`を利用してアクセス可能になりました。

:::note warn
UIの手順がやや複雑で、わかりにくいのですが、最後の`保存`まで行わないと、アクセス ポリシーに新規追加されませんので注意してください
:::

## AKS (Azure Kubernetes Service) 上で SecretProviderClass 及び　Pod を作成
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/5c5d22a3-c296-2e2c-290d-e29f6af8dd9d.png" width=100%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/d8a70ce4-6435-9522-c6ad-288f21a00a16.png" width=15%>

つづいて、AKSに戻ります。以下の YAML より Azure Key Vault （キー コンテナー）へアクセスする`SecretProviderClass`を作成します。

さきほど、控えた`マネージド ID`の以下情報を置き換えてください。

+ クライアント ID : [CLIENT-ID-FOR-YOUR-MANAGED-ID]

以下も自身の Key Vault （キー コンテナー）の情報へ置き換えてください。
+ keyvaultName: Key Vault （キー コンテナー）の名前
+ tenantId: Key Vault （キー コンテナー）のディレクトリ ID[YOUR-TENANT-ID]

``` csi-spc-user-msi.yml 
# This is a SecretProviderClass example using user-assigned identity to access your key vault
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: azure-kvname-user-msi
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "true"    # Set to true for using managed identity
    # 今回はシステムではなくユーザー定義のマネージド ID を使うので、こちらへマネージド ID のクライアント ID を設定
    userAssignedIdentityID: "[CLIENT-ID-FOR-YOUR-MANAGED-ID]"     # If empty, then defaults to use the system assigned identity on the VM
    # Key Vault （キー コンテナー）の名前を設定
    keyvaultName: demo-csi-secret # 
    cloudName: ""                   # [OPTIONAL for Azure] if not provided, the Azure environment defaults to AzurePublicCloud
    tenantId: "[YOUR-TENANT-ID]"      # The tenant ID of the key vault
    # Key Vault （キー コンテナー）の中から利用するシークレットを設定
    objects: |
      array:
        - |
          objectType: secret
          # Key Vault （キー コンテナー）の シークレット名
          objectName: demo-secret01
        - |
          objectType: secret
          # Key Vault （キー コンテナー）の シークレット名
          objectName: demo-secret02
  # 今回は　env　で　Kubernetes　Secret　として利用するため、こちらを設定（ファイルをマウントするだけであれば以下不要）
  secretObjects:
    # Pod より参照させる Kubernetes Secret 名
    - secretName: test-secrets
      type: Opaque
      data:
        # Kubernetes Secret のキー名
        - key: k8s-secret-key1　
          # Key Vault （キー コンテナー）の シークレット名
          objectName: demo-secret01
        # Kubernetes Secret のキー名
        - key: k8s-secret-key2　
          # Key Vault （キー コンテナー）の シークレット名
          objectName: demo-secret02
    # Pod より参照させる Kubernetes Secret 名
    - secretName: test-secret1
      type: Opaque
      data:
        # Kubernetes Secret のキー名
        - key: k8s-secret-key1　
          # Key Vault （キー コンテナー）の シークレット名
          objectName: demo-secret01
```

続いて、以下のような `Secret` を環境変数へ設定、ファイルマウントする `Pod` の YAML ファイルを用意します。

:::note info
　SecretProviderClass の仕組みを利用して、Key Vault （キー コンテナー）の情報を取得しているため、env など secret だけを使いたい場合でも、ボリュームマウントは必須（Pod 作成時に、Kubernetes secret も作成され、Pod 削除時に、Kubernetes secret も削除されるようなので）
:::

```sample-pod.yml
# This is a sample pod definition for using SecretProviderClass and user-assigned identity to access your key vault
kind: Pod
apiVersion: v1
metadata:
  name: busybox-secrets-store-env-user-msi
spec:
  containers:
    - name: busybox
      image: k8s.gcr.io/e2e-test-images/busybox:1.29-1
      command:
        - "/bin/sleep"
        - "10000"
      # 環境変数を設定
      env:
      - name: ENV_KEY_VAULT1
        valueFrom:
          secretKeyRef:
            name: test-secret1
            key: k8s-secret-key1
      - name: ENV_KEY_VAULT2
        valueFrom:
          secretKeyRef:
            name: test-secrets
            key: k8s-secret-key2
 # ボリュームとしてマウント　（必須）
      volumeMounts:
      - name: secrets-store01-inline
        mountPath: "/mnt/secrets-store"
        readOnly: true
 # ボリュームとして　SecretProviderClass を設定　（必須）
  volumes:
    - name: secrets-store01-inline
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: "azure-kvname-user-msi"
```

以上で準備が整いましたので
以下 kubectl コマンドにより、確認していきます。

```bash:"kubectl コマンド"
# SecretProviderClass の適用と確認
kubectl apply -f csi-spc-user-msi.yml 
kubectl get secretproviderclass
# secretを確認（この時点ではまだ作成されていない＝Pod作成時に作成される）
kubectl get secret
# Pod の適用と確認
kubectl apply -f sample-pod.yml 
kubectl get pod
# secretを確認（Podが作成されたので）
kubectl get secret
# pod の環境変数へKey Vault （キー コンテナー）の シークレットの値が反映されていることを確認
kubectl exec busybox-secrets-store-env-user-msi -- env
# pod へKey Vault （キー コンテナー）の シークレットがマウントされていることを確認
kubectl exec busybox-secrets-store-env-user-msi -- ls -al /mnt/secrets-store/
kubectl exec busybox-secrets-store-env-user-msi -- cat /mnt/secrets-store/demo-secret01
kubectl exec busybox-secrets-store-env-user-msi -- cat /mnt/secrets-store/demo-secret02
```

以下実行結果の例

```bash:"実行結果の例"
$# SecretProviderClass の適用と確認
$kubectl apply -f csi-spc-user-msi.yml 
secretproviderclass.secrets-store.csi.x-k8s.io/azure-kvname-user-msi created
$kubectl get secretproviderclass
NAME                    AGE
azure-kvname-user-msi   1s
$# secretを確認（この時点ではまだ作成されていない＝Pod作成時に作成される）
$kubectl get secret
NAME                  TYPE                                  DATA   AGE
default-token-75xb6   kubernetes.io/service-account-token   3      2d
$# Pod の適用と確認
$kubectl apply -f sample-pod.yml 
pod/busybox-secrets-store-env-user-msi created
$kubectl get pod
NAME                                 READY   STATUS              RESTARTS   AGE
busybox-secrets-store-env-user-msi   0/1     ContainerCreating   0          0s
$# secretを確認（Podが作成されたので）
$kubectl get secret
NAME                  TYPE                                  DATA   AGE
default-token-75xb6   kubernetes.io/service-account-token   3      2d
test-secret1          Opaque                                1      1s
test-secrets          Opaque                                2      1s
$# pod の環境変数へKey Vault （キー コンテナー）の シークレットの値が反映されていることを確認
$kubectl exec busybox-secrets-store-env-user-msi -- env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=busybox-secrets-store-env-user-msi
ENV_KEY_VAULT1=This is the value of the secret "demo-secret01" included in the Azure key vault "demo-csi-secret".
ENV_KEY_VAULT2=This is the value of the secret "demo-secret02" included in the Azure key vault "demo-csi-secret". This is "demo-secret02".
KUBERNETES_PORT_443_TCP=tcp://10.0.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.0.0.1
KUBERNETES_SERVICE_HOST=10.0.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.0.0.1:443
HOME=/root
$# pod へKey Vault （キー コンテナー）の シークレットがマウントされていることを確認
$kubectl exec busybox-secrets-store-env-user-msi -- ls -al /mnt/secrets-store/
total 4
drwxrwxrwt    3 root     root           120 Apr  8 08:55 .
drwxr-xr-x    3 root     root          4096 Apr  8 08:55 ..
drwxr-xr-x    2 root     root            80 Apr  8 08:55 ..2022_04_08_08_55_11.2856338384
lrwxrwxrwx    1 root     root            32 Apr  8 08:55 ..data -> ..2022_04_08_08_55_11.2856338384
lrwxrwxrwx    1 root     root            20 Apr  8 08:55 demo-secret01 -> ..data/demo-secret01
lrwxrwxrwx    1 root     root            20 Apr  8 08:55 demo-secret02 -> ..data/demo-secret02
$kubectl exec busybox-secrets-store-env-user-msi -- cat /mnt/secrets-store/demo-secret01
This is the value of the secret "demo-secret01" included in the Azure key vault "demo-csi-secret".$kubectl exec busybox-secrets-store-env-user-msi -- cat /mnt/secrets-store/demo-secret02
This is the value of the secret "demo-secret02" included in the Azure key vault "demo-csi-secret". This is "demo-secret02".$
```

## Tips
+ `Status Code = '400'. Response body: {"error":"invalid_request","error_description":"Identity not found"`エラーで、以下のように`Pod`の作成に失敗する場合は、以下を確認してください。
```bash:"エラーの例"
$kubectl get pod
NAME                                 READY   STATUS              RESTARTS   AGE
busybox-secrets-store-env-user-msi   0/1     ContainerCreating   0          83m
$kubectl describe pod busybox-secrets-store-env-user-msi
Name:         busybox-secrets-store-env-user-msi
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
中略
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Events:
  Type     Reason       Age                   From     Message
  ----     ------       ----                  ----     -------
  Warning  FailedMount  22m (x6 over 59m)     kubelet  Unable to attach or mount volumes: unmounted volumes=[secrets-store01-inline], unattached volumes=[kube-api-access-zkp5c secrets-store01-inline]: timed out waiting for the condition
  Warning  FailedMount  8m18s (x45 over 83m)  kubelet  MountVolume.SetUp failed for volume "secrets-store01-inline" : rpc error: code = Unknown desc = failed to mount secrets store objects for pod default/busybox-secrets-store-env-user-msi, err: rpc error: code = Unknown desc = failed to mount objects, error: failed to get objectType:secret, objectName:demo-secret01, objectVersion:: azure.BearerAuthorizer#WithAuthorization: Failed to refresh the Token for request to https://demo-csi-secret.vault.azure.net/secrets/demo-secret01/?api-version=2016-10-01: StatusCode=400 -- Original Error: adal: Refresh request failed. Status Code = '400'. Response body: {"error":"invalid_request","error_description":"Identity not found"} Endpoint http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&client_id=6c6a0e2c-0c7d-4430-baab-49129f45bc4d&resource=https%!!(MISSING)A(MISSING)%!!(MISSING)F(MISSING)%!!(MISSING)F(MISSING)vault.azure.net
  Warning  FailedMount  2m25s (x29 over 81m)  kubelet  Unable to attach or mount volumes: unmounted volumes=[secrets-store01-inline], unattached volumes=[secrets-store01-inline kube-api-access-zkp5c]: timed out waiting for the condition
~/azurework/csidemo $ 
```

+ アクセスポリシーの設定があっているか？
+ SecretProviderClass の設定があっているか？（とくに以下の設定）
  + userAssignedIdentityID:
  + tenantId:
  + objects:
+ 操作対象の AKS クラスタへちゃんと接続しているか？
  + クラスタがいくつかあったり、クラスタ作成から時間が空いてしまう、`kubectl`実行環境を変えたなどで、やりがちです。。。（今回の記事を書くときにハマりました）
    +  操作対象の AKS クラスタへコンテキストが設定されているか`kubectl config current-context`コマンド確認しましょう！

## さいごに
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117986/1fe8852c-314c-9473-120e-51b53df4f3ee.png" width=100%>

+ 事前準備は必要となりますが、Kubernetes のコードには、一切秘匿情報を利用せずに AKS シークレット ストア CSI ドライバーの Azure Key Vault プロバイダーの仕組みを利用し、さらにセキュアな Azure Managed Identity アクセス認証する例を確認できました。
+ 公式ドキュメントについて、初見では分かりづらい部分も多々ありますので、こちらの記事を参考にいただけると幸いです。

:::note info
本記事の実行結果等は執筆時点（2022年4月8日）での内容となります
:::

以上です。