Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Kubernetes上でのFluentdを使ったログ収集について

こんにちは。ご機嫌いかがでしょうか? SREチームの栗山(id:shepherdMaster)です。 弊社ではKubernetesを導入するために着々と準備を進めております。 どんなシステム上でアプリケーションを動かすにせよ、ログ収集は必要になってきます。
Kubernetes上でログ収集をするために色々調べましたが実用的な情報があまり豊富ではなかったので、今回はKubernetes上でのログ収集、特にFluentdの設定について共有をしたいと思います。
なおまだ実運用は開始してないので今後細かい部分は変わるかもしれません。

ログ収集&ログ分析の構成

構成は以下にしました。

Fluentd + S3 + Amazon Athena

理由は以下です。

  • S3に保存すると非常に安い
  • SQLでログを検索できるのは非常に便利
  • Fluentdの設定の柔軟性
  • 既存のログ収集基盤がFluentd + S3 + Amazon Athenaになっていたため、資産の流用ができ、学習コストや管理コストも抑えられる

ログ収集ツールとしてはより軽量なFluent Bitも考えましたが、S3に保存するためのoutput pluginがなかったので諦めました。

Kubernetes上でのログ取集の課題

Kubernetes上でのログ収集を進めていくうちに課題がいくつか出てきました。
まずログ収集の全体の流れですが、コンテナの標準出力/標準エラー出力結果がホストマシンの/var/log/containers以下にファイル出力されます。そしてFluentdがそのファイルをtailし、S3に保存する流れになります。

このとき、/var/log/containers以下出力されるファイルに難があります。 具体的に/var/log/containers以下にあるログを見てみましょう。

{
  "log": "time:2020-03-11T11:09:55+00:00\thost:10.3.48.x\tvhost:health-check.studyplus.jp\tserver:deployment-7f94cc5958-bgs7g\treq:GET /health_check HTTP/1.1\turi:/health_check\tstatus:200\tmethod:GET\treferer:-\tua:kube-probe/1.14+\treq_time:0.011\tapp_revision:-\truntime:0.009058\tcache:-\tapp_time:0.008\trequest_id:9967a0a0a6486435accf64b495da67b0\tx_request_id:-\tres_size:0\treq_size:177\n",
  "stream": "stdout",
  "time": "2020-03-11T11:09:55.217300042Z"
}

logの部分にコンテナが出力したログが入っています。 ちなみに上記はnginxのログですが、nginxはLTSV形式でログ出力するようにしています。なので\tという文字がところどころ入っています。また厄介なことに末尾に\nが入ってます。
streamにはstdout(標準出力)かstderr(標準エラー)かが入ります。

このことから、

  • コンテナが出力したログをS3に保存するためには、JSONのlog値を取り出さないといけない。
  • log値の末尾の\nを削除する必要がある(じゃないとJSONとしてはinvalid扱いになる)
  • ログの中を見ないと標準出力ログなのか、標準エラー出力ログなのか分からない。

ということが分かります。 つまり、単純に /var/log/containers以下のログをそのままS3に放り込んでもAthenaでは、log値をlike検索するくらいしかできません。 それだと調査のときにログを柔軟に絞り込むことが出来なくて困るので、ログが適切な形でS3に保存されるようにFluentdの設定をする必要があります。

最終的にやりたいことを整理すると、

  • コンテナごとに保存先を変えたい
  • 標準出力ログ、標準エラーログで、保存先を変えたい
    • なぜかというとnginxはエラーログに対してログフォーマットを指定できないので、保存先を変えてエラーログはAthenaから通常のログとは別に検索したい
  • コンテナが出力したログをそのまま保存したい

FluentdをDaemonSetで動かすには

Fluentdの設定ファイルの前にまずFluentdをDaemonSetで動かします。
https://github.com/fluent/fluentd-kubernetes-daemonset で提供されているものを使うと比較的楽にDaemonSetで動かすことができます。
https://github.com/fluent/fluentd-kubernetes-daemonset/tree/master/docker-image 以下にバージョンごとにディレクトリがありますが、さらにそのディレクトリ以下をみると出力先に応じたimageが用意されています。 S3に保存したいので、debian-s3を選びました。

以下が具体的なマニフェストファイルの内容です。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-daemonset
spec:
  selector:
    matchLabels:
      name: fluentd-pod
  template:
    metadata:
      labels:
        name: fluentd-pod
    spec:
      containers:
        - name: fluentd-container
          image: fluent/fluentd-kubernetes-daemonset:v1.8-debian-s3-1
          env:
            - name: S3_BUCKET_NAME
              value: <バケット名>
            - name: FLUENTD_SYSTEMD_CONF
              value: disable
          resources:
            requests:
              cpu: 200m
              memory: 500Mi
            limits:
              memory: 1Gi
          volumeMounts:
            - name: config-volume
              mountPath: /fluentd/etc
            - name: varlog
              mountPath: /var/log
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
      volumes:
        - name: config-volume
          configMap:
            name: fluentd-configmap
        - name: varlog
          hostPath:
            path: /var/log
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers

独自のfluent.confを配置するために、ConfigMapを使ってvolumeマウントをしています。こうすることで、/fluentd/etc/以下にfluent.confが配置されます。 そうしないと https://github.com/fluent/fluentd-kubernetes-daemonset/tree/master/docker-image/v1.9/debian-s3/conf 以下にあるfluent.confが使用されます。

/var/logと/var/lib/docker/containersの両方をvolumeマウントしている理由は、/var/log以下のログは/var/lib/docker/containers以下のログにシンボリックリンクがはられているので両方マウントしないとログが読み込めないためです。

FLUENTD_SYSTEMD_CONFをdisableにしているのはここにあるように不要なログを出力しないためです。

fluent.confの設定

実際には、RailsのログやFluentdのログも処理するように設定を書いてますが、今回は話をシンプルにするためにnginxのログ設定のみを書いてます。

# /var/log/containers以下には様々なコンテナのログが保存されているので、pathで該当のコンテナのログを指定します。
<source>
  @type tail
  path "/var/log/containers/*_nginx-container-*.log"
  pos_file /var/log/nginx-container.log.pos
  tag nginx
  read_from_head true
  <parse>
    @type json
    time_format %Y-%m-%dT%H:%M:%S.%NZ
  </parse>
</source>

# logの値の末尾に\nがつくので削除する
<filter nginx>
  @type record_transformer
  enable_ruby
  <record>
    log ${record["log"].strip}
  </record>
</filter>

# 標準出力と標準エラーでtagを分ける
<match nginx>
  @type rewrite_tag_filter
  <rule>
      key     stream
      pattern /^stdout$/
      tag     "${tag}.stdout"
  </rule>
  <rule>
      key     stream
      pattern /^stderr$/
      tag     "${tag}.stderr"
  </rule>
</match>

# jsonのlog値にコンテナが出力ログが入っているのでを取り出す
<filter nginx.stdout>
  @type parser
  key_name log
  <parse>
    @type ltsv
    keep_time_key true
    types status:integer, req_time:float, runtime:float, app_time:float, res_size:integer, req_size:integer
  </parse>
</filter>

<filter nginx.stderr>
  @type parser
  key_name log
  <parse>
    @type none
  </parse>
</filter>

# S3に保存する
<match nginx.stdout>
  @type s3
  format json
  s3_bucket "#{ENV['S3_BUCKET_NAME']}"
  s3_region ap-northeast-1
  s3_object_key_format "${tag[0]}-log/%{time_slice}/${tag[0]}-%{index}.log.%{file_extension}"
  time_slice_format year=%Y/month=%m/day=%d/hour=%H
  <buffer tag,time>
    @type file
    path "/var/log/fluentd-buffers/s3.buffer"
    timekey 3600
    timekey_wait 10m
    timekey_use_utc true
    chunk_limit_size 1G
    flush_at_shutdown true
  </buffer>
</match>

<match nginx.stderr>
  @type s3
  format single_value
  s3_bucket "#{ENV['S3_BUCKET_NAME']}"
  s3_region ap-northeast-1
  s3_object_key_format "${tag[0]}-error-log/%{time_slice}/${tag[0]}-error-%{index}.log.%{file_extension}"
  time_slice_format year=%Y/month=%m/day=%d/hour=%H
  <buffer tag,time>
    @type file
    path "/var/log/fluentd-buffers/s3-error.buffer"
    timekey 3600
    timekey_wait 10m
    timekey_use_utc true
    chunk_limit_size 1G
    flush_at_shutdown true
  </buffer>
</match>

ちなみに、fluent.conf内で使えるfluentd pluginは https://github.com/fluent/fluentd-kubernetes-daemonset/blob/master/docker-image/ 以下にあるGemfile(たとえばこれ)内に定義されているものが使えます。

おまけ

Fluentdの設定ファイルを書いていると、すぐにログをflushしてS3に保存し、保存されたログファイルの中身を確認したいケースがでてきます。そういうときは、USR1 signalを送ると強制的にflushしてくれます。 たとえば以下のようなワンライナーを用意しておくと便利です。

kubectl exec `kubectl get pod -o name | grep fluentd` -- /bin/sh -c "pkill -USR1 -f fluentd"

まとめ

Kubernetes上で動くアプリケーションのログ収集のために、FluentdのDaemonSetリソースファイルとfluent.confの紹介をしました。 Dockerが出力する癖のあるログによって苦戦しましたが、Fluentdの柔軟さによって助けられました。

それでは、みなさん良きログ収集ライフを。