AWS EKSでPodからログを送信する場合、Container Insightsを組み込んでFluentdかFluent Bitを利用するのが一般的と思われる。そしてFluent BitよりFluentdの方がメジャーなのでまずはそこから入る事例が多いと想像する。

しかし、元々組み込みLinux用に開発されて軽量リソースで動作するFluent Bitの方がコンテナログ送信に向いていると思う。ということで、この記事ではFluent Bitに焦点を当てる。

 

参照
Fluent Bit ドキュメント(設定詳細は画面左「DATA PIPELINE」配下のメニュー参照)
Fluent Bit Documentation

Container Insights全般
Amazon EKS と Kubernetes での Container Insights のセットアップ

Fluent Bit on Container Insights
CloudWatch Logs へログを送信する DaemonSet として Fluent Bit を設定する

サンプルマニフェスト
fluent-bit-compatible.yaml
※AWSがサンプルとして提供しているFluent BitのマニフェストはFluent Bit最適化用とFluentd互換用がある。今回は過去にFluentd使用事例があることから、Fluentd互換用マニフェストをDLしてカスタマイズした。

 

共通マニフェスト例

クラスタの全般的な設定と、アプリ個別のケースでマニフェストを二つに分けた。AWS公式では基本となるEKSクラスタの定義をコマンドでセットしているが、運用の際はマニフェストに落とし込むのが普通だと思う。以下のようなマニフェストを共通用として作成し、個別のマニフェストから参照させる。EKSクラスタ名はdata:cluster.nameで指定している。

fluentbit-cluster.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentbit-cluster
  namespace: amazon-cloudwatch
  selfLink: /api/v1/namespaces/amazone-cloudwatch/configmaps/fluentbit-cluster
data:
  cluster.name: EKS-SAMPLE-CLUSTER
  log.region: ap-northeast-1
  read.head: "On"
  read.tail: "Off"
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluent-bit
  namespace: amazon-cloudwatch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluent-bit-role
rules:
  - nonResourceURLs:
      - /metrics
    verbs:
      - get
  - apiGroups: [""]
    resources:
      - namespaces
      - pods
      - pods/logs
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: fluent-bit-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: fluent-bit-role
subjects:
  - kind: ServiceAccount
    name: fluent-bit
    namespace: amazon-cloudwatch

 

個別マニフェスト例

「個別のマニフェスト」というのはアプリの種類が複数存在して、各種別ごとに送信先(ロググループ/ログストリーム)を振り分けたいケースを想定している。しかしAWS公式サンプルをそのまま使うと要件的に期待値にならない。現状ネットにわかりやすい事例がなく大分迷ったが、最終的に以下のような形に落とした。詳細は後述。

最初に骨組みを説明すると、前半がFluent Bitの設定であるConfigMap、後半がワーカーノード上で起動するDaemonSetの定義となっている。冒頭の[SERVICE]で全体共通の設定を行う。@INCLUDEで3種類のConfig名を指定しているが名称は適当でよい。各Config内に[INPUT] [FILTER] [OUTPUT] を定義していく。

 

confの種類

containers.conf
fluentbit, cloudwatch-agentやアプリ個別ログを定義。簡素化のため対象を絞っているが、aws-node, kube-proxy, corednsのログを送信する場合もここに含める。

kube-systemd.conf
docker,kubeletのログを定義。

host.conf
OS上のログ(基本的に/var/log/配下の各種ログ)を定義。簡素化のためここではmessagesのみ定義している。他に送信したい種別は同様に設定する。

parsers.conf
ログフォーマットのパースの定義

 

fluentbit-sample-app.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
  namespace: amazon-cloudwatch
  labels:
    k8s-app: fluent-bit-sample
data:
  fluent-bit.conf: |
    [SERVICE]
        Flush                     5
        Log_Level                 info
        Daemon                    off
        Parsers_File              parsers.conf
        storage.path              /var/fluent-bit/state/flb-storage/
        storage.sync              normal
        storage.checksum          off
        storage.backlog.mem_limit 5M
        
    @INCLUDE containers.conf
    @INCLUDE kube-systemd.conf
    @INCLUDE host.conf    

