FireLens(Fluent Bit)におけるタグの仕組みについて調査してみた

2023.03.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、つくぼし(tsukuboshi0755)です!

皆さんはECS(Fargate)のログドライバーにFireLensを用いた事はありますでしょうか?

FireLensを使用する事で、カスタマイズ次第で柔軟なコンテナログルーティングを実現できるため、非常に便利です。

しかし実際に触ってみると、ログルーティング設定のベースとなっているFluent Bitのタグの仕組みが、思ったより難しいと感じるかもしれません。(私も理解するのに結構苦労してます。。。)

そこで今回は、Fluent Bitにおけるタグの仕組みの観点から、FireLensを用いたログルーティングの設定方法についてまとめてみたいと思います!

Fluent Bitにおけるタグとは

そもそもFluent Bitにおけるタグとは、どのようなものでしょうか?

Fluent Bitの公式ドキュメントでは、以下の通り説明があります。

Fluent Bit に入るすべてのイベントには、タグが割り当てられます。このタグは、ルーターが通過する必要があるフィルターまたは出力フェーズを決定するために後の段階で使用される内部文字列です。 ほとんどのタグは、構成で手動で割り当てられます。タグが指定されていない場合、Fluent Bit は、そのイベントが生成された場所から入力プラグイン インスタンスの名前を割り当てます。

要するに、「Fluent Bitで、一定の条件を満たすログを、特定のサービスに転送するかどうかの判定基準として、タグが用いられる」と考えて差し支えないかと考えます。

そのためFirelensを用いたログルーティングを実現したい場合、Fluent Bitにおけるタグの理解は避けて通れないと思っています。

なお、AWSでよく見るリソースタグとは、全く別の概念なのでご注意ください。

FireLensにおけるFluent Bitタグ仕様

以下では、FireLensを使用する際に理解しておくと良さそうな、Fluent Bitのタグの仕様について解説します。

FireLensの初期タグ

FireLensを用いる場合、FireLens独自の仕様で、初めからログに対して以下の形式のタグが自動的に付与されています。

<container name>-firelens-<task ID>

AWSの公式ドキュメントにも、以下の通り記載があります。

コンテナーの標準出力ログは、-firelens- でタグ付けされます。したがって、コンテナ名が app でタスク ID が dcef9dee-d960-4af8-a206-46c31a7f1e67 の場合、タグは app-firelens-dcef9dee-d960-4af8-a206-46c31a7f1e67 になります。

なおFireLensの初期タグに関する詳しい仕様については、残念ながら公式ドキュメントでは見つけられませんでしたが、恐らくInputプラグインを利用して、コンテナの標準出力に対して初期タグを付与していると推測します。

Filterによるタグの書き換え

Filterセクションにrewrite_tagフィルターを指定する事で、特定の条件に一致するJSON形式のレコードを複製し、その際にFireLensデフォルトである-firelens-から別のタグ形式に変更できます。

例えばFireLensにより、以下のようなJSON形式のログレコードが出力されるとします。

{
    "container_name": "sample-container",
    "source": "stdout",
    "log": "sample-log",
    "container_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxx",
    "ecs_cluster": "sample-cluster",
    "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:task/sample-cluster/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "ecs_task_definition": "sample-task-definition:x"
}

この場合、例えば以下のFluent Bit設定により、上記のログレコードに対して、タグを書き換える事が可能です。

extra.conf

[FILTER]
    Name rewrite_tag
    Match *-firelens-*
    Rule $log (sample-log) sample-$container_id false

Nameパラメータには、今回使用するフィルターであるrewrite_tagを設定します。

Matchパラメータには、FireLensデフォルトの-firelens-のタグが付与されているログを引っ掛けたいため、*-firelens-*と記述します。

そしてRuleパラメータでは、JSON形式のログレコードを分析し、特定の条件に一致した場合のみ別のタグに書き換えます。
書き方としては以下の通りです。

$KEY  REGEX  NEW_TAG  KEEP

KEYには、条件一致として検索するキーを、$付きで指定します。
今回はログ内容で検索したいため、logキーを指定しています。

REGEXには、KEYに対応する値を検索した際に、条件一致させたい正規表現を指定します。
今回はsample-logという文字列がログ内容に存在する場合にタグを書き換えるため、(sample-log)という正規表現を指定してます。

NEW_TAGには、新しく書き換えるタグ形式を指定します。
今回は、sample-を指定し、コンテナID毎に別々のタグを付与するようにします。

