KubernetesのfsGroupを徹底調査 — 仕組みと注意点

# fsGroupとは？

KubernetesでPodやDeployment等のワークロード系のリソースを作成する場合に、マニフェストの `securityContext` で `fsGroup` というパラメータを指定できます。

fsGroupは、**Podが使用するボリュームにおいて、ファイルやディレクトリのグループID (GID)を指定する** パラメータです。

## 簡単な例

マニフェスト内だと、例えば以下のように記載します。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: fsgroup-pod
spec:
  securityContext:
    fsGroup: 2000
  containers:
  - name: fsgroup-pod
    image: busybox
    command: ["tail", "-f", "/dev/null"]
    volumeMounts:
    - name: shared
      mountPath: /shared
  volumes:
  - name: shared
    emptyDir: {}
```

このマニフェストで、``/shared`` というディレクトリがボリュームのマウントポイントになっていますが、``kubectl apply``してPodを作成すると、以下のように ``/shared`` のGIDが ``fsGroup`` で指定した2000になっているのがわかります。

```
$ kubectl exec -n default pods/fsgroup-pod -- ls -ld /shared
drwxrwsrwx    2 root     2000          4096 Nov 17 22:06 /shared
```

## 活用できるシーン

``fsGroup`` のパラメータは、例えば次のようなケースで役立ちます。

* Pod内の複数のコンテナが同じボリュームを共有し、それらのコンテナが異なるユーザーとして動作している場合
* コンテナ内のアプリケーションが、特定のGIDでのアクセス権を前提として動作する場合
* セキュリティポリシーにより、特定のID以外でアクセスを許可しない要件がある場合

## なぜfsGroupを使うのか？

ファイルやディレクトリのGIDの変更は、``initContainer``等を使ってコンテナ起動時に``chown``コマンドで行うこともできますが、管理が複雑になり設定ミスを起こしてしまう危険性が増えます。例えば、マウントするボリュームが追加されたりマウントポイントが変更になったりした場合に、``chown``コマンドで指定する対象もその都度変更する必要が出てきます。
一方で``fsGroup``を使うと、マウントするほぼ全てのボリュームに指定したGIDを自動的に付与するので、個別に権限を管理する必要がありません。

## 余談：UIDを指定するパラメータはないのか？

``fsGroup`` でボリュームのGIDを指定できるパラメータがあるのならば、同様にユーザID（UID）を指定できるパラメータがあっても良さそうです。ところが現状は存在しません。
理由については明確に特定できていませんが、``fsGroup`` を使う目的の一つが「複数コンテナ間で、ユーザーが異なる場合でもボリュームを共有できるようにすること」なので、 ``fsGroup`` でGIDのみが指定できれば十分だから、という理由が考えられます。

# 利用上の注意点

``fsGroup`` を使う際には、以下のような点に注意する必要があります。

* (1) fsGroup の設定が有効にならない場合がある
* (2) fsGroup の設定により、Podの起動が遅くなる場合がある

各注意点について解説します。

## (1) fsGroup の設定が有効にならない場合がある

すべてのボリュームで fsGroup が有効になるわけではなく、利用するボリュームの種類によって挙動が異なります。Podのマニフェストでは、``spec.volumes`` 配下でボリュームタイプを指定しますが、その内容に応じて以下のような動作に分かれます。

* 有効
   * emptyDir
   * configMap
   * secret
* 状況による（後述）
   * persistentVolumeClaim
* 無効
   * hostPath

### ボリュームに persistentVolumeClaim を指定した場合の挙動

``persistentVolumeClaim``でボリュームを指定する場合は、PVCがどのように作られたかで ``fsGroup`` が反映されるかどうかが変わります。一言での説明が難しいですが、概ね以下のように理解しておくといいです。

#### （ケース１） CSIドライバーを経由してPVCを作る場合、多くのケースで ``fsGroup`` は有効

<details><summary>（←クリック）例えばこちらのようにPVCが作られるケースです。</summary>

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc-from-storageclass
  namespace: default
spec:
  storageClassName: nfs-csi
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
```
</details>

CSIドライバーを介して「動的プロビジョニング」によりPVが作られて、``fsGroup`` の設定が有効になる例です。

#### （ケース２） 自分で作ったPVを参照してPVCを作る場合、多くのケースで ``fsGroup`` は無効

<details><summary>（←クリック）例えばこちらのようにPVCが作られるケースです。</summary>

```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  nfs:
    path: /srv/nfs_share
    server: 10.0.0.5
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc-from-pv
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
  volumeName: nfs-pv
```
</details>

マウントするボリュームのパスをPVで明示的に指定して、``fsGroup`` の設定が無効になる例です。