# containers.confの定義

  containers.conf: |
    [INPUT]
        Name                tail
        Tag                 fluentbit.*
        Path                /var/log/containers/fluentbit*
        Parser              docker
        DB                  /var/fluent-bit/state/flb_log.db
        Mem_Buf_Limit       5MB
        Skip_Long_Lines     On
        Refresh_Interval    10
        Read_from_Head      ${READ_FROM_HEAD}

    [INPUT]
        Name                tail
        Tag                 cloudwatch-agent.*
        Path                /var/log/containers/cloudwatch-agent*
        Docker_Mode         On
        Docker_Mode_Flush   5
        Docker_Mode_Parser  cwagent_firstline
        Parser              docker
        DB                  /var/fluent-bit/state/flb_cwagent.db
        Mem_Buf_Limit       5MB
        Skip_Long_Lines     On
        Refresh_Interval    10
        Read_from_Head      ${READ_FROM_HEAD}

    [INPUT]
        Name                tail
        Tag                 sample-app.*
        Path                /var/log/containers/sample-app*
        Parser              docker
        DB                  /var/fluent-bit/state/flb_sample-app.db
        Mem_Buf_Limit       5MB
        Skip_Long_Lines     On
        Refresh_Interval    10
        Read_from_Head      ${READ_FROM_HEAD}    

# 各INPUTに対応するFILTERを定義する

    [FILTER]
        Name                kubernetes
        Match               fluentbit.*
        Kube_URL            ${MASTER_URL}
        Kube_Tag_Prefix     fluentbit.var.log.containers.
        Merge_Log           On
        Merge_Log_Key       log_processed
        K8S-Logging.Parser  On
        K8S-Logging.Exclude Off
        Annotations         Off

    [FILTER]
        Name                kubernetes
        Match               cloudwatch-agent.*
        Kube_URL            ${MASTER_URL}
        Kube_Tag_Prefix     cloudwatch-agent.var.log.containers.
        Merge_Log           On
        Merge_Log_Key       log_processed
        K8S-Logging.Parser  On
        K8S-Logging.Exclude Off
        Annotations         Off

    [FILTER]
        Name                kubernetes
        Match               sample-app.*
        Kube_URL            ${MASTER_URL}
        Kube_Tag_Prefix     sample-app.var.log.containers.
        Merge_Log           On
        Merge_Log_Key       log_processed
        K8S-Logging.Parser  On
        K8S-Logging.Exclude Off
        Annotations         Off

# アイテムの変換や不要なメタデータ送信抑止を定義

    [FILTER]
        Name                nest
        Match               *
        Operation           lift
        Nested_under        kubernetes
        Add_prefix          Nested.

    [FILTER]
        Name                modify
        Match               *
        Rename              Nested.docker_id            Docker.container_id
    
    [FILTER]
        Name                nest
        Match               *
        Operation           nest
        Wildcard            Nested.*
        Nested_under        kubernetes
        Remove_prefix       Nested.
    
    [FILTER]
        Name                nest
        Match               *
        Operation           nest
        Wildcard            Docker.*
        Nested_under        docker
        Remove_prefix       Docker.

    [FILTER]
        Name                nest
        Match               *
        Operation           lift
        Nested_under        kubernetes
        Add_prefix          Kube.

    [FILTER]
        Name                modify
        Match               *
        Remove              Kube.container_hash 
        Remove              Kube.container_image
        Remove              Kube.pod_id

    [FILTER]
        Name                nest
        Match               *
        Operation           nest
        Wildcard            Kube.*
        Nested_under        Kubernetes
        Remove_prefix       Kube.

# 送信時の定義
# ロググループ名例:/eks/stg/sample-app_fluentbit
# ログストリーム名例:ip-10-1-2-3.ap-northeast-1.compute.internal_[Pod名]_[ネームスペース]_[コンテナ名]
# $(tag[4])とした場合、上記のようなkubeのタグ定義が投入され、ユニークなログストリーム名になる。

    [OUTPUT]
        Name                cloudwatch
        Match               fluentbit.*
        region              ${AWS_REGION}
        log_group_name      /eks/${ENVIRONMENT}/${NODEGROUP}_fluentbit
        log_stream_name     ${HOST_NODE_NAME}_$(tag[4])
        auto_create_group   true
        extra_user_agent    container-insights
        Retry_Limit         5

    [OUTPUT]
        Name                cloudwatch
        Match               cloudwatch-agent.*
        region              ${AWS_REGION}
        log_group_name      /eks/${ENVIRONMENT}/${NODEGROUP}_cwagent
        log_stream_name     ${HOST_NODE_NAME}_$(tag[4])
        auto_create_group   true
        extra_user_agent    container-insights
        Retry_Limit         5

# 個別アプリログ送信用定義

    [OUTPUT]
        Name                cloudwatch
        Match               sample-app.*
        region              ${AWS_REGION}
        log_group_name      /eks/${ENVIRONMENT}/${NODEGROUP}_application
        log_stream_name     ${HOST_NODE_NAME}_$(tag[4])
        auto_create_group   true
        extra_user_agent    container-insights
        Retry_Limit         5