KEEPには、rewrite_tagでレコードが複製された際に、古いタグを持つ元のレコードに対して、保持してパイプラインを続行する必要があるか、破棄する必要があるかを定義します。
今回はルールに一致した場合、古い-firelens-のタグを持つログレコードは必要ないため、falseを選択して破棄します。

Outputによるタグを用いたログルーティング

Outputセクションに特定のプラグインを指定する事で、タグを用いて様々なAWSサービスにログを転送する事が可能です。

例えば以下のFluent Bit設定で、cloudwatch_logs及びkinesis_firehoseプラグインを使用し、CloudWatch Logs、及びKinesis Data Firehose経由でS3へのログルーティングを検討してみます。

extra.conf

[OUTPUT]
    Name   cloudwatch_logs
    Match  sample-*
    region ap-northeast-1
    log_group_name /aws/ecs/sample-logs
    log_stream_prefix fluentbit-
    auto_create_group true

[OUTPUT]
    Name   kinesis_firehose
    Match  *
    region ap-northeast-1
    delivery_stream sample-delivery-stream

cloudwatch_logsプラグインのMatchパラメータでは、sample-*を指定しています。
これにより、sample-というタグがついているログのみ、CloudWatch Logsに出力されます。

ちなみに、cloudwatch_logsプラグインでlog_stream_prefixパラメータを指定すると、指定した値に続いてFluent Bitのタグが付与された名前で、ログストリームが出力されます。
例えば今回の場合は、/aws/ecs/sample-logsロググループにfluentbit-sample-というログストリームが出力されます。

kinesis_firehoseプラグインのMatchパラメータでは、*を指定しています。 これにより、全てのログ(今回は-firelens-またはsample-というタグがついているログ)が、Kinesis Data Firehoseに出力されます。

なおFireLensからAWSサービスにログを転送する際は、タスクロールに転送先AWSサービスへのアクセス許可が追加されたIAMポリシーを付与する必要があるのでご注意ください。

(余談)s3プラグインについて

FireLensでS3にログを出力する方法として、kinesis_firehoseプラグインでKinesis Data Firehoseを経由してS3に出力する方法の他に、s3プラグインを用いて直接S3に出力する方法もあります。

しかしながらFluent Bitの公式ドキュメントにおいて、上記のs3プラグインを使用すると、 Fargateのような永続ストレージがない実行環境でFluent Bitを起動した際に、Fluent Bitのコンテナが停止するとログが消失する可能性が示唆されています。

永続ディスクのない環境で Fluent Bit を実行する場合、または Fluent Bit を再起動して、以前の実行から store_dir に格納されたデータにアクセスできるようにする機能がない場合、いくつかの考慮事項が適用されます。これは、AWS FargateでFluent Bitを実行すると発生する可能性があります。 このような状況では、PutObject API を使用して頻繁にデータを送信し、ローカル バッファリングをできるだけ回避することをお勧めします。これにより、Fluent Bit が予期せず強制終了された場合のデータ損失が制限されます。

またAWS公式から提供されているGitHubの「Amazon ECS FireLens の例」でも、信頼性の問題でKinesis Data Firehoseを経由する事が推奨されています。

信頼性が重要な問題である場合は、Kinesis Data Firehose を Fluent Bit と S3 の間の信頼できる分散バッファーとして使用することをお勧めします。

そのため本番環境でFireLensを用いてログをS3に出力したい場合は、Kinesis Data Firehose経由で送信する事を推奨します。

タグによるログルーティングを試す

実際にALBからアクセス可能なECS(Fargate)上のNginxコンテナに対して、FireLensを設定し、一部のログのタグを書き換えた上で適切にログルーティングが実施されるか試してみます。

(ALB/ECR/ECS関連のコンポーネントの構築方法については、今回説明を割愛します)

なお今回の検証で構築するコンテナログ構成は以下となります。

nginx/fluentbitイメージプッシュ

まずは以下のDockerファイルで、nginxイメージをローカルでビルドし、ECRのプライベートリポジトリにプッシュしておきます。

Dockerfile

FROM nginx:latest

同様に以下のDockerファイルで、fluentbitイメージをローカルでビルドし、ECRのプライベートリポジトリにプッシュしておきます。

なおビルドの際に、Fluent Bit設定ファイルをイメージに反映します。

Dockerfile

FROM amazon/aws-for-fluent-bit:latest
COPY ./extra.conf /fluent-bit/etc/extra.conf

Fluent Bitの設定ファイルとしては、以下を使用します。

