ブログ

Kubernetes + Fluentd + CloudWatch Logs

ソフトウェアエンジニアのskirinoです。
最近ではコンテナ化したアプリケーションの設定の管理・ライフサイクルの管理にkubernetes (以下k8s)を使うことが多いと思います。御多分に洩れずFLYWHEELでもk8sでアプリケーションを稼働させる事例が増えています。
K8sで動かしているアプリケーションの問題調査などの際にはコンテナが出力したログを見ることになるわけですが、 kubectl logs コマンドで取得できる範囲の直近のログだけではなく古いログを保存しておくには、ログ管理サービスに放り込むのが手っ取り早い方法です。この記事ではAWSのログ管理サービスであるCloudWatch Logsを使う場合の設定例を紹介しようと思います。

要件

中身に入る前に、ここで紹介する設定を行うことで実現したいことを整理してみます。

  • ネットワークの一時的な障害や、ログ管理サービス側の障害など、一時的にログを転送できない状態になった場合にもログを失わないでほしい。一時的な障害から復旧したときには、転送できずにいたログを送ってほしい。
  • 全コンテナのログを扱ってほしい。コンテナが増減したり、k8s workersが増減したりした場合に逐一設定を変更したくはないので、自動的に追従してほしい。
  • k8sではいろんなコンテナを稼働することができるが、様々なコンテナのログが適切に整理された状態で保存されてほしい。どこにログがあるかがわかりやすいだけでなく、ログ管理サービスの機能を活用しやすくなる。

構成

上記の要件を実現する方法にもいろいろあるとは思いますが、今回は以下のような構成にしました。と言っても、ほとんどfluentd-kubernetes-daemonsetを使うと言っているだけなのですが。

(fluentdやk8s daemonsetについてはこの記事では紹介しません)
fluentd-kubernetes-daemonsetを単にそのまま使うだけではなく、多少の工夫をしているのは以下の点です。

  • ログの保存先:
    • CloudWatch Logsではロググループ・ログストリームという管理区分があり、ロググループが複数のログストリームを包含する関係になっている。CloudWatch Logs InsightログのSubscriptionなどはロググループに対して動作するようになっているので、各コンテナの種類ごとにロググループを作るのがよい(異なるコンテナのログを1つのロググループに混ぜるといちいち出どころを意識しなければならず面倒)。
    • ところが、fluentd-kubernetes-daemonsetのデフォルトでは1つのロググループに全コンテナのログが入ってしまって望ましくない。自前のfluentd設定でカスタマイズする。

fluent.conf