:::note warn
実際は、上の２つケースで ``fsGroup`` の有効／無効がきっぱりと分かれるわけではありません。PVを自分で作る場合でもCSIドライバーを使うと ``fsGroup`` を有効に出来たり、逆に動的プロビジョニングを行う場合でもCSIドライバーの設定等によって ``fsGroup`` を無効に出来たりします。
:::

### 検証準備

では、上に紹介したケースで実際にボリュームをマウントするPodを作って確かめてみましょう。
以下のマニフェストを用意します。

```yaml:pod-with-fsgroup.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-fsgroup
  namespace: default
spec:
  securityContext:
    fsGroup: 2000
  containers:
  - name: mnt-nfs-pod
    image: busybox
    command: ["tail", "-f", "/dev/null"]
    volumeMounts:
    # fsGroup有効
    - name: emptydir
      mountPath: /mnt/emptydir
    - name: config-volume
      mountPath: /etc/configmap
    - name: secret-volume
      mountPath: /etc/secret
    # 状況による
    - name: nfs-volume-from-storageclass
      mountPath: /mnt/nfs-volume-from-storageclass
    - name: nfs-volume-from-pv
      mountPath: /mnt/nfs-volume-from-pv
    # fsGroup無効
    - name: hostpath
      mountPath: /mnt/hostpath
  volumes:
  # fsGroup有効
  - name: emptydir
    emptyDir: {}
  - name: config-volume
    configMap:
      name: my-configmap
  - name: secret-volume
    secret:
      secretName: my-secret
  # 状況による
  - name: nfs-volume-from-storageclass
    persistentVolumeClaim:
      claimName: nfs-pvc-from-storageclass
  - name: nfs-volume-from-pv
    persistentVolumeClaim:
      claimName: nfs-pvc-from-pv
  # fsGroup無効
  - name: hostpath 
    hostPath:
      path: /hostpath
      type: DirectoryOrCreate
```

マニフェストで参照しているConfigMap、Secret、PVCは既に以下のように作られている状態です。

```
$ kubectl get cm -n default | grep -e ^NAME -e ^my-configmap
NAME               DATA   AGE
my-configmap       1      4d13h

$ kubectl get secret -n default
NAME        TYPE     DATA   AGE
my-secret   Opaque   2      4d13h

$ kubectl get pvc -n default
NAME                        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
nfs-pvc-from-pv             Bound    nfs-pv                                     1Gi        RWX                           <unset>                 6s
nfs-pvc-from-storageclass   Bound    pvc-353f4ce4-fbd3-48a0-9ac0-a7b2f1a74e76   1Gi        RWX            nfs-csi        <unset>                 2m42s
```

### 検証

Podを作成してみます。

```
kubectl apply -f pod-with-fsgroup.yaml
```

マウントポイントのGIDを確認すると、``hostPath``、およびPV経由で作られた``persistentVolumeClaim``のディレクトリに関しては、``fsGroup`` の設定が反映されていない（つまり2000になっていない）のがわかります。

```
$ kubectl exec -it pod-with-fsgroup -- ls -l /mnt/
total 16
drwxrwsrwx    2 root     2000          4096 Nov 21 20:54 emptydir
drwxr-xr-x    2 root     root          4096 Nov 21 20:54 hostpath
drwxr-xr-x    2 root     root          4096 Nov 21 20:21 nfs-volume-from-pv
drwxrwsr-x    2 root     2000          4096 Nov 21 20:38 nfs-volume-from-storageclass
```

また、ConfigMapとSecretからマウントされたディレクトリとファイルは、GIDが ``fsGroup`` で指定した2000になっています。

```
$ kubectl exec -it pod-with-fsgroup -- ls -ld /etc/configmap /etc/secret
drwxrwsrwx    3 root     2000          4096 Nov 21 20:58 /etc/configmap
drwxrwsrwt    3 root     2000           120 Nov 21 20:58 /etc/secret
```

```
$ kubectl exec -it pod-with-fsgroup -- ls -l /etc/configmap/
total 0
lrwxrwxrwx    1 root     2000            17 Nov 21 20:58 app.config -> ..data/app.config

$ kubectl exec -it pod-with-fsgroup -- ls -l /etc/secret/
total 0
lrwxrwxrwx    1 root     2000            15 Nov 21 20:58 password -> ..data/password
lrwxrwxrwx    1 root     2000            15 Nov 21 20:58 username -> ..data/username
```

## (2) fsGroup の設定により、Podの起動が遅くなる場合がある

``fsGroup`` を指定すると、対象のボリューム内のファイルやディレクトリの数が多い場合、それら全てのGIDを変更するのに時間がかかります。その結果、Podの起動が遅くなり、正常に起動するまでの時間が増えてしまいます。

この問題を回避するために、特定のボリュームに対して ``fsGroup`` を無効にしたり、条件つきで有効にしたりする方法があります。

### 回避策１ ： fsGroupChangePolicy の値を変更する