今回は以下の条件を満たすように、Filterセクションで一部のログのタグを書き換え、Outputセクションでタグを確認し適切な箇所にログを振り分ける設定します。

  1. ログの中身にELB-HealthChecker2.0という記載があるログ(ELBヘルスチェックログ)については、初期タグからhealthcheck-$container_idタグに書き換え、CloudWatch Logsの/aws/ecs/firelens/healthcheckロググループに転送する。
  2. 標準エラー出力に該当するログについては、初期タグからstderr-$container_idタグに書き換え、CloudWatch Logsの/aws/ecs/firelens/stderrロググループに転送する。
  3. 全てのログをKinesis Data Firehose経由でS3バケットに転送する。

extra.conf

[FILTER]
    Name rewrite_tag
    Match *-firelens-*
    Rule $log (ELB-HealthChecker\/2\.0) healthcheck-$container_id false

[FILTER]
    Name rewrite_tag
    Match *-firelens-*
    Rule $source (stderr) stderr-$container_id false

[OUTPUT]
    Name   cloudwatch_logs
    Match  healthcheck-*
    region ap-northeast-1
    log_group_name /aws/ecs/firelens/healthcheck
    log_stream_prefix fluentbit-
    auto_create_group true

[OUTPUT]
    Name   cloudwatch_logs
    Match  stderr-*
    region ap-northeast-1
    log_group_name /aws/ecs/firelens/stderr
    log_stream_prefix fluentbit-
    auto_create_group true

[OUTPUT]
    Name   kinesis_firehose
    Match  *
    region ap-northeast-1
    delivery_stream firehose-delivery-stream

なおFireLensのログルーティング構成としては、重要度/緊急度の高いログのみ分析用としてCloudWatch Logsに転送し、全てのログを長期保存用としてKinesis Data Firehose経由でS3に転送するパターンが一般的かと思われます。

ECSタスク構築

次に先ほどECRにプッシュしたnginx/fluentbitイメージのURIを以下のタスク定義ファイルで指定し、ECSタスクを構築します。

container_definitions.json

[
	{
		"name": "web_server",
		"image": "<nginx_image_uri>",
		"essential": true,
		"portMappings": [
			{
				"hostPort": 80,
				"protocol": "tcp",
				"containerPort": 80
			}
		],
		"logConfiguration": {
			"logDriver": "awsfirelens"
		}
	},
	{
	"name": "log_router",
	"image": "<fluentbit_image_uri>",
	"essential": true,
	"logConfiguration": {
		"logDriver": "awslogs",
		"options": {
			"awslogs-group": "/aws/ecs/firelens/log_router",
			"awslogs-region": "ap-northeast-1"",
			"awslogs-stream-prefix": "web_server_sidecar"
		}
	},
	"firelensConfiguration": {
		"type": "fluentbit",
		"options": {
				"config-file-type": "file",
				"config-file-value": "/fluent-bit/etc/extra.conf"
			}
		}
	}
]

この時点で正しくECSタスクが設定されていれば、FireLensによるログルーティングが実施されるはずです。

もしログが出力されていなければ、Fluent BitまたはECSタスクのいずれかが適切に設定されていない可能性が高いです。
(特にタスクロールへのIAMポリシーの付与については忘れがちなのでご注意ください)

ログ出力先確認

最後に、FireLensでNginxコンテナのログが適切に転送されているかを確認します。

初めにCloudWatch Logsの/aws/ecs/firelens/healthcheckロググループを確認すると、fluentbit-healthcheck-というログストリームが作成されています。

ログストリームの中身をみると、logキーにELB-HealthChecker/2.0の文字列が含まれるログのみ転送されている事が分かります。

次にCloudWatch Logsの/aws/ecs/firelens/stderrロググループを確認すると、fluentbit-stderr-というログストリームが作成されています。

ログストリームの中身をみると、sourceキーにstderrが指定されているログのみ転送されている事が分かります。

最後にKinesis Data Firehoseのfirehose-delivery-streamで送信先として指定しているS3バケットを確認すると、時間単位のパーティション区切りで全てのログがバケット内に保存されている事が分かります。

上記により、FireLensによるログルーティングが正しく機能している事が確認できました!

最後に

今回は、Fluent Bitにおけるタグの仕組みの観点から、FireLensを用いたログルーティングの設定方法についてまとめてみました。

FireLensを検証してみて、Fluent Bitに対する理解がかなり重要だなあと感じました。

FireLensを用いたコンテナログ構成を設計する場合、今回紹介させていただいたFluent Bitの知識は押さえておくと良いと思います。

以上、つくぼし(tsukuboshi0755)でした!