SPIFFE CSI Driverを試してみる

## spiffe-csiについて

https://github.com/spiffe/spiffe-csi

2021/12に最初のバージョンである`0.1.0`がリリースされたばかりの新しいものであり、利用には注意が必要です。

spiffe-csiはSPIFFE Workload APIのためのUNIX Domain Socketを含むディレクトリをEphemeralな
VolumeとしてPodにマウントすることができるCSI Driverです。

例えばSPIFFEの実装であるSPIREをK8s上で動かしたい場合、Workload APIはDaemonSetで各Nodeに配置されたSPIRE AgentによってUNIX Domain Socketで提供されます。これまで、各Workloadが動作するPodではSPIRE Agentが提供するWorkload APIをhostPathでマウントして利用する必要がありました。

しかし、現在ではセキュリティの観点からhostPathマウントの利用はポリシーで制限されている環境が多く利用にはハードルがありました。spiffe-csiを利用することで、hostPathを利用する必要なくPodからWorkload APIを利用することが可能になります。

※注: 一方でCSI DriverをPodとして動かす場合にはKubeletとの対話のためにhostPathを利用しなければなりません。

## イメージ

![spiffe-csi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15164/16496b38-da51-137d-59d7-0f38d7ab082b.png)

spiffe-csi DriverはSPIRE Agentのサイドカーとしてデプロイされます。
spiffe-csi DriverとSPIRE AgentはEphemeral VolumeなemptyDirによってWorkload APIを共有して おり、DriverはWorkload APIのディレトリをKubeletからリクエストされたTargetPathにバインドマウントすることで、他のPodから利用できるようにします。