fluentd-kubernetes-daemonsetはdocker imageを提供してくれていて、このimageに含まれる /fluentd/etc/*.conf が使われるようになっています。が、 fluent.conf を見ると`LOG_GROUP_NAME`環境変数で定まる1つのlog groupに全コンテナのログを転送するようになっていることがわかります。
(ここで貼ったリンク先はerb templateになっています。render後のファイルを見るには docker pull ${image} して docker run -it –entrypoint bash ${image} するのが手っ取り早そうです)。
さて、ロググループの指定方法を変えるため、自前の fluent.conf ファイルを作ることにしましょう。コンテナの種類ごとにロググループを作るようにしたいわけですが、ログ送信に使われているout_cloudwatch_logs pluginの設定項目から log_group_name_key を使えば動的にロググループ名を指定できることがわかります。ロググループ名にそのまま使えそうなkeyがすでにrecord内に存在するわけではないので、filter_record_transformer pluginでrecordにkeyを追加する処理を挟むことにします。その際にはそこそこややこしい操作でロググループ名の文字列を構築することになるので、enable_rubyオプションも動員することにしましょう。
ロググループを構築する材料となるコンテナ・podの情報は、fluentd-kubernetes-daemonsetのデフォルト設定でも使われているように、filter_kubernetes_metadata pluginでrecordに付与します。(dockerが作るログファイルのpathがtagとして入っていて、ここに同種の情報があるのでこれを使う手も考えられますが、kubernetes_metadataが足してくれる情報のほうが要素が分割済みで扱いやすい状態になっています)
以下、 fluent.conf ファイルのk8sで動かすコンテナのログを扱う部分だけを抜き出しています。他の部分はデフォルトでイメージに入っている設定内容を並べればOKです。

@type tail
  @id in_tail_container_logs
  @label @containers
  path /var/log/containers/*.log
  pos_file /var/log/fluentd-containers.log.pos
  tag kubernetes.*
  read_from_head true
    @type json
    time_format %Y-%m-%dT%H:%M:%S.%NZ

    @type kubernetes_metadata
    @id filter_kube_metadata
    @type record_transformer
    @id filter_container_stream_transformer
    enable_ruby true
      log_group k8s_#{ENV['CLUSTER_NAME']}_${record["kubernetes"]["namespace_name"]}_${(record["kubernetes"]["labels"] || {}).values_at("app", "k8s-app").compact.first || record["kubernetes"]["pod_name"]}_${record["kubernetes"]["container_name"]}
      log_stream ${record["kubernetes"]["pod_name"]}_${record["kubernetes"]["container_name"]}
      namespace ${record["kubernetes"]["namespace_name"]}
      pod_name ${record["kubernetes"]["pod_name"]}
      container_name ${record["kubernetes"]["container_name"]}
      container_image ${record["kubernetes"]["container_image"]}
      host ${record["kubernetes"]["host"]}
remove_keys time,stream,docker,kubernetes
@type cloudwatch_logs
@id out_cloudwatch_logs_containers
region "#{ENV['AWS_REGION']}"
json_handler yajl # To avoid UndefinedConversionError
log_group_name_key log_group
log_stream_name_key log_stream
auto_create_stream true
remove_log_group_name_key true
remove_log_stream_name_key true
  queued_chunks_limit_size 32
  retry_forever true

要点は以下:

  • Container runtimeが生成する /var/log/containers/*.log を取り込むようにsource pluginを設定。コンテナ以外のログと扱いを分けるため、コンテナログは kubernetes.* でタグ付けする。
  •  enable_ruby true により、 ${…} はrubyの式として評価される。
  • log_groupは以下の5要素を`_`区切りでつなげたものになる。
    • 固定文字列の“k8”。
    • 環境変数 CLUSTER_NAME の値。
    • コンテナのk8s namespace
    • コンテナが属するpodの app または k8s-app というlabelの値。どちらもなければpod名。ここで使用するために、k8s manifestでlabelをつけるようにしておく。
    • コンテナ名。
  • その他にもkubernetes_metadata pluginが付与した情報をflatな形に変換してある。全般的に見苦しくなってしまっているので、 record[“kubernetes”] の値を変数にバインドしたりしたいところだが、evalする側の変数スコープを汚したり処理順序に依存するのは気が引けるので、この形になっている。
  • record_transformerの最後で、いまのところ使うアテのないデータを取り除いている。
  • out_cloudwatch_logs pluginでは、log_group_name_key等を指定。

Kubernetes manifest

fluentd-kubernetes-daemonsetはdocker imageのみならずk8s manifestも提供してくれているので、これをベースにしましょう。なおFLYWHEELではk8s manifestをjsonnetで記述し、k8s API serverへ送りつけるときにYAMLへ変換する方式を取っています。小さい例であって大した意味はないのですが、以下でもjsonnetで書いていくことにします。
上で作った fluent.conf ConfigMapとして登録します。 fluent.conf ファイルが同じディレクトリにある前提になっています。

{
  apiVersion: 'v1',
  kind: 'ConfigMap',
  metadata: {
    namespace: 'kube-system',
    name: 'fluent-conf',
  },
  data: {
    'fluent.conf': importstr 'fluent.conf',
  }
}

デフォルトでdocker imageに含まれている *.conf ではなく、このconfigmapの中身が読み込まれるようにしたいわけですが、これにはfluentd podの /fluentd/etc にマウントすることで元のディレクトリを置き換えてしまえばいいでしょう。つまり、fluentd daemonsetのpod templateの volumes

{
  name: 'fluent-conf',
  configMap: {
    name: 'fluent-conf',
  }, 
},

を足し、かつ、fluentdコンテナの volumeMounts

{
  name: 'fluent-conf',
  mountPath: '/fluentd/etc', 
},

を足します。
また、環境変数 AWS_REGION CLUSTER_NAME (ロググループ名の一部として使われる)を設定します。fluentd-kubernetes-daemonsetのmanifestのうち他の部分については、kubernetes_metadata pluginが情報を取得できるように、 fluentd というClusterRoleが設定されていること一応認識しておきましょう。
加えて、ロググループをカスタマイズするためにfluent.confから参照していたlabel( app  or  k8s-app )がすべてのpodにつくようにしておけばOKです。

終わりに

以上、k8s daemonsetとしてfluentdを動かしてコンテナログをCloudWatch Logsへ転送する設定例、特にロググループの設定について紹介しました。かなりニッチな内容のような気もしますが、どなたかの参考になれば幸いです。