# docker,kubenetesログ定義

  kube-systemd.conf: |
    [INPUT]
        Name                systemd
        Tag                 dockerlog.systemd.*
        Systemd_Filter      _SYSTEMD_UNIT=docker.service
        DB                  /var/fluent-bit/state/systemd.db
        Path                /var/log/journal
        Read_From_Head      ${READ_FROM_HEAD}

    [INPUT]
        Name                systemd
        Tag                 kubelet.systemd.*
        Systemd_Filter      _SYSTEMD_UNIT=kubelet.service
        DB                  /var/fluent-bit/state/systemd.db
        Path                /var/log/journal
        Read_From_Head      ${READ_FROM_HEAD}

    [FILTER]
        Name                modify
        Match               dockerlog.systemd.*
        Rename              _HOSTNAME                   hostname
        Rename              _SYSTEMD_UNIT               systemd_unit
        Rename              MESSAGE                     message
        Remove_regex        ^((?!hostname|systemd_unit|message).)*$

    [FILTER]
        Name                modify
        Match               kubelet.systemd.*
        Rename              _HOSTNAME                   hostname
        Rename              _SYSTEMD_UNIT               systemd_unit
        Rename              MESSAGE                     message
        Remove_regex        ^((?!hostname|systemd_unit|message).)*$

    [OUTPUT]
        Name                cloudwatch
        Match               kubelet.systemd.*
        region              ${AWS_REGION}
        log_group_name      /eks/${ENVIRONMENT}/${NODEGROUP}_docker
        log_stream_name     ${HOST_NODE_NAME}_$(tag[2])
        auto_create_group   true
        extra_user_agent    container-insight

    [OUTPUT]
        Name                cloudwatch
        Match               kubelet.systemd.*
        region              ${AWS_REGION}
        log_group_name      /eks/${ENVIRONMENT}/${NODEGROUP}_kubelet
        log_stream_name     ${HOST_NODE_NAME}_$(tag[2])
        auto_create_group   true
        extra_user_agent    container-insight    

# messages等、OSのログ定義

  host-log.conf: |

    [INPUT]
        Name                tail
        Tag                 host.messages
        Path                /var/log/messages
        Parser              syslog
        DB                  /var/fluent-bit/state/flb_messages.db
        Mem_Buf_Limit       5MB
        Skip_Long_Lines     On
        Refresh_Interval    10
        Read_from_Head      ${READ_FROM_HEAD}

