AWS ECSのログをFireLens/Fluent BitとKinesis Data Firehoseで処理する

## 概要

AWS ECSにデプロイしたアプリケーションのログをFireLens/Fluent BitとKinesis Data Firehoseを使って処理する方法をご紹介します。FireLens/Fluent Bitを利用することでECSのログをスマートに各種サービスに送信することができます。またKinesis Data Firehoseを活用することでログの蓄積を容易に行うことができ、Glue/Athenaと連携して高パフォーマンスでログを集計することができます。

## 構成

![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458395/a6434801-8292-13c1-58c2-0a0b6dfa950a.png)

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というモダンアプリケーション開発のベストプラクティス集によるとログは標準出力にストリームするのが良いとされています。

https://12factor.net/logs

ECSでは標準のタスク定義でCloudWatchにログを送信することができます。ログのエラーの検知などは可能ですが、集計するのには不向きです。そこでストリームされたデータを集めて適切な形式で保存する仕組みを構築することにしました。

今回扱うログの内容は以下のようなAPIサーバの処理ログです。JSON文字列形式で標準出力に出力されます。

```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.png](https://d2908q01vomqb2.cloudfront.net/b3f0c7f6bb763af1be91d9e74eabfeb199dc1f1f/2021/06/25/Screen-Shot-2019-09-26-at-5.21.35-PM-1024x572-1.png)

[詳解 FireLens – Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る | Amazon Web Services ブログ](https://aws.amazon.com/jp/blogs/news/under-the-hood-firelens-for-amazon-ecs-tasks/)

Fluent Bitの方が新しく、設定のサンプルコードも充実していたので今回はこちらを利用しました。

## Fluent Bit

https://fluentbit.io/

Fluent Bitはオープンソースのログ処理ライブラリで、ログの整形やフィルタ、出力ができます。

### FireLensでの利用

FireLensはECSで利用する際のテンプレートの設定を自動で行ってくれます。ただし、出力先を複数指定したりログにフィルタをかけたりするためにはカスタムの定義ファイルを作成する必要があります。Fargateで利用する場合はFluent Bitの設定ファイルを含めたDockerコンテナを作成、ECRにアップロードしてECSから利用します。

例えば`extra.conf`という名前で設定ファイルを作成し、以下のような簡単なDockerfileを作成してビルドしておきます(設定ファイルの内容は後述)。コンテナイメージはFireLens用にAWSが用意しているものを利用します。追加で何かをインストールする必要はありません。

```Dockerfile
FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:latest