詳細は[mount --bind](https://man7.org/linux/man-pages/man8/mount.8.html)を参照してください。

## 動作確認

リポジトリで提供されている[Example](https://github.com/spiffe/spiffe-csi/blob/main/example/README.md)に従ってkindでk8sクラスタを作成しSPIREとDriver,Workloadなどをデプロイします。

CSIDriverのマニフェスト

```yaml
apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
  name: "csi.spiffe.io"
spec:
  # Only ephemeral, inline volumes are supported. There is no need for a
  # controller to provision and attach volumes.
  attachRequired: false

  # Request the pod information which the CSI driver uses to verify that an
  # ephemeral mount was requested.
  podInfoOnMount: true

  # Don't change ownership on the contents of the mount since the Workload API
  # Unix Domain Socket is typically open to all (i.e. 0777).
  fsGroupPolicy: None

  # Declare support for ephemeral volumes only.
  volumeLifecycleModes:
    - Ephemeral

```

SPIRE AgentとCSI Driverのマニフェスト

```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: spire-agent
  namespace: spire
  labels:
    app: spire-agent
spec:
  selector:
    matchLabels:
      app: spire-agent
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      namespace: spire
      labels:
        app: spire-agent
    spec:
      hostPID: true
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      serviceAccountName: spire-agent
      containers:
        - name: spire-agent
          image: ghcr.io/spiffe/spire-agent:1.1.1
          imagePullPolicy: IfNotPresent
          args: ["-config", "/run/spire/config/agent.conf"]
          volumeMounts:
            - name: spire-config
              mountPath: /run/spire/config
              readOnly: true
            - name: spire-bundle
              mountPath: /run/spire/bundle
              readOnly: true
            - name: spire-token
              mountPath: /var/run/secrets/tokens
            - name: spire-agent-socket-dir
              mountPath: /run/spire/sockets
        # This is the container which runs the SPIFFE CSI driver.
        - name: spiffe-csi-driver
          image: ghcr.io/spiffe/spiffe-csi-driver:nightly
          imagePullPolicy: IfNotPresent
          args: [
            "-workload-api-socket-dir", "/spire-agent-socket",
            "-csi-socket-path", "/spiffe-csi/csi.sock",
          ]
          env:
            # The CSI driver needs a unique node ID. The node name can be
            # used for this purpose.
            - name: MY_NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          volumeMounts:
            # The volume containing the SPIRE agent socket. The SPIFFE CSI
            # driver will mount this directory into containers.
            - mountPath: /spire-agent-socket
              name: spire-agent-socket-dir
              readOnly: true
            # The volume that will contain the CSI driver socket shared
            # with the kubelet and the driver registrar.
            - mountPath: /spiffe-csi
              name: spiffe-csi-socket-dir
            # The volume containing mount points for containers.
            - mountPath: /var/lib/kubelet/pods
              mountPropagation: Bidirectional
              name: mountpoint-dir
          securityContext:
            privileged: true
        # This container runs the CSI Node Driver Registrar which takes care
        # of all the little details required to register a CSI driver with
        # the kubelet.
        - name: node-driver-registrar
          image: quay.io/k8scsi/csi-node-driver-registrar:v2.0.1
          imagePullPolicy: IfNotPresent
          args: [
            "-csi-address", "/spiffe-csi/csi.sock",
            "-kubelet-registration-path", "/var/lib/kubelet/plugins/csi.spiffe.io/csi.sock",
          ]
          volumeMounts:
            # The registrar needs access to the SPIFFE CSI driver socket
            - mountPath: /spiffe-csi
              name: spiffe-csi-socket-dir
            # The registrar needs access to the Kubelet plugin registration
            # directory
            - name: kubelet-plugin-registration-dir
              mountPath: /registration
      volumes:
        - name: spire-config
          configMap:
            name: spire-agent
        - name: spire-bundle
          configMap:
            name: spire-bundle
        - name: spire-token
          projected:
           sources:
           - serviceAccountToken:
              path: spire-agent
              expirationSeconds: 7200
              audience: spire-server
        # This volume is used to share the workload api socket between the
        # CSI driver and SPIRE agent
        - name: spire-agent-socket-dir
          emptyDir: {}
        # This volume is where the socket for kubelet->driver communication lives
        - name: spiffe-csi-socket-dir
          hostPath:
            path: /var/lib/kubelet/plugins/csi.spiffe.io
            type: DirectoryOrCreate
        # This volume is where the SPIFFE CSI driver mounts volumes
        - name: mountpoint-dir
          hostPath:
            path: /var/lib/kubelet/pods
            type: Directory
        # This volume is where the node-driver-registrar registers the plugin
        # with kubelet
        - name: kubelet-plugin-registration-dir
          hostPath:
            path: /var/lib/kubelet/plugins_registry
            type: Directory
```

Workloadのマニフェスト

```yaml
piVersion: v1
kind: Pod
metadata:
  name: example-workload
spec:
  containers:
  - name: example-workload
    image: spiffe-csi-driver-example-workload:example
    imagePullPolicy: Never
    volumeMounts:
    - name: spiffe-workload-api
      mountPath: /spiffe-workload-api
      readOnly: true
    env:
    - name: SPIFFE_ENDPOINT_SOCKET
      value: unix:///spiffe-workload-api/spire-agent.sock
  volumes:
  - name: spiffe-workload-api
    csi:
      driver: "csi.spiffe.io"
```


それでは、READMEに従ってデプロイを実施していきます。

```
$ kind create cluster
```

ここでクラスタ名を指定した場合は以降の手順が一部失敗してしまうと思います。
`./build-and-load-workload-image.sh` などで`kind load`コマンドを呼び出している箇所がありますので、`--name`でクラスタ名を指定するように修正が必要です。

```
$ git clone git@github.com:spiffe/spiffe-csi.git
$ cd spiffe-csi/example

$ ./build-and-load-workload-image.sh
$ ./deploy-spire-and-csi-driver.sh
$ ./register-workload.sh
$ kubectl apply -f config/workload.yaml
```


デプロイが完了するとspire namespaceにServerとAgentがデプロイされているはずです。

```
$ kubectl get pods -n spire
NAME                            READY   STATUS    RESTARTS   AGE
spire-agent-9476q               3/3     Running   0          1h29m
spire-server-6f6d88f75c-lsfsc   1/1     Running   0          1h30m
```

また、default namespaceではWorkloadのPodがデプロイされており、VolumeMountで指定したパスにてWorkload APIのUDSが参照できるようになっており、SVIDの取得・更新が行われていることが確認できます。

```
$ kubectl get pods
NAME               READY   STATUS    RESTARTS   AGE
example-workload   1/1     Running   0          1h33m

$ kubectl exec example-workload -- ls -li /spiffe-workload-api
total 0
684260 srwxrwxrwx    1 root     root             0 Dec 18 08:58 spire-agent.sock

$ kubectl logs pod/example-workload
2021/12/18 09:01:06 Watching...
2021/12/18 09:01:07 Update:
2021/12/18 09:01:07   SVIDs:
2021/12/18 09:01:07     spiffe://example.org/workload
2021/12/18 09:01:07   Bundles:
2021/12/18 09:01:07     example.org (1 authorities)
2021/12/18 09:30:48 Update:
2021/12/18 09:30:48   SVIDs:
2021/12/18 09:30:48     spiffe://example.org/workload
2021/12/18 09:30:48   Bundles:
2021/12/18 09:30:48     example.org (1 authorities)
```

ここからは実際にどのような仕組みになっているのか見てみたいと思います。
Agent Podのuidを取得し、Node上の状況について確認します。

```
$ kubectl get pods -n spire spire-agent-9476q -o json | jq .metadata.uid
"4a686f16-5505-45fd-b3a9-ae175641ce17"

$ docker exec $ID ls -li /var/lib/kubelet/pods/4a686f16-5505-45fd-b3a9-ae175641ce17/volumes/kubernetes.io~empty-dir/spire-agent-socket-dir/
total 0
684260 srwxrwxrwx 1 root root 0 Dec 18 08:58 spire-agent.sock
```
`/var/lib/kublet/pods`配下に `kubernetes.io~empty-dir`が作成されており、`spire-agent-socket-dir` という名前(manifestで指定した名前)のディレトリ配下にUDSのファイルが作成されています。

Workload Podに対しても同様にNodeの状況を確認してみます。

```
$ kubectl get pods example-workload -o json | jq .metadata.uid
"db7db9ae-d651-41dc-820f-1c5e5e3d5e28"

$ docker exec 3457f65502d7 ls -li /var/lib/kubelet/pods/db7db9ae-d651-41dc-820f-1c5e5e3d5e28/volumes/kubernetes.io~csi/spiffe-workload-api/mount
total 0
684260 srwxrwxrwx 1 root root 0 Dec 18 08:58 spire-agent.sock
```

`/var/lib/kubelet/pods/$uid/volumes`配下に`kubernetes.io~csi`が作成されており、その配下にはマニフェストで指定した名前のディレトリ`spiffe-workload-api`が作成されており、同じinode番号のUDSが参照できました。

このディレトリは、Podがデプロイされた際にKubeletからDriverの`NodePublishVolume` APIが呼び出されますが、その際に`targetPath`として指定されています。DriverがそのパスにWorkload APIのディレトリパスをバインドマウントすることでディレトリが作成されます

WorkloadのPodにはこのディレトリがVolumeとして利用可能になっており、WorkloadはVolumeをマウントしたパスを参照してWorklaod APIを利用します。


## 課題

SPIRE AgentとCSI DriverはWorkload APIをEphemeral Volumeで共有しているため、Podが何らかの理由で再作成されてしまった場合には、CSIを使って利用しているWorkloadから参照できなくなってしまう問題があるように思いました。

現時点では、Workload側のヘルスチェック等によりWorkload APIが利用できない場合には再作成する等の対応が必要になりそうです。
自前でK8sを構築しているなど、環境によってはSPIRE agentとCSI driverをPodではなくNodeにバインドしてsystemdなどで動作させることもできそうです。

また、この問題についてはすでにissueが作成されていました。
https://github.com/spiffe/spiffe-csi/issues/16

## まとめ

CSI DriverのPodが再作成されてしまった場合の課題はありますが、バインドマウントすることでhostPathの利用を回避するという手段について勉強になりました。今後はkubelet側に機能が追加されるなどで課題が解消できることを期待して引き続きアップデートを追いかけていきたいと思います。