この記事はZOZO Advent Calendar 2021に出したかったけど既に枠が埋まっていたので野良で出した記事です。
はじめに
こんにちは。絶対にKubernetes上の分析ログを欠損させたくない@civitaspoです。今年は社内のログ収集基盤構築プロジェクトでfluent-pvc-operatorというKubernetes Operatorを作りました。この記事ではfluent-pvc-operatorが解決する課題、及び機能や使い方について紹介したいと思います。
なお、この記事ではfluent-pvc-operatorの説明に集中するため、ログ収集基盤構築プロジェクトの全体像等には触れません。プロジェクトの全体像や詳細に関しては弊社@shiozakiの資料をご参照ください。
また、Pod内のアプリケーションが出力する分析ログはSidecar Containerのfluentdが回収しCloud Pub/Subへ送信する設計1としています。これから説明するfluent-pvc-operatorは、このPodからCloud Pub/Subへログを送信するログ収集フローの中でログ欠損を起こさないことを目的として設計・開発されました。そのため、fluent-pvc-operatorの機能を説明する時に度々このログ収集フローに言及します。このフローを念頭に置きつつ記事を読み進めて頂くと理解が進みやすいと思います。
fluent-pvc-operator とは
fluent-pvc-operatorはPodに対してPVC(Persistent Volume Claim)を動的にプロビジョニングするKubernetes Operatorです。このKubernetes Operatorを導入することで事前にPVCを発行することなく、Podに対して使い捨てのPVCをアタッチすることができます。ちなみに「fluent」という名称が付いているのは、当初fluentdに強く依存したPVC管理を実現するKubernetes Operatorとして設計されていたためです。実装を洗練するにつれfluentd関連の依存を完全に切り離すことができたので、現在は単に「なめらかな」PVC管理を実現するという意味しか持っていません2。
fluent-pvc-operatorが解決する課題
fluent-pvc-operatorを導入することで解決したい課題は以下の通りです。
- データをPV(Persistent Volume)へ永続化することでNodeの突然死からPod内のデータを保護可能にする
- ファイルシステムに依存した終了処理を持つPodのLifecycleから終了処理を分離し、別のPodにその終了処理を委譲可能にする
- DeploymentやDaemonSetなどPod Templateから生成されるPodに対してもAccess ModeがRWO(ReadWriteOnce)またはRWOP(ReadWriteOncePod)である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を使用してログ回収を再開することで欠損無くログを回収することができます。
ファイルシステムに依存した終了処理を持つ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)を実体とするようなAccess ModeがRWX(ReadWriteMany)であるPVの利用は想定していませんでした4。AWS(Amazon Web Services)のEBS(Amazon Elastic Block Store)やGCP(Google Cloud Platform)のPD(Persistent Disk)といったブロックストレージをPVとして利用する場合はAccess ModeがRWOまたはRWOPとなります。つまり、PVCはPodに対して1対1となるようにしかアタッチできません。1つのPVCを複数のPodで共有することはできません。このとき、DeploymentやDaemonSetといったPod Templateを利用してPodをデプロイするリソースで問題が出ます。それは定義したPVCが最初にデプロイされたPodでしか利用できず、2つ目以降のPodを立ち上げられなくなる問題です。StatefulSetには.spec.volumeClaimTemplates
を使ってPVCを動的に発行する機能が備わっていますが、DeploymentやDaemonSetにはそれに相当する機能はありません。つまり、DeploymentやDaemonSetではPVCを実質利用することができません。
DeploymentやDaemonSetで利用できないとなると、既存の多くのワークロードをStatefulSetに置き換える必要があり現実的な選択肢ではありませんでした。またKubernetesクラスタ運用者として、ログ収集に関する設定を意識しながらクラスタ利用者がManifestを書かなければならない状態は避ける必要がありました5。
fluent-pvc-operatorはこの問題を解決するため、PVCを定義するためのテンプレートを別のリソースとして事前定義しておき、PodがデプロイされるタイミングでテンプレートからPVC発行してPodのManifestに注入する機能を持っています。ログ収集に関する詳細な設定はKubernetesクラスタ運用者が定義しておき、クラスタ利用者はその定義を利用するためにアノテーションを1行追加するだけで済むような仕組みにしています。
fluent-pvc-operatorが持つ具体的な機能
ここまでの内容でfluent-pvc-operatorの持つ機能はある程度伝わっていると思いますが、具体的にfluent-pvc-operatorが持つ機能を一つ一つ説明していきます。
Dynamic PVC Provisioning
Podが作成されるタイミングでPVCを動的に作成し、PodのManifestへPVCがアタッチされるような設定を注入します。Admission WebhookというKubernetesの機能を使って実現しています。
Sidecar Container Injection
Podが作成されるタイミングでManifestへ事前に定義されたSidecar Containerの設定を注入します。この機能もAdmission Webhookを使って実現しています。
Unhealthy Pod Auto Deletion
Pod起動中にSidecar Containerが再起動を繰り返すなど異常状態を検知したらそのPodの終了処理を行います。
PVC Auto Finalization
Podが削除されたあと、アタッチされていたPVCの終了処理を行うJobをデプロイします。Job
が成功ステータスになるとPVCは自動で削除されます。
Sidecar Container Auto Termination
JobでfluentdのようなデーモンプロセスをSidecar Containerとして利用する場合に使用する機能です。主となるContainerの終了を検知したらSidecar Containerは終了して良いと見なし終了処理を行います。この機能6はまだ実装されておらず将来実装予定です。
使い方
fluent-pvc-operatorによって解決する課題や機能を説明したので、最後にfluent-pvc-operatorをデプロイして使う方法について説明します。
CRD(Custom Resource Definition)とCustom Controllerをデプロイする
fleunt-pvc-operatorで使用するCRDとCustom Controllerをデプロイします。必要なマニフェストを1つにまとめたファイルを用意しているのでこのファイルを使ってデプロイをしてください。Admission Webhookを定義するため事前にCert Managerがデプロイされている必要があります。
$ 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を定義します。
定義したCRDのうちFluentPVCBinding
はfluent-pvc-operatorが内部的に使用するものです。利用者がCR(Custom Resource)として定義することはありません。FluentPVCBinding
は動的に生成されたPVCとPodや終了処理を行うJobを内部的に関連付けるために使用されています。
FluentPVCを定義する
利用者がCRとして定義する必要のあるFluentPVC
は以下の設定項目を持っています。PVCやPodに注入するSidecar Containerの定義、終了処理を行うJobの定義などです。
名前 | 型 | 必須? | デフォルト値 | 説明 |
---|---|---|---|---|
pvcSpecTemplate | PersistentVolumeClaimSpec | true | 発行するPVCのテンプレート | |
pvcFinalizerJobSpecTemplate | JobSpec | true | PVCの終了処理を行うJobのテンプレート | |
pvcVolumeName | string | true | PodにVolumeとして定義を注入するときの名称。この値はRFC 1123に定義されたDNSラベル名の仕様を満たした上でPod内でユニークである必要があります。 | |
pvcVolumeMountPath | string | true | Pod内のコンテナにマウントするときのPath。':'が含まれてはいけません。 | |
sidecarContainerTemplate | Container | true | Podに注入するSidecar Containerのテンプレート | |
commonEnvs | []EnvVar | false | [] |
Pod内の全てのコンテナに注入される共通の環境変数 |
commonVolumes | []Volume | false | [] |
デプロイされるPodや終了処理を行うJobに注入される共通のVolume定義 |
commonVolumeMounts | []VolumeMount | false | [] |
Pod内のコンテナに共通で注入されるVolumeMount定義 |
deletePodIfSidecarContainerTerminationDetected | boolean | false | true |
Sidecar Containerの異常を検知したときにPodを終了するか否か |
設定例
簡略化のためspec.pvcFinalizerJobSpecTemplate.template.spec.containers
やspec.sidecarContainerTemplate
の定義は単に文字列をecho
するだけにしています。より詳細な定義は後述の例に含まれるManifestを参照してください。
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を使って構築したKubernetesにfluent-pvc-operatorをデプロイする例を用意しています。以下のコマンドを実行することでKubernetesクラスタがローカルに構築され、例がデプロイされます。
$ make examples/log-collection/clean-deploy
実際にデプロイされるManifest群はexamplesディレクトリにまとめているので、合わせて参照ください。
おわりに
この記事ではfluent-pvc-operatorの解きたかった課題や使い方について説明をしました。fluent-pvc-operatorが他者でも利用され、ログ欠損が発生しない幸せな世界になれば良いなと切に願っています。
-
Logging Architectureで紹介されているとおり、他にもstdout/stderrを回収する方法やDaemonSetでログを回収する方法などがあります。ただ前者に関してはCRI: Log management for container stdout/stderr streamsで語られているとおりログ関連の要件を完全に満たすCRIは存在しないため欠損の可能性があり、後者に関してもDaemonSetのfluentdが他のPodよりも先に終了するとログが欠損するなど、要件を満たせるのはSidecar Containerがログを回収する方法のみでした。 ↩
-
開発当初に「fluentd」という名前を使わずに「fluent」に留めたのは先見の明があったなと勝手に思っています。 ↩
-
クラウドベンダーのディスク障害などPVへの書き込みが不可になるケースは救えません。 ↩
-
NFSはSSDを使用したブロックストレージと比較してネットワークのスループットに影響されるなどパフォーマンス要求を満たさないため。またflock取得時に書込モードでopenする必要があるなど通常のファイルシステムとは異なる部分があるため早期に選択肢から外しました。 ↩
-
基盤提供者と利用者で責任分界点が曖昧になると運用コストが非常に高くなるため。 ↩
-
この機能はKEP 753 Sidecar Containersの機能群がKubernetesで実装されたら不要になります。 ↩