ソフトウェアエンジニアの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です。

要点は以下:

  • 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.confConfigMapとして登録します。fluent.confファイルが同じディレクトリにある前提になっています。

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

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

を足します。

また、環境変数AWS_REGIONCLUSTER_NAME(ロググループ名の一部として使われる)を設定します。fluentd-kubernetes-daemonsetのmanifestのうち他の部分については、kubernetes_metadata pluginが情報を取得できるように、fluentdというClusterRoleが設定されていること一応認識しておきましょう。

加えて、ロググループをカスタマイズするためにfluent.confから参照していたlabel(app or k8s-app)がすべてのpodにつくようにしておけばOKです。

終わりに

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