fluentd(fluent-bit) -> Kinesis Data Firehose -> S3 で大量ログを欠損なく保存する

# 今回のミッション
- EKSにデプロイしたアプリケーションログの一部をS3に保存する
- 秒間数百〜数千件、欠損はNG

AWS, コンテナ技術素人が上記ミッションに挑んだ備忘録です。


# 構成図
ログをfluentd(fluent-bit)が監視してKinesis Data Firehoseの配信ストリームに送り、S3に集約する。
前提：EKS, Firehose, S3の各リソースは既にあり、連携できる状態になっている。
（あとで絵を書く）

## 試行パターン
1. fluentd デーモンセット
- fluentd サイドカー <--最終的に採用した方式
- fluent-bit デーモンセット
- fluent-bit サイドカー


# ログを出力するアプリケーション

ここではnginxを例に記載する。
実際のアプリケーションはtomcat + Java。
サイドカー方式では/applog/配下に収集対象のログ（*.log）が出力されるものとする。

```yaml:nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
```

# fluentd

まずはKubernetes上のログ収集の常套手段であるデーモンセットでfluentdを動かすことを試しました。
しかし今回のアプリケーションはそもそものログ出力が多く、最終的には収集対象のログのみを別のログファイルに切り出し、それをサイドカーで収集する方針としました。

## デーモンセット

ベースはここから取得（fluentd-daemonset-elasticsearch-rbac.yaml）
https://github.com/fluent/fluentd-kubernetes-daemonset

```yaml:fluentd-daemonset.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd
  namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluentd
  namespace: kube-system
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - namespaces
  verbs:
  - get
  - list
  - watch

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: fluentd
roleRef:
  kind: ClusterRole
  name: fluentd
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
  name: fluentd
  namespace: kube-system

---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
    version: v1
spec:
  selector:
    matchLabels:
      k8s-app: fluentd-logging
      version: v1
  template:
    metadata:
      annotations:
        iam.amazonaws.com/role: ap-northeast-1a.staging.kubernetes.ruist.io-service-role #要確認
      labels:
        k8s-app: fluentd-logging
        version: v1
    spec:
      serviceAccount: fluentd
      serviceAccountName: fluentd
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd
        image: fluent/fluentd-kubernetes-daemonset:v1-debian-kinesis
        env:
          - name: LOG_GROUP_NAME #要確認
            value: "k8s"
          - name: AWS_REGION #要確認
            value: "ap-northeast-1"
          - name: FLUENT_KINESIS_STREAMS_REGION
            value: "ap-northeast-1"
          - name: FLUENT_KINESIS_DELIVERY_STREAM_NAME
            value: "XXXXX-stream"
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
        - name: fluentd-config
          mountPath: /fluentd/etc/fluent.conf
          subPath: fluent.conf
          readOnly: true
        - name: kubernetes-config
          mountPath: /fluentd/etc/kubernetes.conf
          subPath: kubernetes.conf
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: fluentd-config
        configMap:
          defaultMode: 0644
          name: fluentd-config
      - name: kubernetes-config
        configMap:
          defaultMode: 0644
          name: kubernetes-config

```


```yaml:fluentd-daemonset-configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
    version: v1
    kubernetes.io/cluster-service: "true"
data:
  fluent.conf: |
    @include kubernetes.conf
    <match **>
       @type kinesis_firehose
       @id out_kinesis_firehose
	   region "#{ENV['FLUENT_KINESIS_STREAM_REGION'] || nil}"
	   delivery_stream_name "#{ENV['FLUENT_KINESIS_DELIVERY_STREAM_NAME'] || nil}"
    </match>

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: kubernetes-config
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
    version: v1
    kubernetes.io/cluster-service: "true"
data:
  kubernetes.conf: |
    <label @FLUENT_LOG>
      <match fluent.**>
        @type null
      </match>
	</label>
    <source>
      @type tail
      @id in_tail_container_logs
      path "/var/log/containers/nginx*.log"
      pos_file "/var/log/fluentd-container.log.pos"
      tag "kubernetes.*"
      read_from_head true
      <parse>
        @type "json"
        time_format "%Y-%m-%dT%H:%M:%S.%NZ"
        time_type string
      </parse>
    </source>
```

```console
kubectl apply -f nginx-deployment.yaml
kubectl apply -f fluentd-daemonset-configmap.yaml
kubectl apply -f fluentd-daemonset.yaml
```

## サイドカー