Podのマニフェストで、securityContext 配下に ``fsGroupChangePolicy`` というパラメータを追加します。この値を ``"OnRootMismatch"`` にすると、「マウントポイントのGIDが ``fsGroup`` のGIDと一致しない場合のみ、ボリューム内のファイルとディレクトリのGIDを変更する」挙動になります。

実際に挙動を確認してみましょう。

先ほど「(1) fsGroup の設定が有効にならない場合がある」でデプロイしたPodで、``nfs-pvc-from-storageclass`` のPVC経由でマウントしたディレクトリに、次のようにGIDが ``fsGroup`` のGIDとは異なるファイルを作ります。

```
$ kubectl exec -it pod-with-fsgroup -- sh
(以下はPod内でのコマンド)
/ # touch /mnt/nfs-volume-from-storageclass/tmp.txt
/ # chown root:1000 /mnt/nfs-volume-from-storageclass/tmp.txt
/ # ls -l /mnt/nfs-volume-from-storageclass/
total 0
-rw-r--r--    1 root     1000             0 Nov 23 00:43 tmp.txt
```

ちなみに、マウントポイントのルートディレクトリのGIDは ``fsGroup`` で指定した 2000 になっています。

```
(Pod内でのコマンド)
/ # ls -ld /mnt/nfs-volume-from-storageclass/
drwxrwsr-x    2 root     2000          4096 Nov 23 00:43 /mnt/nfs-volume-from-storageclass/
```

この後、一旦Podを削除します。

```
kubectl delete pod pod-with-fsgroup
```

続いて、``fsGroupChangePolicy: "OnRootMismatch"`` を追加した以下のマニフェストを使ってPodをデプロイします。

```yaml:pod-with-fsgroup-change-policy
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-fsgroup-change-policy
  namespace: default
spec:
  securityContext:
    fsGroup: 2000
    fsGroupChangePolicy: "OnRootMismatch" # 追加
  containers:
  - name: mnt-nfs-pod
    image: busybox
    command: ["tail", "-f", "/dev/null"]
    volumeMounts:
    - name: nfs-volume-from-storageclass
      mountPath: /mnt/nfs-volume-from-storageclass
  volumes:
  - name: nfs-volume-from-storageclass
    persistentVolumeClaim:
      claimName: nfs-pvc-from-storageclass
```

デプロイするコマンドは以下です。
```
kubectl apply -f pod-with-fsgroup-change-policy.yaml
```

``fsGroupChangePolicy: "OnRootMismatch"`` の設定が効いて、先ほど作成した ``tmp.txt`` のGIDが ``fsGroup`` で指定した 2000 にはならず、1000のままであることを確認できます。

```
$ kubectl exec -it pod-with-fsgroup-change-policy -- sh
(Pod内でのコマンド)
/ # ls -l /mnt/nfs-volume-from-storageclass/
total 0
-rw-r--r--    1 root     1000             0 Nov 23 00:43 tmp.txt
```

#### マウントポイントの GID が fsGroup の値を一致しない場合の挙動を確認

上で試したのは、「マウントポイントと ``fsGroup`` のGIDが一致する場合」の動作でした。
そこで次はマウントポイントのGIDを変更して、``fsGroup`` のGIDと異なる場合にどうなるかを試してみます。

```
(Pod内でのコマンド)
/ # chown root:root /mnt/nfs-volume-from-storageclass/
/ # ls -l /mnt/
total 4
drwxrwsr-x    2 root     root          4096 Nov 23 00:43 nfs-volume-from-storageclass
```

一旦Podを削除して再デプロイします。

```
kubectl delete pod pod-with-fsgroup-change-policy && \
kubectl apply -f pod-with-fsgroup-change-policy.yaml
```

すると、マウントポイントだけではなく、ファイルのGIDも ``fsGroup`` で指定した値の2000になっているのがわかります。

```
$ kubectl exec -it pod-with-fsgroup-change-policy -- sh
(Pod内でのコマンド)
/ # ls -ld /mnt/nfs-volume-from-storageclass/
drwxrwsr-x    2 root     2000          4096 Nov 23 00:43 /mnt/nfs-volume-from-storageclass/
/ # ls -l /mnt/nfs-volume-from-storageclass/
total 0
-rw-rw-r--    1 root     2000             0 Nov 23 00:43 tmp.txt
```

### 回避策２ ： CSIDriver の fsGroupPolicy を変更する

ボリュームがCSIドライバーによって作られている場合、CSIDriver リソースのパラメータ ``fsGroupPolicy`` の値を変更することによって、 ``fsGroup`` の有効／無効を制御できます。

:::note warn
CSIDriver の設定を変更する場合、これを使う StorageClass や PVC 全てに影響が及んでしまうので注意が必要です。すべてに影響を与えても問題がないことを確認した場合にのみ、設定変更を適用してください。
:::


