fluentbitでk8sコンテナログ配信

fluentbitはコンパクトなログ加工・配信エージェントです。入力・出力のプラグインを切り替えることで多様な構成に対応できます。

k8sノードからコンテナログ収集する構成

fluentbitは多様なフォーマットに対応し、クラスタ内に複数のエージェントをセットアップする構成を想定しています。アプリケーションから直接取得するログはセマンティックな構造を極力保存し、構造を持たない単なる文字列は素朴なエントリとして加工・集約できます。

kubernetes標準のログは、コンテナのstdout, stderrに出力したメッセージがnodeのログファイルに記録されます。
このログは構造を持たず高度なフィルタなどを実装できませんが、シェルスクリプトのようなワークロードの場合には構造化する手段もないため現実的な実装方式と言えます。

DaemonSetで各nodeにfluentbitを配備することで、クラスタ全体のログファイルを処理できます。

このほか、各コンテナのサイドカーとしてfluentbitをセットアップする構成も可能です。
サイドカーは単一podをデータソースとするため、json出力などを加工する用途に向いています。その場合、アプリケーションの仕様に合わせてfluentbitをセットアップすることとなり、kubernetesとはあまり関係のない構成になるため本記事の検討からは除外します。

fluentbitのconfig

公式dockerイメージは、/fluent-bit/etc/fluent-bit.confを参照しています。YAMLで記述する際は/fluent-bit/etc/fluent-bit.yamlが同義です。configの拡張子でパース挙動が変わります。

2024年現在fluentbitのconfigフォーマットはYAMLに移行済で、TOML類似のクラシックフォーマットは2025年に終了するのですが、 YAMLの公式ドキュメント類は整備途上です。

kubernetesのログファイルを扱うサンプルは次のような記述で動作します。

service:
  flush: 5
  grace: 120
  log_level: info
  log_file: /dev/stderr
  daemon: off
  http_server: Off

pipeline:
  inputs:
  # NOTE: tektonネームスペースのログファイルを処理対象とする設定例
  - name:            tail
    alias:           kube_containers_tekton
    tag:             kube_<namespace_name>_<pod_name>_<container_name>
    tag_regex:       '(?<pod_name>[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?<namespace_name>[^_]+)_(?<container_name>.+)-'
    path:            /var/log/containers/*_tekton_*.log
    db:              /var/lib/fluentbit-custom/kube_tekton.db
    read_from_head:  true

  filters:
  - name:         parser
    match:        kube_*
    key_name:     log
    reserve_data: true
    parser:       [cri]

  - name:         parser
    match:        kube_*
    key_name:     log
    reserve_data: true
    parser:       [json]

  outputs:
  # TODO: 実用上はログストアに送信するプラグイン設定が必要
  - name:        stdout
    match:       '*'

parsers:
- name:        cri
  format:      regex
  regex:       '^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<log>.*)$'
  time_key:    time
  time_format: "%Y-%m-%dT%H:%M:%S.%L%z"

- name:        json
  format:      json

Podから参照するには、 Create a ConfigMap from a directoryの手順でConfigMapとして登録しておきます。
kubernetes向けのポイントは次のとおりです。

  • ログファイルと作業DBはホストのファイルを参照している想定
    • inputs.[].pathinputs.[].dbはホスト上の実ファイルをk8sのhostPathでマウント
  • kubernetesログの各ファイル名に、<pod_name>_<namespace_name>_<container_name>という文字列が含まれる規約を利用
    • inputs.[].pathでネームスペースtektonを指定して処理対象をフィルタしている
    • inputs.[].tag_regexでタグに設定する文字列の変数を抽出している
  • parsersにCRI規約に沿ったパーサを定義
    • parsers.[].regexのマッチ結果はキーに追加される。この例では、time, stream, logtag, logというキーに値が入る
  • pipeline.filtersに加工プロセスを定義
    • filters.[].parserに複数のパーサを指定する方法は説明が存在しないが、配列を受けつける
  • service: { log_file: /dev/stderr }を指定するとkubectl logsでfluentdコンテナのログを参照できる

ログ加工時にコンテナ名称を参照する方法

既述のとおりコンテナ名称はログファイル名に含まれており、上の設定ではfluentbitのtagに抽出しました。tagはfilters.[].matchで複雑な処理分岐を制御するための変数です。
加工後のログでコンテナ名を参照したい場合には、tagと別にログレコードのキーに抽出しておく必要があります。関連するconfigパーツは次のようなものです。

pipeline:
  inputs:
  # NOTE: 既存のinputエントリにpath_keyオプション追加
  - name:            tail
    # 指定したキーにログファイルのパスが入る
    path_key:        log_path

  filters:
  # NOTE: キー抽出処理を追加
  - name:         parser
    match:        kube_*
    key_name:     log_path
    reserve_data: true
    parser:       pod_name

parsers:
# NOTE: キー抽出のためのパーサを追加
- name:        pod_name
  format:      regex
  # inputs.[].tag_regexと同じ処理
  regex:       '(?<pod_name>[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?<namespace_name>[^_]+)_(?<container_name>.+)-'

これ以外にLuaで実装する方法もとれます。

ログレベルのフィルタ

ログ配信を整備する際、出力量の爆発によく直面します。
次のようなフィルタを追加するとログレベルに応じて配信をフィルタできます。

pipeline:
  filters:
  # remove low level logs from EventListener
  - name: grep
    match: kube_tekton_eventlistener-*
    exclude: severity info|debug

ただし、この例のフィルタが機能するためには、severityというキーにログレベルが入る必要があります。
特定のアプリケーションがjsonにログレベルを示す構造データを出力している場合には利用できます。シェルスクリプトなどのメッセージ出力にはログレベルがないため、この方法はとれません。

fluentbitコンテナマニフェスト

次のとおり、configとログファイルのセットアップがポイントです。

  • configファイルはConfigMapから供給
  • 起動時の-cオプションでconfigのパスを指定。fluentbitは拡張子でパース挙動がかわる
  • ホストのログをhostPathでマウントする
apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    k8s-app: fluentbit-custom
  name: fluentbit-custom
spec:
  selector:
    matchLabels:
      k8s-app: fluentbit-custom
  template:
    metadata:
      labels:
        k8s-app: fluentbit-custom
    spec:
      containers:
      - image: fluent/fluent-bit:3.2
        name: fluentbit
        args: ["-c", "/fluent-bit/etc/fluent-bit.yaml"]
        volumeMounts:
        - mountPath: /var/log
          name: logdir
        - mountPath: /var/lib/fluentbit-custom
          name: workdir
        - mountPath: /fluent-bit/etc/
          name: configs

      volumes:
      - hostPath:
          path: /var/log
          type: Directory
        name: logdir
      - hostPath:
          path: /var/lib/fluentbit-custom
          type: DirectoryOrCreate
        name: workdir
      - configMap:
          defaultMode: 420
          # NOTE: 上のconfigを保持しているConfigMapを参照
          name: fluentbit-config
        name: configs

この例ではnodeのログファイル参照に絞っていますが、利用するプラグインによってはkubernetesクラスタへのアクセス権が必要なケースもあります。
GKE向けfluentbit構成のリポジトリには、ServiceAccountを指定するサンプルがあり参考になります。

⁋ 2024/11/25↻ 2024/12/18
中馬崇尋
Chuma Takahiro