```yaml:fluentd-sidecar.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      - name: fluentd
        image: fluent/fluentd-kubernetes-daemonset:v1-debian-kinesis
        env:
          - name:  LOG_GROUP_NAME
            value: "k8s"
          - name:  AWS_REGION
            value: "ap-northeast-1"
          - name: FLUENT_KINESIS_STREAMS_REGION
            value: "ap-northeast-1"
          - name: FLUENT_KINESIS_DELIVERY_STREAM_NAME
            value: "XXXXX-stream"
        volumeMounts:
        - name: applog-volume
          mountPath: /applog
        - name: fluentd-config
          mountPath: /fluentd/etc/fluent.conf
          subPath: fluent.conf
          readOnly: true
        - name: kubernetes-config
          mountPath: /fluentd/etc/kubernetes.conf
          subPath: kubernetes.conf
          readOnly: true
      volumes:
      - name: applog-volume
        emptyDir: {}
      - name: fluentd-config
        configMap:
          defaultMode: 0644
          name: fluentd-config
      - name: kubernetes-config
        configMap:
          defaultMode: 0644
          name: kubernetes-config
```

```yaml:fluentd-sidecar-configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
  labels:
    k8s-app: fluentd-logging
    version: v1
    kubernetes.io/cluster-service: "true"
data:
  fluent.conf: |
    @include kubernetes.conf
    <match **>
       @type kinesis_firehose
       @id out_kinesis_firehose
	   region "#{ENV['FLUENT_KINESIS_STREAM_REGION'] || nil}"
	   delivery_stream_name "#{ENV['FLUENT_KINESIS_DELIVERY_STREAM_NAME'] || nil}"
	   retries_on_batch_request 20
       <buffer>
	 flush_interval 1
         chunk_limit_size 1m
         flush_thread_interval 0.1
         flush_thread_burst_interval 0.01
         flush_thread_count 15
       </buffer>
    </match>

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: kubernetes-config
  labels:
    k8s-app: fluentd-logging
    version: v1
    kubernetes.io/cluster-service: "true"
data:
  kubernetes.conf: |
    <source>
      @type tail
      path "/applog/*.log"
      pos_file "/var/log/fluentd-applog.log.pos"
      tag "applog.*"
      read_from_head true
      <parse>
        @type "json"
        time_format "%Y-%m-%dT%H:%M:%S.%NZ"
        time_type string
      </parse>
    </source>
```

```console
kubectl apply -f fluentd-sidecar-configmap.yaml
kubectl apply -f fluentd-sidecar.yaml
```

# fluent-bit

fluentdでロストが発生したため、fluent-bitの利用も試しました。
結果的にはKinesis側のLimit超過が原因であったため、情報が多いfluentdでの検証に戻りましたが、ログとして残しておきます。

## デーモンセット

```yaml:fluentbit-daemonset.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: flunt-bit
  namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: flunt-bit
  namespace: kube-system
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - namespaces
  verbs:
  - get
  - list
  - watch

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: flunt-bit
roleRef:
  kind: ClusterRole
  name: flunt-bit
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
  name: flunt-bit
  namespace: kube-system

---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: flunt-bit
  namespace: kube-system
  labels:
    k8s-app: flunt-bit-logging
    version: v1
spec:
  selector:
    matchLabels:
      k8s-app: flunt-bit-logging
      version: v1
  template:
    metadata:
      annotations:
        iam.amazonaws.com/role: ap-northeast-1a.staging.kubernetes.ruist.io-service-role # 要確認
      labels:
        k8s-app: flunt-bit-logging
        version: v1
    spec:
      serviceAccount: flunt-bit
      serviceAccountName: flunt-bit
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: flunt-bit
        image: public.ecr.aws/aws-observability/aws-for-fluent-bit:latest
        env:
          - name:  LOG_GROUP_NAME
            value: "k8s"
          - name:  AWS_REGION
            value: "ap-northeast-1"
          - name: FLUENT_KINESIS_STREAMS_REGION
            value: "ap-northeast-1"
          - name: FLUENT_KINESIS_DELIVERY_STREAM_NAME
            value: "XXXXX-stream"
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
        - name: flunt-bit-config
          mountPath: /flunt-bit/etc/
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: flunt-bit-config
        configMap:
          name: flunt-bit-config
```

```yaml:fluentbit-daemonset-configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: flunt-bit-config
  namespace: kube-system
  labels:
    k8s-app: flunt-bit-logging
    version: v1
    kubernetes.io/cluster-service: "true"
data:
  fluent.conf: |
    [SERVICE]
      Parsers_File  parsers.conf
    [INPUT]
      Name tail
      Path /var/log/containers/*.log
      Parser docker
      DB /var/log/flb_kube.db
      Tag kube.*
      Mem_Buf_Limit 5MB
      Skip_Long_Lines On
      Refresh_Interval 10
    [FILTER]
      name grep
      match *
      regex log XXXXX
    [OUTPUT]
      Name kinesis_firehose
      Match *
      delivery_stream XXX-stream
      region ap-northeast-1
  parsers.conf: |
    [PARSER]
      Name docker
      Format json
      Time_Key time
      Timt_Format "%Y-%m-%dT%H:%M:%S.%NZ"
      Time_Keep On
```

