俺たちは絶対にKubernetes上の分析ログを欠損させたくないんだっ...！！その対策としてのfluent-pvc-operatorを紹介🐥

この記事は[ZOZO Advent Calendar 2021](https://qiita.com/advent-calendar/2021/zozo)に出したかったけど既に枠が埋まっていたので野良で出した記事です。

# はじめに

こんにちは。絶対にKubernetes上の分析ログを欠損させたくない[@civitaspo](https://twitter.com/Civitaspo)です。今年は社内のログ収集基盤構築プロジェクトで[fluent-pvc-operator](https://github.com/st-tech/fluent-pvc-operator)という[Kubernetes Operator](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)を作りました。この記事ではfluent-pvc-operatorが解決する課題、及び機能や使い方について紹介したいと思います。

なお、この記事ではfluent-pvc-operatorの説明に集中するため、ログ収集基盤構築プロジェクトの全体像等には触れません。プロジェクトの全体像や詳細に関しては弊社@shiozakiの資料をご参照ください。

https://speakerdeck.com/shiozaki/log-collection-infrastructure-to-support-zozo-in-the-future?slide=19

また、[Pod](https://kubernetes.io/docs/concepts/workloads/pods/)内のアプリケーションが出力する分析ログは[Sidecar Container](https://kubernetes.io/docs/concepts/workloads/pods/#how-pods-manage-multiple-containers)の[fluentd](https://www.fluentd.org/)が回収し[Cloud Pub/Sub](https://cloud.google.com/pubsub)へ送信する設計[^1]としています。これから説明するfluent-pvc-operatorは、このPodからCloud Pub/Subへログを送信するログ収集フローの中でログ欠損を起こさないことを目的として設計・開発されました。そのため、fluent-pvc-operatorの機能を説明する時に度々このログ収集フローに言及します。このフローを念頭に置きつつ記事を読み進めて頂くと理解が進みやすいと思います。

[^1]: [Logging Architecture](https://kubernetes.io/docs/concepts/cluster-administration/logging/)で紹介されているとおり、他にもstdout/stderrを回収する方法やDaemonSetでログを回収する方法などがあります。ただ前者に関しては[CRI: Log management for container stdout/stderr streams](https://github.com/kubernetes/community/blob/3b28385/contributors/design-proposals/node/kubelet-cri-logging.md)で語られているとおりログ関連の要件を完全に満たすCRIは存在しないため欠損の可能性があり、後者に関してもDaemonSetのfluentdが他のPodよりも先に終了するとログが欠損するなど、要件を満たせるのはSidecar Containerがログを回収する方法のみでした。

# fluent-pvc-operator とは

fluent-pvc-operatorはPodに対して[PVC（Persistent Volume Claim）](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)を動的にプロビジョニングするKubernetes Operatorです。このKubernetes Operatorを導入することで事前にPVCを発行することなく、Podに対して使い捨てのPVCをアタッチすることができます。ちなみに「fluent」という名称が付いているのは、当初fluentdに強く依存したPVC管理を実現するKubernetes Operatorとして設計されていたためです。実装を洗練するにつれfluentd関連の依存を完全に切り離すことができたので、現在は単に「なめらかな」PVC管理を実現するという意味しか持っていません[^2]。

[^2]: 開発当初に「fluentd」という名前を使わずに「fluent」に留めたのは先見の明があったなと勝手に思っています。

# fluent-pvc-operatorが解決する課題

fluent-pvc-operatorを導入することで解決したい課題は以下の通りです。

- データを[PV（Persistent Volume）](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)へ永続化することで[Node](https://kubernetes.io/docs/concepts/architecture/nodes/)の突然死からPod内のデータを保護可能にする
- ファイルシステムに依存した終了処理を持つPodの[Lifecycle](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/)から終了処理を分離し、別のPodにその終了処理を委譲可能にする
- [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)や[DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/)など[Pod Template](https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates)から生成されるPodに対しても[Access ModeがRWO（ReadWriteOnce）またはRWOP（ReadWriteOncePod）](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes)であるPVCを利用可能にする

一つ一つ説明していきます。

### データをPVへ永続化することでNodeの突然死からPod内のデータを保護可能にする

Kubernetesを運用していると予期しない理由でNodeが終了してしまうことがあります。こういったケースでは多くの場合Podは正しい終了処理を行う時間なく終了させられてしまいます。Podが正しい終了処理を行えない場合、処理内容によってはPod内に保持している一部のデータが欠損してしまう可能性があります。我々のログ収集フローもPodが出力したログは一定程度バッファリングしてCloud Pub/Subへ送信するため、Podが正しい終了処理を経ずに終了することでバッファリングしていたログが欠損することになります。

fluent-pvc-operatorを導入するとPod内のデータをPVへ永続化できるため、この課題は大きく軽減されます。我々のログ収集フローではアプリケーションがログを出力した時点で必ずPVへ永続化することでログ欠損の可能性をほぼ無くしています[^3]。Sidecar ContainerのfluentdはPVに永続化されたログを回収してCloud Pub/Subへ送信しますが、Nodeが突然終了しても別のPodがそのPVを使用してログ回収を再開することで欠損無くログを回収することができます。

[^3]: クラウドベンダーのディスク障害などPVへの書き込みが不可になるケースは救えません。

### ファイルシステムに依存した終了処理を持つPodのLifecycleから終了処理を分離し、別のPodにその終了処理を委譲可能にする

我々のログ収集フローでは送信先のCloud Pub/Subが障害に陥ると障害復旧してログが送信完了するまでfluentdがログをバッファリングし続けることになります。このような状態で新規Podをデプロイすることを考えると、たとえば既存Pod終了時に全てのログをCloud Pub/Subへ送信しきらなければログが欠損するような仕組みであった場合は、Cloud Pub/Sub障害復旧まで旧Podの削除ができず新規Podがデプロイ出来ないことになります。

fluent-pvc-operatorはこういったケースに対応するため、発行したPVC毎に終了処理を行うPodをデプロイ出来る機能を持っています。この機能を利用することで我々のログ収集フローでは送信先の状態にかかわらず新規Podのデプロイが可能になっています。送信先のCloud Pub/Subが障害に陥っている状態で新規Podをデプロイする場合は、バッファリングしているログをPVへ永続化しPodを即終了します。そして、そのPVに永続化されたログに対して終了処理を行うPodがデプロイされ、Cloud Pub/Subの障害復旧まで送信のリトライを続けます。

このようにfluent-pvc-operatorを導入することで、アタッチされたPVCを利用してPodの終了処理を別のPodに委譲することができます。

### DeploymentやDaemonSetなどPod Templateから生成されるPodに対してもAccess ModeがRWOまたはRWOPであるPVCを利用可能にする

ログ収集時の永続化層としてPVを使用するため、[NFS（Network File System）](https://ja.wikipedia.org/wiki/Network_File_System)を実体とするようなAccess Modeが[RWX（ReadWriteMany）](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes)であるPVの利用は想定していませんでした[^4]。[AWS（Amazon Web Services）](https://aws.amazon.com/)の[EBS（Amazon Elastic Block Store）](https://aws.amazon.com/ebs/)や[GCP（Google Cloud Platform）](https://cloud.google.com/)の[PD（Persistent Disk）](https://cloud.google.com/persistent-disk)といったブロックストレージをPVとして利用する場合はAccess ModeがRWOまたはRWOPとなります。つまり、PVCはPodに対して1対1となるようにしかアタッチできません。1つのPVCを複数のPodで共有することはできません。このとき、DeploymentやDaemonSetといったPod Templateを利用してPodをデプロイするリソースで問題が出ます。それは定義したPVCが最初にデプロイされたPodでしか利用できず、2つ目以降のPodを立ち上げられなくなる問題です。[StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)には[`.spec.volumeClaimTemplates`](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#volume-claim-templates)を使ってPVCを動的に発行する機能が備わっていますが、DeploymentやDaemonSetにはそれに相当する機能はありません。つまり、DeploymentやDaemonSetではPVCを実質利用することができません。

DeploymentやDaemonSetで利用できないとなると、既存の多くのワークロードをStatefulSetに置き換える必要があり現実的な選択肢ではありませんでした。またKubernetesクラスタ運用者として、ログ収集に関する設定を意識しながらクラスタ利用者が[Manifest](https://kubernetes.io/docs/reference/glossary/?all=true#term-manifest)を書かなければならない状態は避ける必要がありました[^5]。

fluent-pvc-operatorはこの問題を解決するため、PVCを定義するためのテンプレートを別のリソースとして事前定義しておき、PodがデプロイされるタイミングでテンプレートからPVC発行してPodのManifestに注入する機能を持っています。ログ収集に関する詳細な設定はKubernetesクラスタ運用者が定義しておき、クラスタ利用者はその定義を利用するためにアノテーションを1行追加するだけで済むような仕組みにしています。

[^4]: NFSはSSDを使用したブロックストレージと比較してネットワークのスループットに影響されるなどパフォーマンス要求を満たさないため。また[flock取得時に書込モードでopenする必要がある](https://man7.org/linux/man-pages/man2/flock.2.html)など通常のファイルシステムとは異なる部分があるため早期に選択肢から外しました。

[^5]: 基盤提供者と利用者で責任分界点が曖昧になると運用コストが非常に高くなるため。

# fluent-pvc-operatorが持つ具体的な機能

ここまでの内容でfluent-pvc-operatorの持つ機能はある程度伝わっていると思いますが、具体的にfluent-pvc-operatorが持つ機能を一つ一つ説明していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24710/60ae5759-e53e-6ef9-9217-9ecb4bd572ea.png)


### Dynamic PVC Provisioning

[Podが作成されるタイミングでPVCを動的に作成し、PodのManifestへPVCがアタッチされるような設定を注入します](https://github.com/st-tech/fluent-pvc-operator/blob/2bf9ad5/webhooks/pod_webhook.go#L71-L88)。[Admission Webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)というKubernetesの機能を使って実現しています。

### Sidecar Container Injection

[Podが作成されるタイミングでManifestへ事前に定義されたSidecar Containerの設定を注入します](https://github.com/st-tech/fluent-pvc-operator/blob/2bf9ad5/webhooks/pod_webhook.go#L132-L148)。この機能もAdmission Webhookを使って実現しています。

### Unhealthy Pod Auto Deletion

[Pod起動中にSidecar Containerが再起動を繰り返すなど異常状態を検知したらそのPodの終了処理を行います](https://github.com/st-tech/fluent-pvc-operator/blob/2bf9ad5/controllers/pod_controller.go#L76-L96)。

### PVC Auto Finalization

[Podが削除されたあと、アタッチされていたPVCの終了処理を行うJobをデプロイします](https://github.com/st-tech/fluent-pvc-operator/blob/2bf9ad5/controllers/pvc_controller.go#L72-L157)。[Job](https://kubernetes.io/docs/concepts/workloads/controllers/job/)
が成功ステータスになるとPVCは自動で削除されます。

### Sidecar Container Auto Termination

JobでfluentdのようなデーモンプロセスをSidecar Containerとして利用する場合に使用する機能です。主となるContainerの終了を検知したらSidecar Containerは終了して良いと見なし終了処理を行います。この機能[^6]はまだ実装されておらず将来実装予定です。

[^6]: この機能は[KEP 753 Sidecar Containers](https://github.com/kubernetes/enhancements/tree/0e4d5df/keps/sig-node/753-sidecar-containers)の機能群がKubernetesで実装されたら不要になります。

# 使い方

fluent-pvc-operatorによって解決する課題や機能を説明したので、最後にfluent-pvc-operatorをデプロイして使う方法について説明します。

### [CRD（Custom Resource Definition）](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)と[Custom Controller](https://kubernetes.io/docs/concepts/architecture/controller/)をデプロイする

fleunt-pvc-operatorで使用するCRDとCustom Controllerをデプロイします。[必要なマニフェストを1つにまとめたファイルを用意している](https://github.com/st-tech/fluent-pvc-operator/blob/76ede7f/deploy/fluent-pvc-operator-aio.yaml)のでこのファイルを使ってデプロイをしてください。Admission Webhookを定義するため事前に[Cert Manager](https://cert-manager.io/docs/)がデプロイされている必要があります。

```shell
$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.3.1/cert-manager.yaml
$ kubectl apply -f ./deploy/fluent-pvc-operator-aio.yaml
```

fluent-pvc-operatorは2つのCRDを定義します。

* [`FluentPVC`](https://github.com/st-tech/fluent-pvc-operator/blob/2bf9ad5/config/crd/bases/fluent-pvc-operator.tech.zozo.com_fluentpvcs.yaml)
* [`FluentPVCBinding`](https://github.com/st-tech/fluent-pvc-operator/blob/2bf9ad5/config/crd/bases/fluent-pvc-operator.tech.zozo.com_fluentpvcbindings.yaml)

定義したCRDのうち`FluentPVCBinding`はfluent-pvc-operatorが内部的に使用するものです。利用者が[CR（Custom Resource）](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)として定義することはありません。`FluentPVCBinding`は動的に生成されたPVCとPodや終了処理を行うJobを内部的に関連付けるために使用されています。

### FluentPVCを定義する

利用者がCRとして定義する必要のある`FluentPVC`は以下の設定項目を持っています。PVCやPodに注入するSidecar Containerの定義、終了処理を行うJobの定義などです。

|名前|型|必須？|デフォルト値|説明|
|:---|:---|:--------|:------|:----------|
|pvcSpecTemplate|[PersistentVolumeClaimSpec](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimSpec)|true||発行するPVCのテンプレート|
|pvcFinalizerJobSpecTemplate|[JobSpec](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/job-v1/#JobSpec)|true||PVCの終了処理を行うJobのテンプレート|
|pvcVolumeName|string|true||Podに[Volume](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume/#Volume)として定義を注入するときの名称。この値は[RFC 1123に定義されたDNSラベル名](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names)の仕様を満たした上でPod内でユニークである必要があります。|
|pvcVolumeMountPath|string|true||Pod内のコンテナにマウントするときのPath。':'が含まれてはいけません。|
|sidecarContainerTemplate|[Container](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#Container)|true||Podに注入するSidecar Containerのテンプレート|
|commonEnvs|[][EnvVar](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables)|false|`[]`|Pod内の全てのコンテナに注入される共通の環境変数|
|commonVolumes|[][Volume](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume/#Volume)|false|`[]`|デプロイされるPodや終了処理を行うJobに注入される共通のVolume定義|
|commonVolumeMounts|[][VolumeMount](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1)|false|`[]`|Pod内のコンテナに共通で注入される[VolumeMount](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1)定義|
|deletePodIfSidecarContainerTerminationDetected|boolean|false|`true`|Sidecar Containerの異常を検知したときにPodを終了するか否か|


##### 設定例

簡略化のため`spec.pvcFinalizerJobSpecTemplate.template.spec.containers`や`spec.sidecarContainerTemplate`の定義は単に文字列を`echo`するだけにしています。より詳細な定義は後述の[例](#例)に含まれるManifestを参照してください。

```yaml
apiVersion: fluent-pvc-operator.tech.zozo.com/v1alpha1
kind: FluentPVC
metadata:
  name: fluent-pvc-sample
spec:
  pvcSpecTemplate:
    accessModes: [ "ReadWriteOnce" ]
    storageClassName: standard
    resources:
      requests:
        storage: 1Gi
  pvcFinalizerJobSpecTemplate:
    template:
      spec:
        restartPolicy: Never
        containers:
          - name: sidecar
            image: alpine:latest
            imagePullPolicy: Always
            command: [echo, finalizer]
            resources:
              limits:
                cpu: '1'
                memory: 1Gi
  pvcVolumeName: fluent-pvc
  pvcVolumeMountPath: /mnt/fluent-pvc
  sidecarContainerTemplate:
    name: sidecar
    image: alpine:latest
    imagePullPolicy: Always
    command: [echo sidecar]
    resources:
      limits:
        cpu: '1'
        memory: 1Gi
  deletePodIfSidecarContainerTerminationDetected: true
  commonEnvs:
    - name: FLUENT_PVC_MOUNT_DIR
      value: /mnt/fluent-pvc
  commonVolumes:
    - name: SOME_SECRET
      secret:
        secretName: some-secret
  commonVolumeMounts:
    - name: SOME_SECRET
      mountPath: /path/to/secret
```


# 例

fluent-pvc-operatorの動きを理解しやすくするため[kind](https://kind.sigs.k8s.io/)を使って構築したKubernetesにfluent-pvc-operatorをデプロイする例を用意しています。以下のコマンドを実行することでKubernetesクラスタがローカルに構築され、例がデプロイされます。

```shell
$ make examples/log-collection/clean-deploy
```

実際にデプロイされるManifest群は[examplesディレクトリ](https://github.com/st-tech/fluent-pvc-operator/tree/76ede7f/examples/log-collection/manifests)にまとめているので、合わせて参照ください。

# おわりに

この記事ではfluent-pvc-operatorの解きたかった課題や使い方について説明をしました。fluent-pvc-operatorが他者でも利用され、ログ欠損が発生しない幸せな世界になれば良いなと切に願っています。