#### CSIDriver リソースの設定確認

Podのマウントに使っているPVCに対応する CSIDriver リソースを特定します。

以下より、StorageClass は ``nfs-csi`` であることがわかります。
```
$ kubectl describe pvc nfs-pvc-from-storageclass | grep ^StorageClass
StorageClass:  nfs-csi
```
この StorageClass は、以下より ``nfs.csi.k8s.io`` の Provisioner を使っていることがわかります。
```
$ kubectl describe storageclasses.storage.k8s.io nfs-csi  | grep ^Provisioner
Provisioner:           nfs.csi.k8s.io
```
Provisioner名として出力された ``nfs.csi.k8s.io`` が、以下のようにそのまま CSIDriver リソースの名前になります。

```
$ kubectl get csidrivers
NAME             ATTACHREQUIRED   PODINFOONMOUNT   STORAGECAPACITY   TOKENREQUESTS   REQUIRESREPUBLISH   MODES        AGE
nfs.csi.k8s.io   false            false            false             <unset>         false               Persistent   6d14h
```

``nfs.csi.k8s.io`` のパラメータを確認すると、 ``fsGroupPolicy`` の値が ``File`` になっているのがわかります。
```yaml
$ kubectl get csidrivers nfs.csi.k8s.io -oyaml | grep -A 3 ^spec
spec:
  attachRequired: false
  fsGroupPolicy: File    # この部分
  podInfoOnMount: false
```

[ドキュメント](https://kubernetes-csi.github.io/docs/support-fsgroup.html#supported-modes)を読むと、``fsGroupPolicy`` が ``File`` の場合は「Kubernetesが ``fsGroup`` を使ってボリュームの権限と所有権を変更可能」と記載があります。これまで ``fsGroup`` を指定した際にマウントポイントのGIDが変更されていたのは、この設定が有効だったためです。

この値を ``None``にすると、``fsGroup`` の設定を完全に無効化できます。
（ちなみに ``ReadWriteOnceWithFSType`` にすると、 「ReadWriteOnce 以外の場合は ``fsGroup`` が無効」になります）

実際に試してみましょう。

#### 事前準備

※ 回避策1で作ったPodが残っている前提で書きます。
該当ボリュームのファイルを削除して、GIDをrootにしておきます。

```
$ kubectl exec -it pod-with-fsgroup-change-policy -- sh
(Pod内でのコマンド)
/ # rm /mnt/nfs-volume-from-storageclass/*
/ # chown root:root /mnt/nfs-volume-from-storageclass/
/ # exit
```

一旦Podを削除します。
```
kubectl delete pod pod-with-fsgroup-change-policy
```

#### fsGroupPolicy を None に変更して、fsGroup が反映されないことを確認

次のコマンドで、CSIDriver リソースの fsGroupPolicy を None に変更します。

```
kubectl patch csidriver nfs.csi.k8s.io \
--type='merge' -p '{"spec":{"fsGroupPolicy":"None"}}'
```

値が変わっていることを確認します。
```
$ kubectl get csidrivers nfs.csi.k8s.io -oyaml | grep fsGroup
  fsGroupPolicy: None
```

この状態で、再び「回避策1」で使ったPodをデプロイします。

```
kubectl apply -f pod-with-fsgroup-change-policy.yaml
```

すると、マウントポイントのGIDはrootのままで、 ``fsGroup`` が無効になっているのを確認できます。

```
$ kubectl exec -it pod-with-fsgroup-change-policy -- ls -ld /mnt/nfs-volume-from-storageclass/
drwxrwsr-x    2 root     root          4096 Nov 23 21:22 /mnt/nfs-volume-from-storageclass/
```

# おわりに

本記事を書いた背景を最後に書いておきます。

業務で動かしているシステムでDeploymentリソースから作られたPodがあり、起動に20分くらい近く掛かる事象が頻繁に起きていました。そのDeploymentリソースにReadWriteManyで複数のPodが同時にマウントしているボリュームがあり、ボリュームには大量のファイルが格納されていました。

この条件下で、Deploymentリソースには今回説明した ``fsGroup`` が設定されていました。そのため、Podが一つ起動する度に全ファイルのGIDを変更する処理が走り、Pod の起動がやたらと遅くなっていたのです。

Podの起動が遅くなっている原因が ``fsGroup`` であることを特定するのに、結構なエネルギーを要しました。問題解決のヒントになりそうなログやメトリクスがほぼ何も出てこなかったので、Deploymentリソースのマニフェストのパラメータを一つ一つ変更したり消したりして地道な調査をしました。作業当時のドキュメントを確認しところ、パラメータ変更の色々試し22回目くらいで、ようやく ``fsGroup`` が原因だと判明していました。

このような背景から、``fsGroup`` についてしっかり調べてみようと思い本記事を書きました。