🦆

FireLens を使用したログルーティングのあれこれ

2023/01/29に公開
2

はじめに

みなさん FireLens は使ってますか🦆
Fluent Bit 自体が強力なログルーティング機能を備えてますし、ECS との連携も非常に良くできたツールです。

FireLens のマニュアルは少し難解で、わたしのように複雑な日本語を読むのが苦手なタイプの人間にはちょっときびしいです。
実際の動きを見るために検証するとしても、ログを出力するコンテナを用意して、ECS 周りの環境を整えて、いざタスクを起動したと思ったら設定ミスで再起動を無限に繰り返してる…みたいな感じでなかなか面倒です。また、設定変更の都度コンテナを再デプロイしないといけないのも地味に苦痛です。

今回は FireLens の各種設定に基づき Fluent Bit がどんな挙動を取るか、検証した結果を踏まえてできるだけ具体的にまとめていきます。
この記事がどなたかのお役に立てば幸いです🙇

FireLens について

FireLens はメインコンテナが出力したログを Fluent Bit(もしくは Fluentd)に転送するための仕組みです。
以下のような特徴があります。

  • Fluent Bit に関する設定の一部をタスク定義から投入できる
  • 特に設定しなくてもメインコンテナの標準出力 / 標準エラー出力を FireLens で受け取ることができる
  • ECS に関するメタデータがログに自動的に追加される

これらの処理は ECS エージェントが FireLens を使用するタスクを起動したタイミングで実行されます。具体的には以下のような内容で Fluent Bit 設定ファイル(/fluent-bit/etc/fluent-bit.conf)を自動生成します。

/fluent-bit/etc/fluent-bit.conf
[INPUT]
    Name tcp
    Listen 127.0.0.1
    Port 8877
    Tag firelens-healthcheck

[INPUT]
    Name forward
    unix_path /var/run/fluent.sock

[INPUT]
    Name forward
    Listen 127.0.0.1
    Port 24224

[FILTER]
    Name record_modifier
    Match *
    Record ecs_cluster cluster-name
    Record ecs_task_arn arn:aws:ecs:region:111122223333:task/cluster-name/f2ad7dba413f45ddb4EXAMPLE
    Record ecs_task_definition task-def-name:revision

@INCLUDE /extra.conf

[OUTPUT]
    Name null
    Match firelens-healthcheck

この処理が FireLens のほぼ全てといってよく、逆にこれ以外の部分は Fluent Bit と変わりません。実際に、ログルーターとして使用する Fluent Bit コンテナは任意のものを指定可能です。FireLens は「ECS から Fluent Bit を使いやすくするためのラッパー」程度に思っておくのがよさそうです。

fluent-bit.conf に自動生成されるパラメーター

FireLens を使用する上で、この自動生成されるパラメーターについてある程度理解しておく必要があります。ひとつずつ見ていきましょう。
# [OUTPUT]はヘルスチェック用に受信したデータを破棄しているだけなので省略します。

ヘルスチェック

[INPUT]
    Name tcp
    Listen 127.0.0.1
    Port 8877
    Tag firelens-healthcheck

この定義は FireLens の TCP Input Health Check を行うためのものです。

