ソフトウェアエンジニアのskirinoです。
最近ではコンテナ化したアプリケーションの設定の管理・ライフサイクルの管理にkubernetes (以下k8s)を使うことが多いと思います。御多分に洩れずFLYWHEELでもk8sでアプリケーションを稼働させる事例が増えています。
K8sで動かしているアプリケーションの問題調査などの際にはコンテナが出力したログを見ることになるわけですが、 kubectl logs コマンドで取得できる範囲の直近のログだけではなく古いログを保存しておくには、ログ管理サービスに放り込むのが手っ取り早い方法です。この記事ではAWSのログ管理サービスであるCloudWatch Logsを使う場合の設定例を紹介しようと思います。
中身に入る前に、ここで紹介する設定を行うことで実現したいことを整理してみます。
上記の要件を実現する方法にもいろいろあるとは思いますが、今回は以下のような構成にしました。と言っても、ほとんどfluentd-kubernetes-daemonsetを使うと言っているだけなのですが。
(fluentdやk8s daemonsetについてはこの記事では紹介しません)
fluentd-kubernetes-daemonsetを単にそのまま使うだけではなく、多少の工夫をしているのは以下の点です。
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
要点は以下:
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へ転送する設定例、特にロググループの設定について紹介しました。かなりニッチな内容のような気もしますが、どなたかの参考になれば幸いです。