Fluent Bitを使ってOpenShiftからログを飛ばす

# はじめに
Kubernetes環境でFluentdを使ってログを転送するという情報はよく見るのですが、Fluentdより軽量と噂のFluent Bitを使い、且つOpenShift環境でという構成はあまり情報なさそうでしたので、実際にやってみた結果を記載します。
Fluent Bitについては[公式サイト](https://fluentbit.io/)を参照ください。

# 構成
## 全体構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142407/0728a563-15ed-7cd1-da07-cf6076a833c0.png)

上記のように、OpenShiftクラスター上にFluent BitのPodをDaemonsetでデプロイし、OpenShiftクラスター全体のログを仮想サーバー上にインストールしたFluent Bitに転送する構成で検証を行いました。OpenShiftはIBM Cloudで提供されるRed Hat OpenShift on IBM Cloudを利用しています。

## 詳細構成情報

OpenShift（Red Hat OpenShift on IBM Cloud）：v4.5.24_1527
　Workerノード x3（以前[こちら](https://qiita.com/rjegg/items/3fb6ca1ac2d508d2d662)で記事にした環境です）
Fluent Bit：v1.6
仮想サーバー：RHEL7.9

# 作ってみた
## 仮想サーバー側（受信側）
インストールは公式サイトの[こちら](https://docs.fluentbit.io/manual/installation/linux/redhat-centos)の手順に従い実施し、サービスを起動させておきます。

設定情報はこちらです。INPUT/OUTPUTの部分のみデフォルト値から変更しています。

```/etc/td-agent-bit/td-agent-bit.conf
[SERVICE]
    flush        5
    daemon       Off
    log_level    info
    parsers_file parsers.conf
    plugins_file plugins.conf
    http_server  Off
    http_listen  0.0.0.0
    http_port    2020
    storage.metrics on
[INPUT]
    Name              forward
    Port              24225
    Buffer_Chunk_Size 32MB
    Buffer_Max_Size   64MB
[OUTPUT]
    name  file
    match *
    path  /data/log/td-agent-bit    # ディレクトリ配下にタグ名のファイルが出力されます。
```
※Port、Bufferサイズ等は特に意図はありません。サイズはあまり小さいとエラーになりました。

## OpenShift側（送信側）
今回検証するにあたり、下記GitHubの情報を参考にしています。
[fluent/fluent-bit-kubernetes-logging](https://github.com/fluent/fluent-bit-kubernetes-logging)
[fluent/fluentd-kubernetes-daemonset](https://github.com/fluent/fluentd-kubernetes-daemonset)

Fluent BitのPodをデプロするために下記7つのリソースを作成します。
- Namespace
- ServiceAccount
- ClusterRole
- ClusterRoleBinding
- SecurityContextConstraints
- ConfigMap
- DaemonSet

下記それぞれの設定及びYAMLファイルです。
- Namespace
Namespaceは`oc new-project fluentbittest`コマンドでfluentbittestというNamespaceを作成しました。

- ServiceAccount

```fluent-bit-service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluent-bit
  namespace: fluentbittest
```

- ClusterRole

```fluent-bit-role.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: fluent-bit-read
rules:
- apiGroups: [""]
  resources:
  - namespaces
  - pods
  verbs: ["get", "list", "watch"]
```

- ClusterRoleBinding

```fluent-bit-role-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: fluent-bit-read
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: fluent-bit-read
subjects:
- kind: ServiceAccount
  name: fluent-bit
  namespace: fluentbittest
```

- SecurityContextConstraints

こちらはOpenShift特有の特権設定となり、上記GitHubでも紹介されています。

```fluent-bit-openshift-security-context-constraints.yaml
kind: SecurityContextConstraints
apiVersion: security.openshift.io/v1
metadata:
  name: fluentbittest
allowPrivilegedContainer: true
allowHostNetwork: true
allowHostDirVolumePlugin: true
priority:
allowedCapabilities: []
allowHostPorts: true
allowHostPID: true
allowHostIPC: true
readOnlyRootFilesystem: false
requiredDropCapabilities: []
defaultAddCapabilities: []
runAsUser:
  type: RunAsAny
seLinuxContext:
  type: MustRunAs
fsGroup:
  type: MustRunAs
supplementalGroups:
  type: RunAsAny
volumes:
  - configMap
  - downwardAPI
  - emptyDir
  - hostPath
  - persistentVolumeClaim
  - projected
  - secret
users:
  - system:serviceaccount:fluentbittest:builder
  - system:serviceaccount:fluentbittest:default
  - system:serviceaccount:fluentbittest:deployer
  - system:serviceaccount:fluentbittest:fluent-bit
```


- ConfigMap

OpenShiftのコンテナランタイムにはcri-oが使われているため、[INPUT]のparserには`cri`を指定します。`cri`設定の詳細は[PARSER]の`cri`部分を確認ください。

```fluent-bit-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
  namespace: fluentbittest
  labels:
    k8s-app: fluent-bit
data:
  # Configuration files: server, input, filters and output
  # ======================================================
  fluent-bit.conf: |
    [SERVICE]
        Flush         1
        Log_Level     info
        Daemon        off
        Parsers_File  parsers.conf
        HTTP_Server   On
        HTTP_Listen   0.0.0.0
        HTTP_Port     2020

    @INCLUDE input-kubernetes.conf
    @INCLUDE filter-kubernetes.conf
    @INCLUDE output-forward.conf

  input-kubernetes.conf: |
    [INPUT]
        Name              tail
        Tag               kube.*
        Path              /var/log/containers/*.log
        Parser            cri
        DB                /var/log/flb_kube.db
        Mem_Buf_Limit     5MB
        Skip_Long_Lines   On
        Refresh_Interval  10

  filter-kubernetes.conf: |
    [FILTER]
        Name                kubernetes
        Match               kube.*
        Kube_URL            https://kubernetes.default.svc:443
        Kube_CA_File        /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        Kube_Token_File     /var/run/secrets/kubernetes.io/serviceaccount/token
        Kube_Tag_Prefix     kube.var.log.containers.
        Merge_Log           On
        Merge_Log_Key       log_processed
        K8S-Logging.Parser  On
        K8S-Logging.Exclude Off

  output-forward.conf: |
    [OUTPUT]
        Name          forward
        Match         *
        Host          ${FLUENT_FOWARD_HOST}
        Port          ${FLUENT_FOWARD_PORT}
        Retry_Limit     False

  parsers.conf: |
    [PARSER]
        Name   apache
        Format regex
        Regex  ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
        Time_Key time
        Time_Format %d/%b/%Y:%H:%M:%S %z

    [PARSER]
        Name   apache2
        Format regex
        Regex  ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
        Time_Key time
        Time_Format %d/%b/%Y:%H:%M:%S %z

    [PARSER]
        Name   apache_error
        Format regex
        Regex  ^\[[^ ]* (?<time>[^\]]*)\] \[(?<level>[^\]]*)\](?: \[pid (?<pid>[^\]]*)\])?( \[client (?<client>[^\]]*)\])? (?<message>.*)$

    [PARSER]
        Name   nginx
        Format regex
        Regex ^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
        Time_Key time
        Time_Format %d/%b/%Y:%H:%M:%S %z

    [PARSER]
        Name   json
        Format json
        Time_Key time
        Time_Format %d/%b/%Y:%H:%M:%S %z

    [PARSER]
        Name        docker
        Format      json
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L
        Time_Keep   On

    [PARSER]
        # http://rubular.com/r/tjUt3Awgg4
        Name cri
        Format regex
        Regex ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<message>.*)$
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L%z

    [PARSER]
        Name        syslog
        Format      regex
        Regex       ^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$
        Time_Key    time
        Time_Format %b %d %H:%M:%S
```


- DaemonSet

```fluent-bit-ds.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluent-bit
  namespace: fluentbittest
  labels:
    k8s-app: fluent-bit-logging
    version: v1
    kubernetes.io/cluster-service: "true"
spec:
  selector:
    matchLabels:
      k8s-app: fluent-bit-logging
  template:
    metadata:
      labels:
        k8s-app: fluent-bit-logging
        version: v1
        kubernetes.io/cluster-service: "true"
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "2020"
        prometheus.io/path: /api/v1/metrics/prometheus
    spec:
      containers:
      - name: fluent-bit
        image: fluent/fluent-bit:1.6
        imagePullPolicy: Always
        ports:
          - containerPort: 2020
        env:
        - name: FLUENT_FOWARD_HOST
          value: "X.X.X.X"
        - name: FLUENT_FOWARD_PORT
          value: "24225"
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
        - name: fluent-bit-config
          mountPath: /fluent-bit/etc/
        securityContext:
          privileged: true
      terminationGracePeriodSeconds: 10
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: fluent-bit-config
        configMap:
          name: fluent-bit-config
      serviceAccountName: fluent-bit
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      - operator: "Exists"
        effect: "NoExecute"
      - operator: "Exists"
        effect: "NoSchedule"
```

※Docker関連の設定が残ったままです。。
※上記Daemonsetに下記のようにSecurityContextの設定を入れておかないと、Fluent Bit Podがノード上のログファイルを読めずにエラーになりますので、注意です。（[参照](https://github.com/fluent/fluentd-kubernetes-daemonset#running-on-openshift)）

```
        securityContext:
          privileged: true
```


上記リソースを順に`oc create -f <filename>`で作成するとPodが起動します。

```
$ oc get pod
NAME               READY   STATUS    RESTARTS   AGE
fluent-bit-7wj72   1/1     Running   0          53s
fluent-bit-97z7r   1/1     Running   0          53s
fluent-bit-w26z5   1/1     Running   0          53s
```

# 受信確認
仮想サーバー側で転送されたログがファイル出力されているかを確認します。

```
$ ls -l /data/log/td-agent-bit/
total 80592
-rw-r--r-- 1 root root     1477 Jan 25 20:18 kube.var.log.containers.calico-kube-controllers-6c4d9c955b-k8w6d_calico-system_calico-kube-controllers-355b03ba53e7dea6461944eb616be24ae6817d21acec8133e9ff6a845fc7b954.log
-rw-r--r-- 1 root root   164785 Jan 25 20:19 kube.var.log.containers.calico-node-lgrns_calico-system_calico-node-592a8f869fcf0460fd01c0114ea8c558e6e8167d92c3e0d59a609d0abb72be2e.log
-rw-r--r-- 1 root root   171322 Jan 25 20:19 kube.var.log.containers.calico-node-wjsbn_calico-system_calico-node-a8cfa55b9431bd668bcf744ed125d29747ae3c454d2c6c292556789822b79a5f.log
-rw-r--r-- 1 root root   175063 Jan 25 20:19 kube.var.log.containers.calico-node-xdxjb_calico-system_calico-node-eb24febe3c3d651e39f941fac47f70f45664d83a31371bc774c25f61253347cf.log
-rw-r--r-- 1 root root     1128 Jan 25 20:17 kube.var.log.containers.calico-typha-5c8d96f77d-sfltp_calico-system_calico-typha-d70a3dde704772db3015d654db936e4b9b1902f8312228836dcb99e17d64959e.log
：
（後略）
```

ログファイルの中身（一番上に表示されたログで確認）。

```
[user@vsi ~]$ cat /data/log/td-agent-bit/kube.var.log.containers.calico-kube-controllers-6c4d9c955b-k8w6d_calico-system_calico-kube-controllers-355b03ba53e7dea6461944eb616be24ae6817d21acec8133e9ff6a845fc7b954.log
kube.var.log.containers.calico-kube-controllers-6c4d9c955b-k8w6d_calico-system_calico-kube-controllers-355b03ba53e7dea6461944eb616be24ae6817d21acec8133e9ff6a845fc7b954.log: [1611627483.220251215, {"stream":"stderr","logtag":"F","message":"2021-01-26 02:18:03.220 [INFO][1] watchercache.go 96: Watch channel closed by remote - recreate watcher ListRoot=\"/calico/resources/v3/projectcalico.org/nodes\"","kubernetes":{"pod_name":"calico-kube-controllers-6c4d9c955b-k8w6d","namespace_name":"calico-system","pod_id":"a25ffa7a-ecf7-4140-8e65-4e4f174e80f6","labels":{"k8s-app":"calico-kube-controllers","pod-template-hash":"6c4d9c955b"},"annotations":{"cni.projectcalico.org/podIP":"172.17.59.83/32","cni.projectcalico.org/podIPs":"172.17.59.83/32","k8s.v1.cni.cncf.io/network-status":"[{\n    \"name\": \"k8s-pod-network\",\n    \"ips\": [\n        \"172.17.59.83\"\n    ],\n    \"default\": true,\n    \"dns\": {}\n}]","k8s.v1.cni.cncf.io/networks-status":"[{\n    \"name\": \"k8s-pod-network\",\n    \"ips\": [\n        \"172.17.59.83\"\n    ],\n    \"default\": true,\n    \"dns\": {}\n}]"},"host":"10.240.0.4","container_name":"calico-kube-controllers","docker_id":"355b03ba53e7dea6461944eb616be24ae6817d21acec8133e9ff6a845fc7b954","container_hash":"registry.ng.bluemix.net/armada-master/calico/kube-controllers@sha256:eb456f071b19614a6a4eb149cf1eb2dc924780accc2f9b426305790e03c2403b","container_image":"registry.ng.bluemix.net/armada-master/calico/kube-controllers:v3.16.5"}}]
[user@vsi ~]$
```

これでOpenShift上のログが転送されていることが確認できました。

# おまけ

せっかく軽量と噂のFluent Bitをたてたので、Fluentdと比較して軽量っぷりを見てみました。

- 比較用の環境

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142407/0d90e5e1-0776-1cb8-8e41-1d109ceea2c0.png)


Fluentdは[fluent/fluentd-kubernetes-daemonset](https://github.com/fluent/fluentd-kubernetes-daemonset)を参照して構築しました。このfluentdとFluent Bitの検証環境と同じOpenShift上のログを同仮想サーバーに転送するためのFluentd Podのリソース使用量を比較してみます。

```
$ oc get pod -n logtest
NAME            READY   STATUS    RESTARTS   AGE
fluentd-2cbqz   1/1     Running   0          5d19h
fluentd-4mh8f   1/1     Running   0          5d19h
fluentd-77h8b   1/1     Running   0          5d19h
```

こちらのFluentdは、多少のフィルタ処理をいれているため、正確な比較にはなっていませんが、ザックリとした比較にはなるかと思います。
以下結果となります。

- 結果：Fluentd

```
$ oc adm top po -n logtest
NAME            CPU(cores)   MEMORY(bytes)
fluentd-2cbqz   12m          112Mi
fluentd-4mh8f   15m          112Mi
fluentd-77h8b   15m          119Mi
```


- 結果：Fluent Bit

```
$ oc adm top po -n fluentbittest
NAME               CPU(cores)   MEMORY(bytes)
fluent-bit-7wj72   8m           9Mi
fluent-bit-97z7r   4m           8Mi
fluent-bit-w26z5   3m           7Mi
```

結果、Fluent BitのほうがFluentdと比較して**CPU使用量は1/2程度以下**、**メモリ使用量は1/10程度以下**のリソース使用量でした。（正確な比較ができてないのでザックリな表現となります。。）

上記記載の通り、Fluentdのほうが処理内容が多少多いというのはありますが、**CPU使用量は1/2程度以下**、**メモリ使用量1/10程度以下**のリソース使用量というのは魅力的ですね。特にOpenShift/Kubernetesクラスター環境はログの量が非常に多いため、ログコレクターの処理も重くなりがちです。今回の構成は単純にforward設定によりログを転送させただけの構成ですので、本番環境で使用するためには諸々調整および検証が必要になる点考慮いただければと思います。