# host.confの[OUTPUT]定義は、複数のINPUTがあっても1つでよい。
# $(tag[1]) にはこの場合messagesが入る。

    [OUTPUT]
        Name                cloudwatch
        Match               host.*
        region              ${AWS_REGION}
        log_group_name      /eks/${ENVIRONMENT}/${NODEGROUP}_$(tag[1])
        log_stream_name     ${HOST_NODE_NAME}_$(tag[1])
        auto_create_group   true
        extra_user_agent    container-insights
        Retry _Limit        5

  parsers.conf: |
    [PARSER]
        Name                docker
        Format              json
        Time_Key            time
        Time_Format         %Y-%m-%dT%H:%M:%S.%LZ

    [PARSER]
        Name                syslog-rfc5424
        Format              regex
        Regex               ^(?<time>[^ ]* {1.2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_¥/¥.¥-]*) (?:¥[?<pid>[-0-9]+)¥])?(?:[^¥:]*¥:)? * (?<message>.*)$
        Time_Key            time
        Time_Format         %Y-%m-%dT%H:%M:%S.%L
        Time_Strict         Off

    [PARSER]
        Name                container_firstline
        Format              regex
        Regex               (?<log>(?<="log":")\S(?!\.).*?)(?<!\\)".*(?<stream>(?<="stream":").*?)".*(?<time>\d{4}-\d{1,2}-\d{1,2}T\d{2}:\d{2}:\d{2}\.\w*).*(?=})
        Time_Key            time
        Time_Format         %Y-%m-%dT%H:%M:%S.%LZ

    [PARSER]
        Name                cwagent_firstline
        Format              regex
        Regex               (?<log>(?<="log":")\d{4}[\/-]\d{1,2}[\/-]\d{1,2}[ T]\d{2}:\d{2}:\d{2}(?!\.).*?)(?<!\\)".*(?<stream>(?<="stream":").*?)".*(?<time>\d{4}-\d{1,2}-\d{1,2}T\d{2}:\d{2}:\d{2}\.\w*).*(?=})
        Time_Key            time
        Time_Format         %Y-%m-%dT%H:%M:%S.%LZ    
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluent-bit-sample
  namespace: amazon-cloudwatch
  labels:
    k8s-app: fluent-bit-sample
    version: v1
    kubernetes.io/cluster-service: "true"
spec:
  selector:
    matchLabels:
      k8s-app: fluent-bit-sample
  template:
    metadata:
      labels:
        k8s-app: fluent-bit-sample
        version: v1
        kubernetes.io/cluster-service: "true"
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: nodelabel
                operator: In
                values:
                - STG-NODELABEL-APP-001
      containers:
      - name: fluent-bit-sample
        image: amazon/aws-for-fluent-bit:2.12.0
        imagePullPolicy: Always
        env:
            - name: AWS_REGION
              valueFrom:
                configMapKeyRef:
                  name: fluentbit-cluster
                  key: logs.region
            - name: CLUSTER_NAME
              valueFrom:
                configMapKeyRef:
                  name: fluentbit-cluster
                  key: cluster.name
            - name: READ_FROM_HEAD
              valueFrom:
                configMapKeyRef:
                  name: fluentbit-cluster
                  key: read.head
            - name: READ_FROM_TAIL
              valueFrom:
                configMapKeyRef:
                  name: fluentbit-cluster
                  key: read.tail
            - name: HOST_NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: CI_VERSION
              value: "k8s/1.3.8"

# これ以降独自に設定追加。設定項目はユースケースに合わせてください。
            - name: ENVIRONMENT
              value: "stg"
            - name: NODEGROUP
              value: "sample-app"
            - name: MASTER_URL
              value: "https://kubernetes.default.svc:443"
        resources:
            limits:
              cpu: 200m
              memory: 200Mi
            requests:
              cpu: 200m
              memory: 100Mi
        volumeMounts:
        # Please don't change below read-only permissions
        - name: fluentbitstate
          mountPath: /var/fluent-bit/state
        - name: varlog
          mountPath: /var/log
          readOnly: true
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
        - name: fluent-bit-config
          mountPath: /fluent-bit/etc/
        - name: runlogjournal
          mountPath: /run/log/journal
          readOnly: true
        - name: dmesg
          mountPath: /var/log/dmesg
          readOnly: true
      terminationGracePeriodSeconds: 90
      volumes:
      - name: fluentbitstate
        hostPath:
          path: /var/fluent-bit/state
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: fluent-bit-config
        configMap:
          name: fluent-bit-config
      - name: runlogjournal
        hostPath:
          path: /run/log/journal
      - name: dmesg
        hostPath:
          path: /var/log/dmesg
      serviceAccountName: fluent-bit
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      - operator: "Exists"
        effect: "NoExecute"
      - operator: "Exists"
        effect: "NoSchedule"

 

補足説明

imageパスの指定
上記ではコンテナイメージをインターネットを通って都度落とすようになっているが、業務利用時はECRに格納してプライベートな通信で完結させるのが望ましい。ECRに格納した場合は以下のように指定する。

image: [AWS-AccoundID].dkr.ecr.ap-northeast-1.amazon.com/[repo-name]:2.12.0

 

nodeAffinityで起動するノードを指定
今回の事例では、sample-appのPodが起動するワーカーノード上にsample-appログ送信用のDaemonSetを起動させる必要がある。そのためnodeAffinityを定義する。EKSノードグループのラベルにkey:nodelabel values:STG-SAMPLE-APPを設定している前提の場合以下の様になる。sample-app用のマニフェストにも同様の記述をする。

    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: nodelabel
                operator: In
                values:
                - STG-SAMPLE-APP

 

停止時のGracePeriod
DaemonSetがKillシグナル受信後に削除されるまでの猶予時間を指定。公式サンプル10秒だと、コンテナログを送信しきる前に削除されてしまう可能性がある。ここでは余裕を持たせて90秒。

terminationGracePeriodSeconds: 90

 

syslogのPARSER
AWS公式の設定だとTime_FormatがダメらしきエラーがでるのでFluent Bit公式の例を適用した。Fluent Bit公式ではsyslogではなくsyslog-rfc5424。ではINPUTのParsersyslogではなくsyslog-rfc5424とするのが正ではないか?と思ったが、そうすると動作しない。謎だが深追いはしない。Time_StrictはOffにしておく。しかしそれでもまだエラーになるので調べると、Regexの正規表現が原因らしいのでそこも直した。参照サイトは失念。

 

DaemonSetの起動〜ログ送信
今回cloudwatch-agentについては触れていないが、cloudwatch-agentネームスペースが存在している状態でマニフェストをapplyする。この時点ではノードグループは停止中でもよい。

$ kubectl apply -f fluentbit-cluster.yaml
$ kubectl apply -f fluentbit-sample-app.yaml

 

ノードグループが起動すると、各種ログがCloudWatchLogsに送信される。アプリ用マニフェストをapplyすればアプリログも送信される。

あとresourceのcpuはデフォルトが500mになっていたが、そこまで割り当てなくてもちゃんと動作する。よほどのことがなければ100mでもいい気がする。メモリもFlunetdに比べて全然余裕。さすが軽量版。その他細かいチューニング項目もあるにはあるのだが、これ以上の長文は避けたいのでまた別の機会に投稿しようと思う。