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のParser
はsyslog
ではなく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に比べて全然余裕。さすが軽量版。その他細かいチューニング項目もあるにはあるのだが、これ以上の長文は避けたいのでまた別の機会に投稿しようと思う。