TCP Input Health Check
"healthCheck": {
    "retries": 2,
    "command": [
        "CMD-SHELL",
        "echo '{\"health\": \"check\"}' | nc 127.0.0.1 8877 || exit 1"
    ],

以下に詳細が記載されています。

https://github.com/aws-samples/amazon-ecs-firelens-examples/tree/mainline/examples/fluent-bit/health-check#not-recommended-tcp-input-health-check

少し話はそれますが TCP Input Health Check が非推奨になっていることに注意しましょう。理由も先ほどのリンクに記載されています。

DeepL 翻訳

私たちは、すべてのユーザーに簡単にディープヘルスチェックのオプションを提供するために、ヘルスチェックのための設定を FireLens に組み込むことを選択しました。それ以来、少数のユーザーから、ヘルスチェックが、Fluent Bit が機能し続けているにもかかわらず不健康とマークされる誤検知の可能性があるようだとの報告がありました。残念ながら、AWS for Fluent Bit チームでは、これらの不具合を独自に再現したり、原因を突き止めることはできていません。

Original Text

We chose to build the config for the health check into FireLens to provide all users with an easy deep health check option. Since then, a minority of users have reported that the health check appears to be susceptible to false positives where Fluent Bit is marked as unhealthy despite continuing to function. Unfortunately, AWS for Fluent Bit team has never been able to independently reproduce or root-cause these failures.

現在はヘルスチェックを行わないか、Simple Uptime Health Check を採用することが推奨されています。Simple Uptime Health Check を行う場合は、Fluent Bit 設定ファイルに追加の定義が必要です。詳細は後述します。

Simple Uptime Health Check
"healthCheck": {
    "retries": 2,
    "command": [
        "CMD-SHELL",
        "curl -f http://127.0.0.1:2020/api/v1/uptime || exit 1"
    ],

ログの受信

[INPUT]
    Name forward
    unix_path /var/run/fluent.sock

[INPUT]
    Name forward
    Listen 127.0.0.1
    Port 24224

この定義はメインコンテナ上に出力されたログを受信するために使用されます。
FireLens は以下の 2 パターンの定義をプリセットしてくれます。

INPUT 説明
/var/run/fluent.sock メインコンテナの標準出力 / 標準エラー出力を UNIX ソケット経由で受け取る
TCP : 24224 アプリケーションの Logger ライブラリなどから TCP 経由で送られてきたログを受け取る

UNIX ソケット経由で受け取ったログには{container name}-firelens-{task ID}というタグが付与されます。

https://github.com/aws-samples/amazon-ecs-firelens-under-the-hood/blob/mainline/generated-configs/fluent-bit/

DeepL 翻訳

FireLens が作成する出力は、{container name}-firelens*というマッチパターンを持ちます。この場合、ログがこの出力に送られるコンテナはappという名前なので、マッチパターンはapp-firelens*となります。コンテナの標準出力 / エラーストリームからのログには、{container name}-firelens-{task ID}というタグが付けられます。

Original Text

Outputs created by FireLens will have the match pattern {container name}-firelens*. In this case, the container whose logs will be sent to this output was named app, so the match pattern is app-firelens*. Logs from a container's standard out / error stream will be tagged with {container name}-firelens-{task ID}.

メタデータの追加

[FILTER]
    Name record_modifier
    Match *
    Record ecs_cluster cluster-name
    Record ecs_task_arn arn:aws:ecs:region:111122223333:task/cluster-name/f2ad7dba413f45ddb4EXAMPLE
    Record ecs_task_definition task-def-name:revision

この定義はログに ECS 関連のメタデータを追加するためのものです。
FireLens が受信したすべてのログに以下の情報が追加されます。

レコード名 説明
ecs_cluster ECS クラスター名
ecs_task_arn ECS タスク ARN
ecs_task_definition ECS タスク定義 + リビジョン

実際のログはこんな感じになります。

Log Example
{
    "container_id": "11112222333344445555666677778888-123456789",
    "container_name": "nginx",
    "ecs_cluster": "cluster-name",
    "ecs_task_arn": "arn:aws:ecs:region:111122223333:task/cluster-name/11112222333344445555666677778888",
    "ecs_task_definition": "task-def-name:revision",
    "log": "{\"time\": \"27/Jan/2023:01:23:45 +0000\",\"remote_addr\": \"xxx.xxx.xxx.xxx\",\"host\": \"xxx.xxx.xxx.xxx\",\"remote_user\": \"\",\"status\": \"200\",\"server_protocol\": \"HTTP/1.1\",\"request_method\": \"GET\",\"request_uri\": \"/\",\"request\": \"GET / HTTP/1.1\",\"body_bytes_sent\": \"143\",\"request_time\": \"0.000\",\"upstream_response_time\": \"\",\"http_referer\": \"\", \"http_user_agent\": \"ELB-HealthChecker/2.0\",\"http_x_forwarded_for\": \"xxx.xxx.xxx.xxx\",\"http_x_forwarded_proto\": \"https\"}",
    "source": "stdout"
}

この辺りの情報を自動的に追加してくれるのはありがたいですね。何らかの事情でメタデータを付与されては困る場合は、タスク定義のfirelensConfigurationオブジェクトに以下の設定を記述することで無効化できます。

"firelensConfiguration":{
    "type":"fluentbit",
    "options":{
        "enable-ecs-log-metadata":"false"
    }
}

カスタム設定ファイルの読み込み

@INCLUDE /extra.conf

この定義はユーザーが作成したカスタム設定ファイルを読み込むために使用されます。カスタム設定ファイルの取り扱いは FireLens を使用する上で重要な要素ですので、次の章でもう少し詳しく見ていきましょう。

任意の Fluent Bit 設定を追加する

ここからはユーザー側で任意の Fluent Bit 設定を追加する際に必要な手順について整理します。
ユーザー側で Fluent Bit の設定値を調整する方法は、以下の2パターンです。

  • タスク定義のlogConfigurationオブジェクトにオプションを記述する
  • カスタム設定ファイルを使用する

logConfigurationオブジェクトに、出力先に応じた適切なオプションを記述することで[OUTPUT]を設定できますが、複数の宛先にログを送信するケースには対応していません。
わざわざ FireLens を使用するのにログの送信先が単一であるケースはほとんどないでしょう。そのため、ほぼすべてのケースでカスタム設定ファイルを使用することになると思います。

また、2 つの方式は組み合わせて使用できますが、管理がややこしくなるので推奨しません。[OUTPUT]に関する定義はカスタム設定ファイルに統一した方がよいです。

カスタム設定ファイルを使用するにはfirelensConfigurationオブジェクトに以下のような設定を記述します。config-file-valueには Fluent Bit コンテナ上のカスタム設定ファイルのフルパスを指定します。公式マニュアルにならって/extra.confとしていますが、ファイル名は自由に設定可能です。

"firelensConfiguration":{
    "type":"fluentbit",
    "options":{
        "config-file-type":"file",
        "config-file-value":"/extra.conf"
    }
}

ちなみにカスタム設定ファイルを Fluent Bit コンテナに格納する方法ですが、現状はコンテナのビルド時にコピーするしかなさそうです。GUI から設定できたらいいのに…。

Dockerfile
FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:latest
COPY extra.conf /extra.conf

Fluent Bit のコンテナイメージをユーザー側で管理しないといけなくなるのがややツラいところではありますが、仕方ないのであきらめましょう。カスタム設定ファイルを Git で管理して CI/CD に組み込んでしまうのがよいですね。あと、Fluent Bit 設定ファイルは環境変数が利用可能です。コンテナごとに異なるログルーティングが必要なケースはあまりないと思うので、カスタム設定ファイルは共通化しコンテナごとに異なるパラメーターは環境変数で調整するのがオススメです。

ここまでで Fluent Bit 設定ファイルに定義されるパラメーターがどうやって決定するか、ユーザーが任意のパラメーターを定義するにはどうすればいいか、という点について見てきました。すでに大分ややこしいので Fluent Bit 設定ファイルが完成するまでの流れを図示します。たぶんこんな感じになります。

Fluent Bit の設定

カスタム設定ファイルを読み込むことができたら、あとの設定は通常の Fluent Bit と同様です。
ここからはわたしが設計する際に悩んだパラメーターについて説明します。

よくあるケースのサンプル設定が以下のリポジトリにまとまっています。とても参考になるので一度は目を通しておきましょう。

https://github.com/aws-samples/amazon-ecs-firelens-examples/tree/mainline/examples/fluent-bit

[SERVICE]

[SERVICE]には Fluent Bit 全体の動作に関わる設定を記述できます。コンテナ環境で Fluent Bit を使用する場合は、以下の定義を設定しましょう。

extra.conf
[SERVICE]
    HTTP_Server On
    HTTP_Listen 127.0.0.1
    HTTP_PORT 2020
    Flush 1
    Grace 30

以下の 3 つは Simple Uptime Health Check を行う場合に必要な設定です。Fluent Bit の統計情報 HTTP サーバー機能を有効化しています。

    HTTP_Server On
    HTTP_Listen 127.0.0.1
    HTTP_PORT 2020

Flushには受け取ったログを[OUTPUT]に渡す間隔を指定します。デフォルトは 5 秒です。
コンテナのように永続ディスクが存在しない環境では、可能な限りバッファを使用せず、ログが出力されたらすぐに外部の保管先に送ってしまうことが推奨されます。そうしないと、コンテナに障害が発生したときにバッファリングされたログが欠損してしまうからです。そのため最小値の 1 秒を指定します。

Graceは Fluent Bit がSIGTERMを受け取ってから停止するまでの待機時間を指定するオプションです。デフォルトは 5 秒ですが、タスク定義のstopTimeout(デフォルト 30 秒)に合わせて設定することで、SIGKILLで強制停止されるまでの時間をログのフラッシュに使用できます。

この辺りの設計に関しては、以下の Amazon Web Services ブログが詳しいです。

https://aws.amazon.com/jp/blogs/news/under-the-hood-firelens-for-amazon-ecs-tasks/

[FILTER] - parser

FireLens を使用して Fluent Bit に届いたログは、その時点で JSON 形式の構造化されたデータになっています。これはアプリケーションが出力したログの他に、メタデータが追加されるからです。アプリケーションが出力したログはlogに格納されています。

以下のログは nginx のアクセスログを JSON 形式で出力した例です。logの内容も JSON 形式になっていますが、この時点では単なるテキストデータとして扱われています。

Log Example
{
    "container_id": "11112222333344445555666677778888-123456789",
    "container_name": "nginx",
    "ecs_cluster": "cluster-name",
    "ecs_task_arn": "arn:aws:ecs:region:111122223333:task/cluster-name/11112222333344445555666677778888",
    "ecs_task_definition": "task-def-name:revision",
    "log": "{\"time\": \"27/Jan/2023:01:23:45 +0000\",\"remote_addr\": \"xxx.xxx.xxx.xxx\",\"host\": \"xxx.xxx.xxx.xxx\",\"remote_user\": \"\",\"status\": \"200\",\"server_protocol\": \"HTTP/1.1\",\"request_method\": \"GET\",\"request_uri\": \"/\",\"request\": \"GET / HTTP/1.1\",\"body_bytes_sent\": \"143\",\"request_time\": \"0.000\",\"upstream_response_time\": \"\",\"http_referer\": \"\", \"http_user_agent\": \"ELB-HealthChecker/2.0\",\"http_x_forwarded_for\": \"xxx.xxx.xxx.xxx\",\"http_x_forwarded_proto\": \"https\"}",
    "source": "stdout"
}

収集したログに対して何らかの処理を行う場合、JSON 形式だと何かと都合がいいのでパース処理を行います。

https://docs.fluentbit.io/manual/pipeline/filters/parser

Fluent Bit でパース処理を行うには、ログの構造を正規表現で表現したパーサーが必要です。独自にパーサーを作成できますが、Fluent Bit に同梱されているパーサーが非常に多くのログをサポートしているため、ほとんどのケースでカスタマイズは不要でしょう。
Fluent Bit に同梱されているパーサーは以下のリポジトリで確認できます。

https://github.com/fluent/fluent-bit/blob/master/conf/parsers.conf

JSON をパースする場合は以下のようにすれば OK です。

extra.conf
[SERVICE]
    Parsers_File /fluent-bit/parsers/parsers.conf

[FILTER]
    Name parser
    Match *
    Key_Name log
    Parser json
    Reserve_Data True

パース後のログは以下のようになります。

Log Example
{
    "body_bytes_sent": "143",
    "container_id": "11112222333344445555666677778888-123456789",
    "container_name": "nginx",
    "ecs_cluster": "cluster-name",
    "ecs_task_arn": "arn:aws:ecs:region:111122223333:task/cluster-name/11112222333344445555666677778888",
    "ecs_task_definition": "task-def-name:revision",
    "host": "xxx.xxx.xxx.xxx",
    "http_referer": "",
    "http_user_agent": "ELB-HealthChecker/2.0",
    "http_x_forwarded_for": "",
    "http_x_forwarded_proto": "",
    "remote_addr": "xxx.xxx.xxx.xxx",
    "remote_user": "",
    "request": "GET / HTTP/1.1",
    "request_method": "GET",
    "request_time": "0.000",
    "request_uri": "/",
    "server_protocol": "HTTP/1.1",
    "source": "stdout",
    "status": "200",
    "upstream_response_time": ""
}

Reserve_Dataはパース処理で解析された結果のみを保持するオプションですが、無効にした場合、どんな結果になるのかいまいち分かりにくいです。
結論としては、以下のようにもともとlogに含まれていたデータ以外が削除されます。

extra.conf
[SERVICE]
    Parsers_File /fluent-bit/parsers/parsers.conf

[FILTER]
    Name parser
    Match *
    Key_Name log
    Parser json
    Reserve_Data False
Log Example
{
    "body_bytes_sent": "143",
    "host": "xxx.xxx.xxx.xxx",
    "http_referer": "",
    "http_user_agent": "ELB-HealthChecker/2.0",
    "http_x_forwarded_for": "",
    "http_x_forwarded_proto": "",
    "remote_addr": "xxx.xxx.xxx.xxx",
    "remote_user": "",
    "request": "GET / HTTP/1.1",
    "request_method": "GET",
    "request_time": "0.000",
    "request_uri": "/",
    "server_protocol": "HTTP/1.1",
    "status": "200",
    "upstream_response_time": ""
}

[Output] - s3

ログを S3 に送信するケースは多いと思います。シンプルにログを送るだけであれば簡単ですが、障害時の挙動やログ検索ツールとの連携まで考えると気にしないといけないパラメーターがいくつかあります。

https://docs.fluentbit.io/manual/pipeline/outputs/s3

コンテナ環境の場合、基本的には以下の推奨設定を踏襲するのがオススメです。

https://docs.fluentbit.io/manual/pipeline/outputs/s3#using-s3-without-persisted-disk

extra.conf
[OUTPUT]
    Name s3
    Match *
    region ap-northeast-1
    bucket bucket-name
    total_file_size 1M
    upload_timeout 1m
    use_put_object On
    compression gzip
    s3_key_format /$TAG/%Y/%m/%d/%H/%M/%S/$UUID.gz

S3 に保管したログの検索方法は限られます。ほとんど Athena 一択じゃないでしょうか。
Athena で検索するにはパーティショニングの考慮が必須です。詳細はここでは触れませんが、必然的にプレフィックスに日付を含む形でログを保管することになるはずです。
s3_key_formatを使用することでプレフィックスに日付を設定できますが、この時の日付は UTC 固定です。JST とは 9 時間ズレることに注意しましょう。
以下の Issue で議論されていますが、OS のタイムゾーンとは独立しているようです。実際に OS のタイムゾーンをAsia/Tokyoにしてみましたが UTC のままでした。

https://github.com/aws/aws-for-fluent-bit/issues/432

$TAG{container name}-firelens-{task ID}になっているはずです。一部の要素だけを抽出したい場合は、以下のようにs3_key_format_tag_delimitersに区切り文字を指定します。

[OUTPUT]
    Name s3
    Match *
    region ap-northeast-1
    bucket bucket-name
    total_file_size 1M
    upload_timeout 1m
    use_put_object On
    compression gzip
    s3_key_format_tag_delimiters -
    s3_key_format /$TAG/%Y/%m/%d/%H/%M/%S/$UUID.gz

各プレースホルダーに含まれる値は以下のようになります。

キー
$TAG {container name}-firelens-{task ID}
$TAG[0] {container name}
$TAG[1] firelens
$TAG[2] {task ID}

おわりに

FireLens は 2019 年に発表されて以来、非常に多くのユーザーに利用されています。
インターネット上にも参考になるナレッジが多数あるため、本記事には重複する内容も含みますが、できるだけ公式なソースを引用し、実践的な内容を記載することを心掛けました。
ぜひみなさんのログルーティング・ライフにお役立てください😇

Discussion

dehio3dehio3

ECS on Fargateにてカスタムログルーティングの実装要件がり調べていたのですが、丁寧にまとまっており、とても参考になりました!
ありがとうございます!!