```console
kubectl apply -f nginx-deployment.yaml
kubectl apply -f fluentbit-daemonset-configmap.yaml
kubectl apply -f fluentbit-daemonset.yaml
```

## サイドカー

```yaml:fluentbit-sidecar.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      - name: flunet-bit
        image: public.ecr.aws/aws-observability/aws-for-fluent-bit:latest
        env:
          - name:  LOG_GROUP_NAME
            value: "k8s"
          - name:  AWS_REGION
            value: "ap-northeast-1"
          - name: FLUENT_KINESIS_STREAMS_REGION
            value: "ap-northeast-1"
          - name: FLUENT_KINESIS_DELIVERY_STREAM_NAME
            value: "XXXXX-stream"
        volumeMounts:
        - name: applog-volume
          mountPath: /applog
        - name: flunt-bit-config
          mountPath: /flunt-bit/etc/
      volumes:
      - name: applog-volume
        emptyDir: {}
      - name: flunt-bit-config
        configMap:
          name: flunt-bit-config
```

```yaml:fluentbit-sidecar-configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: flunt-bit-config
  labels:
    k8s-app: flunt-bit-logging
    version: v1
    kubernetes.io/cluster-service: "true"
data:
  fluent.conf: |
    [SERVICE]
      Parsers_File  parsers.conf
    [INPUT]
      Name tail
      Path /applog/*.log
      Parser docker
      DB /var/log/flb_kube.db
      Tag applog.*
      Mem_Buf_Limit 5MB
      Skip_Long_Lines On
      Refresh_Interval 10
    [OUTPUT]
      Name kinesis_firehose
      Match *
      delivery_stream XXX-stream
      region ap-northeast-1
  parsers.conf: |
    [PARSER]
      Name docker
      Format json
      Time_Key time
      Timt_Format "%Y-%m-%dT%H:%M:%S.%NZ"
      Time_Keep On
```

```console
kubectl apply -f fluentbit-sidecar-configmap.yaml
kubectl apply -f fluentbit-sidecar.yaml
```

# 詰まりポイント
## AWSがわからない

プラットフォームは提供してもらったものの、AWSそのものを初めて触るので何から手を付けていいかわからない状態でした。

- IAM
    - ロールとポリシーが何にせよ必要
    - AWS内のサービス間で連携する際はこの辺の設定ちゃんとしないと繋がらない
- AWS CLI
    - 各サービスの作成、編集をCLIで実施する際に使用するコンソール
    - 画面ぽちぽちでできないことをする時はコマンドで実施する必要あり
    - kubectlの実行もここから
    - 今回はブラウザ上でやったけど、デスクトップアプリでもできるらしい
- Kinesis
    - データ収集、分析のために利用するAWSサービス
    - Kinesisの中でも複数のサービスがあり、今回使用したData Firehose以外にData Streams、Video Streams、Data Analyticsがある
    - 今回は即時性が不要 かつ S3に溜め込むだけという用途だったため、Firehoseを採用

## EKS(k8s)がわからない

AzureのAKSを使用した経験はあったもののk8sの経験自体がほとんどなく、ネットで調べた情報をつなぎ合わせてマニフェストを作り上げた状態でした。

継続調査

- ServiceAccount
- ClusterRole
- ClusterRoleBinding
- spec.template.metadata.annotaionsのiam.amazonaws.com/role


## fluentd（fluent-bit）がわからない

fluent.confの書き方がわからず、問題が発生した際にどうメンテナンスをしてよいかがわかりませんでした。
また、そもそもの動作原理を知らないために各種設定値の意味と結びつかないのも課題でした。

継続調査

- 環境変数の使い方
- 性能改善のための各種設定値
- parse, filterの機能、設定方法
- プラグインの使い方
- fluent-bitで同じことをする場合の書き方

## エラーによるデータ欠損

アプリケーションをデプロイした直後は動作が不安定になるのか、ログの出力が急激かつ局所的に増えてKinesisのLimitを超えてしまい、リトライ回数上限以内に送りきれずエラーとなってログが欠損してしまう事象が発生しました。

解決方法：`retries_on_batch_request`をデフォルトの8から20に設定

# リンク

- [Fluentd Documentation](https://docs.fluentd.org/)
- [Fluent plugin for Amazon Kinesis](https://github.com/awslabs/aws-fluent-plugin-kinesis)
- [Fluentd Daemonset for Kubernetes](https://github.com/fluent/fluentd-kubernetes-daemonset)
- [Fluent Bit Documentation](https://docs.fluentbit.io/manual/)
- [Fluent Bit Plugin for Amazon Kinesis Firehose](https://github.com/aws/amazon-kinesis-firehose-for-fluent-bit)