ADD ./extra.conf /extra.conf
```

次にECSのタスク定義をFireLensを利用する設定に変更します。まず本体のアプリケーションコンテナの`logConfiguration`項目で`logDriver`を`awsfirelens`と指定します。次にログ処理用のコンテナを追加します。以下のように`containerDefinitions`の配列に新しくコンテナの設定オブジェクトを追加します。`firelensConfiguration`項目で前の手順でイメージに追加した設定ファイルのパスを指定しています。

```json
{
    "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の設定ファイルの内容についてご説明していきます。以下は筆者が利用している設定例です。各項目について詳しく見ていきましょう。

```conf
[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以上になると以下のような形式で分割します。

```json
{
    "source": "stdout",
    "log": "Sher",
    "partial_message": "true",
    "partial_id": "...",
    "partial_ordinal": "1",
    "partial_last": "false",
    "container_id": "...",
    "container_name": "/hopeful_taussig"
}
```

そうした分割ログを一つのログに統合するのがmultilineフィルタです。以下の項目がその設定です。

```conf
[FILTER]
    Name multiline
    Match *
    multiline.key_content log
    mode partial_message
```

https://github.com/aws-samples/amazon-ecs-firelens-examples/tree/mainline/examples/fluent-bit/filter-multiline-partial-message-mode

#### jsonパーサ

FireLensでは以下のようにJSONのログを出力すると文字列化されて処理されます。

```json
{
    "log": "{\"requestID\": \"b5d716fca19a4252ad90e7b8ec7cc8d2\", \"requestInfo\": {\"ipAddress\": \"204.16.5.19\", \"path\": \"/activate\", \"user\": \"TheDoctor\"}}",
    "container_id": "...",
    "container_name": "...",
    // ...
}
```

jsonパーサを設定することで以下のようにメタ情報を含めた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パーサのフィルタを設定します。

```conf
[SERVICE]
    Parsers_File /fluent-bit/parsers/parsers.conf
    Flush 1
    Grace 30

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

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

#### grepフィルタ

Fluent Bitではgrepフィルタを使うことで`key value`の形式の正規表現にマッチしたログだけに絞り込むことができます。アプリケーションが出力することを意図したログ(ここでは`name`という項目が含まれているもの)のみを後続処理に渡すように設定しています。標準出力なので例えばNext.jsのログなども一緒に出力されており、それらを除外することが目的です。

```conf
[FILTER]
    Name grep
    Match *
    Regex name .*
```

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

#### 出力先の設定

Fluent Bitには各種サービスにログを送信するためのプラグインが用意されており、必要な情報を記載するだけでCloudWatchやFirehoseにルーティングすることができます。

```conf
[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
```

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


CloudWatchに送信する場合はECSのタスクロールに以下の権限を追加する必要があります。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Resource": [
                "*"
            ],
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
            ]
        }
    ]
}
```

Firehoseに送信する場合はECSのタスクロールに以下の権限を追加する必要があります。

```json
{
    "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に変換したりすることも可能です。

https://aws.amazon.com/jp/kinesis/data-firehose/

以下で具体的な設定方法を順番に見ていきましょう。

### データの入出力

基本的に`Direct PUT`オプションを選択します。AWS SDKや他のAWSサービス、今回のFluent Bitから送信する場合はこちらのオプションです。Kinesis Data Streamsから入力する場合は`Direct PUT`ではなくそちらを選択することになります。また送信先にはS3を選択し、ログの出力先のバケットを指定します。

![firehose1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458395/9ac90a3b-a7ae-1fba-d89f-6426f1103f8a.png)

https://docs.aws.amazon.com/firehose/latest/dev/create-name.html

### データの変換

Lambdaに送って任意にデータ形式を変換することができます。またGlueのスキーマ情報を使ってログをパースした上でApache Parquet/ORCに変換することができます。今回の構成では利用しませんでした。

https://docs.aws.amazon.com/firehose/latest/dev/data-transformation.html

https://docs.aws.amazon.com/firehose/latest/dev/record-format-conversion.html

### パーティショニング

GlueはS3のキーが`year=2023/month=01`といったkey-value形式になっているとそれを基準にパーティションを作成してくれます。パーティションがあるとAthenaでのデータスキャン量を抑えられます。つまりクエリの処理時間や料金を減らすことができます。

JSON形式のログは[jq](https://stedolan.github.io/jq/manual/)を使ってパースしてパーティショニングの情報として利用することができます。

https://docs.aws.amazon.com/firehose/latest/dev/dynamic-partitioning.html

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 

![firehose2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458395/2f63d409-516f-2a14-d6e1-13e200e8859a.png)

#### ログのパース

パーティションのキーをとして利用するため、以下のようにログにunixタイムスタンプ(秒)を含めておきます。

```json
{   
    "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でパーティショニングされたテーブルが作成されます。

![firehose3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458395/1895aeb8-c20f-c1e1-2eb7-f227980a25a1.png)

### バッファリング

S3に送信する場合、バッファするファイルサイズを1~128MB、バッファする時間を60~900秒で設定できます。いずれかの制限に到達するとファイルがS3に配置されます。推奨されているデフォルトの設定は128MB/300秒です。

データはGZIPなどの形式で圧縮することもできます。`Compression for data records`で目的の形式を指定します。

![firehose4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458395/8fdc3bea-0bf8-58a8-90f9-625564b1416b.png)

https://docs.aws.amazon.com/firehose/latest/dev/basic-deliver.html

## まとめ

ECSのログをFireLens/Fluent BitとKinesis Data Firehoseを使って処理する方法をご紹介しました。ECS特有の複雑さをFireLensで解決しながら、Fluent Bitの機能を活用してスマートにログを処理することができます。またFirehoseで適切にパーティションの設定を行いながらS3にログ蓄積することができるため、Athenaによる集計のパフォーマンスを最適化することができます。アプリケーション側では標準出力にログを出力するだけなのでログ処理基盤との分離も明確です。

## 参考文献

[Custom log routing - Amazon ECS](https://docs.aws.amazon.com/AmazonECS/latest/userguide/using_firelens.html)

[詳解 FireLens – Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る | Amazon Web Services ブログ](https://aws.amazon.com/jp/blogs/news/under-the-hood-firelens-for-amazon-ecs-tasks/)

[AWS ECS on Fargate + FireLens で大きなログが扱いやすくなった話 | BLOG - DeNA Engineering](https://engineering.dena.com/blog/2022/08/firelens/)

[AWS FargateでFireLensを使って同じログを3箇所に送ってみた | DevelopersIO](https://dev.classmethod.jp/articles/fargate-fiirelens-fluentbit/)