概要
AWS ECSにデプロイしたアプリケーションのログをFireLens/Fluent BitとKinesis Data Firehoseを使って処理する方法をご紹介します。FireLens/Fluent Bitを利用することでECSのログをスマートに各種サービスに送信することができます。またKinesis Data Firehoseを活用することでログの蓄積を容易に行うことができ、Glue/Athenaと連携して高パフォーマンスでログを集計することができます。
構成
ECSタスク内でアプリケーションのコンテナとログ処理ソフトウェアのFluent Bitが動作するコンテナを立ち上げます。コンテナ間のログの送信はやFluent Bit用コンテナの実行はFireLensというECSのプラグインが設定してくれます。Fluent BitからKinesis Data FirehoseとCloudWatchにログを送信します。さらにKinesis Data FirehoseからS3にデータを配置し、Glueによってクローリングすることでカタログ化します。そうして作成されたテーブルに対してAthenaでクエリを実行するとログを集計することができる仕組みです。
背景
ECS on Fargateのログから出力されるログは、例えばEC2などのサーバのようにファイルに書き出して保存しておくことはできません。コンテナの停止によってファイルが消えてしまうからです。The Twelve-Factor Appというモダンアプリケーション開発のベストプラクティス集によるとログは標準出力にストリームするのが良いとされています。
ECSでは標準のタスク定義でCloudWatchにログを送信することができます。ログのエラーの検知などは可能ですが、集計するのには不向きです。そこでストリームされたデータを集めて適切な形式で保存する仕組みを構築することにしました。
今回扱うログの内容は以下のようなAPIサーバの処理ログです。JSON文字列形式で標準出力に出力されます。
{
"level": "info",
"name": "apiInfoLog",
"status": 200,
"pathName": "/path/to/api",
"method": "POST",
"bodyJson": {},
"query": "",
"timestamp": "2023-02-09T02:55:02+00:00",
"unixtime": 1675911302,
"responseTime": 116,
"user": {},
}
FireLens
ECSにはFireLensというログ処理用のプラグインがあります。これを使うとタスク内にFluentdまたはFluent Bitを実行するためにコンテナを追加で立ち上げ、そのコンテナに標準出力のログをルーティングすることができます。
詳解 FireLens – Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る | Amazon Web Services ブログ
Fluent Bitの方が新しく、設定のサンプルコードも充実していたので今回はこちらを利用しました。
Fluent Bit
Fluent Bitはオープンソースのログ処理ライブラリで、ログの整形やフィルタ、出力ができます。
FireLensでの利用
FireLensはECSで利用する際のテンプレートの設定を自動で行ってくれます。ただし、出力先を複数指定したりログにフィルタをかけたりするためにはカスタムの定義ファイルを作成する必要があります。Fargateで利用する場合はFluent Bitの設定ファイルを含めたDockerコンテナを作成、ECRにアップロードしてECSから利用します。
例えばextra.conf
という名前で設定ファイルを作成し、以下のような簡単なDockerfileを作成してビルドしておきます(設定ファイルの内容は後述)。コンテナイメージはFireLens用にAWSが用意しているものを利用します。追加で何かをインストールする必要はありません。
FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:latest
ADD ./extra.conf /extra.conf
次にECSのタスク定義をFireLensを利用する設定に変更します。まず本体のアプリケーションコンテナのlogConfiguration
項目でlogDriver
をawsfirelens
と指定します。次にログ処理用のコンテナを追加します。以下のようにcontainerDefinitions
の配列に新しくコンテナの設定オブジェクトを追加します。firelensConfiguration
項目で前の手順でイメージに追加した設定ファイルのパスを指定しています。
{
"containerDefinitions": [
{
"name": "application",
// ...
"dependsOn": [
{
"containerName": "log-router",
"condition": "START"
}
],
"logConfiguration": {
"logDriver": "awsfirelens"
}
},
{
"name": "log-router",
"image": "000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/log-router",
"logConfiguration": {
// ...
},
"firelensConfiguration": {
"type": "fluentbit",
"options": {
"config-file-type": "file",
"config-file-value": "/extra.conf"
}
}
}
],
}
Fluent Bitの設定
先ほどコンテナイメージに含めたFluent Bitの設定ファイルの内容についてご説明していきます。以下は筆者が利用している設定例です。各項目について詳しく見ていきましょう。
[SERVICE]
Parsers_File /fluent-bit/parsers/parsers.conf
Flush 1
Grace 30
[FILTER]
Name multiline
Match *
multiline.key_content log
mode partial_message
[FILTER]
Name parser
Match *
Key_Name log
Parser json
Reserve_Data True
[FILTER]
Name grep
Match *
Regex name .*
[OUTPUT]
Name cloudwatch_logs
Match *
region ap-northeast-1
log_group_name /ecs/firelens/sample
log_stream_name sample
auto_create_group true
[OUTPUT]
Name kinesis_firehose
Match *
region ap-northeast-1
delivery_stream sample
multilineフィルタ
Fluent Bitのログドライバは一行のログが16kb以上になると以下のような形式で分割します。
{
"source": "stdout",
"log": "Sher",
"partial_message": "true",
"partial_id": "...",
"partial_ordinal": "1",
"partial_last": "false",
"container_id": "...",
"container_name": "/hopeful_taussig"
}
そうした分割ログを一つのログに統合するのがmultilineフィルタです。以下の項目がその設定です。
[FILTER]
Name multiline
Match *
multiline.key_content log
mode partial_message
jsonパーサ
FireLensでは以下のようにJSONのログを出力すると文字列化されて処理されます。
{
"log": "{\"requestID\": \"b5d716fca19a4252ad90e7b8ec7cc8d2\", \"requestInfo\": {\"ipAddress\": \"204.16.5.19\", \"path\": \"/activate\", \"user\": \"TheDoctor\"}}",
"container_id": "...",
"container_name": "...",
// ...
}
jsonパーサを設定することで以下のようにメタ情報を含めたJSON形式に変換することができます。
{
"source": "stdout",
"container_id": "...",
"container_name": "...",
// ...
"requestID": "b5d716fca19a4252ad90e7b8ec7cc8d2",
"requestInfo": {
"ipAddress": "204.16.5.19",
"path": "/activate",
"user": "TheDoctor"
}
}
後続のFirehoseでパースして処理する際にJSON形式であることが必要なのでこの変換処理を挟んでいます。設定ファイルでは以下の項目が対応しています。FireLens用のイメージにはあらかじめパーサの設定ファイルが含まれているので、SERVICE
の項目でそれを読み込んだ上でjsonパーサのフィルタを設定します。
[SERVICE]
Parsers_File /fluent-bit/parsers/parsers.conf
Flush 1
Grace 30
[FILTER]
Name parser
Match *
Key_Name log
Parser json
Reserve_Data True
grepフィルタ
Fluent Bitではgrepフィルタを使うことでkey value
の形式の正規表現にマッチしたログだけに絞り込むことができます。アプリケーションが出力することを意図したログ(ここではname
という項目が含まれているもの)のみを後続処理に渡すように設定しています。標準出力なので例えばNext.jsのログなども一緒に出力されており、それらを除外することが目的です。
[FILTER]
Name grep
Match *
Regex name .*
出力先の設定
Fluent Bitには各種サービスにログを送信するためのプラグインが用意されており、必要な情報を記載するだけでCloudWatchやFirehoseにルーティングすることができます。
[OUTPUT]
Name cloudwatch_logs
Match *
region ap-northeast-1
log_group_name /ecs/firelens/sample
log_stream_name sample
auto_create_group true
[OUTPUT]
Name kinesis_firehose
Match *
region ap-northeast-1
delivery_stream sample
CloudWatchに送信する場合はECSのタスクロールに以下の権限を追加する必要があります。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Resource": [
"*"
],
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
]
}
]
}
Firehoseに送信する場合はECSのタスクロールに以下の権限を追加する必要があります。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"firehose:PutRecord",
"firehose:PutRecordBatch"
],
"Resource": "*"
}
]
}
以上でECSからFireLens/Fluent Bitを使ってFirehoseにログを送信する仕組みが整いました。次にFirehoseでのログ処理の設定をご説明していきます。
Kinesis Data Firehose
Kinesis Data Firehoseはストリーミングデータに加工を施しながら各種サービスに送信するサービスです。今回はFluent Bitが流したログデータをS3にファイルとして配置するために利用します。ログ一行ごとにファイルが作成されるのではなく、適宜バッファしてまとめて送信されます。またその際にGlueでのパーティションを考慮したキーで配置できます。さらにデータの形式をJSON->Apache Parqueに変換したりすることも可能です。
以下で具体的な設定方法を順番に見ていきましょう。
データの入出力
基本的にDirect PUT
オプションを選択します。AWS SDKや他のAWSサービス、今回のFluent Bitから送信する場合はこちらのオプションです。Kinesis Data Streamsから入力する場合はDirect PUT
ではなくそちらを選択することになります。また送信先にはS3を選択し、ログの出力先のバケットを指定します。
データの変換
Lambdaに送って任意にデータ形式を変換することができます。またGlueのスキーマ情報を使ってログをパースした上でApache Parquet/ORCに変換することができます。今回の構成では利用しませんでした。
パーティショニング
GlueはS3のキーがyear=2023/month=01
といったkey-value形式になっているとそれを基準にパーティションを作成してくれます。パーティションがあるとAthenaでのデータスキャン量を抑えられます。つまりクエリの処理時間や料金を減らすことができます。
JSON形式のログはjqを使ってパースしてパーティショニングの情報として利用することができます。
JSON形式のログをパースするために以下の設定にします。Fluent Bitもログをバッファするため、Multi record deaggregationをEnabledにしておきます。
- Multi record deaggregation: Enabled
- Multi record deaggregation type: JSON
- New line delimiter: Enabled
- Inline parsing for JSON: Enabled
ログのパース
パーティションのキーをとして利用するため、以下のようにログにunixタイムスタンプ(秒)を含めておきます。
{
"unixtime": 1565382027
}
jqの関数を使ってyear/monthなどを取り出します。
- year:
.unixtime| strftime("%Y")
- month:
.unixtime| strftime("%m")
- date/hourなども同様
S3パスの指定
次に取り出した値を変数にしてログファイルを配置するS3のパスを決めます。以下のように変数名を使ったプレースホルダーを活用します。
year=!{partitionKeyFromQuery:year}/month=!{partitionKeyFromQuery:month}/date=!{partitionKeyFromQuery:date}/hour=!{partitionKeyFromQuery:hour}/
あとはGlueのクローラでログを置いたS3バケットを読み込めばyear/month/date/hourでパーティショニングされたテーブルが作成されます。
バッファリング
S3に送信する場合、バッファするファイルサイズを1~128MB、バッファする時間を60~900秒で設定できます。いずれかの制限に到達するとファイルがS3に配置されます。推奨されているデフォルトの設定は128MB/300秒です。
データはGZIPなどの形式で圧縮することもできます。Compression for data records
で目的の形式を指定します。
まとめ
ECSのログをFireLens/Fluent BitとKinesis Data Firehoseを使って処理する方法をご紹介しました。ECS特有の複雑さをFireLensで解決しながら、Fluent Bitの機能を活用してスマートにログを処理することができます。またFirehoseで適切にパーティションの設定を行いながらS3にログ蓄積することができるため、Athenaによる集計のパフォーマンスを最適化することができます。アプリケーション側では標準出力にログを出力するだけなのでログ処理基盤との分離も明確です。
参考文献
Custom log routing - Amazon ECS
詳解 FireLens – Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る | Amazon Web Services ブログ
AWS ECS on Fargate + FireLens で大きなログが扱いやすくなった話 | BLOG - DeNA Engineering