AWS FireLensでfluentbitコンテナをカスタマイズする具体的な方法

## ECSのログ事情について

AWSのECSによるコンテナ運用では、ログの出力がデフォルトで`CloudWatch`に限定されています。
これはECSのログドライバが`awslogs`になっているということになりますが、実用面では力不足であることが否めません。
S3やElasticsearchにログを流すには？Datadogなどのサードパーティツールにログを流すには？
解決方法として、AWSの公式で`FireLens`という方法が紹介されています。

- [Firelens の発表 – コンテナログの新たな管理方法](https://aws.amazon.com/jp/blogs/news/announcing-firelens-a-new-way-to-manage-container-logs/)

FireLensは`fluentbit`のコンテナをサイドカーとして起動し、ログの収集を`fluentbit`の機能で実行するというものです。
（`fluentd`も選択できます）

例）今回目指すWebアプリケーションの構成
![firelens.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/13e04daf-580c-e0f3-6212-d9e506297196.png)

実際に、AWSコンソールのECSのタスク定義を設定する画面には以下のような設定項目があります。

![スクリーンショット 2020-03-14 15.01.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/61bd739b-58f5-031f-a70c-3b060a17b384.png)

`FireLensの統合を有効にする`を選択すると、自動で`906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest`のコンテナがサイドカーとして追加されるようになります。
このコンテナはAWSの公式コンテナです。

当記事では、このコンテナを自身でカスタマイズしたものを適用する一例を紹介します。

## コンテナ定義

それではコンテナ定義がどのようになるかTerraformで見てみましょう。

```task_definitisons.tf
data "template_file" "default" {
  template = <<EOF
[
  {
    "image": "${var.aws-account-id}.dkr.ecr.ap-northeast-1.amazonaws.com/firelens-sample/go:latest",
    "name": "go",
    "essential": false,
    "logConfiguration": {
      "logDriver": "awsfirelens"
    },
    "portMappings": [],
    "cpu": 64,
    "memoryReservation": 128
  },
  {
    "image": "${var.aws-account-id}.dkr.ecr.ap-northeast-1.amazonaws.com/firelens-sample/nginx:latest",
    "name": "nginx",
    "essential": true,
    "logConfiguration": {
      "logDriver": "awsfirelens"
    },
    "portMappings": [
      {
        "hostPort": 80,
        "protocol": "tcp",
        "containerPort": 80
      }
    ],
    "cpu": 64,
    "memoryReservation": 128
  },
  {
    "image": "${var.aws-account-id}.dkr.ecr.ap-northeast-1.amazonaws.com/firelens-sample/fluentbit:latest",
    "name": "log_router",
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/firelens-sample",
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "ecs"
      }
    },
    "firelensConfiguration": {
      "type": "fluentbit",
      "options": {
        "config-file-type": "file",
        "config-file-value": "/fluent-bit/etc/fluent-bit_custom.conf"
      }
    },
    "portMappings": [],
    "cpu": 128,
    "memoryReservation": 256
  }
]
EOF
}
```

Goのコンテナ、Nginxのコンテナ、fluentbitのコンテナを定義しました。
以下を確認してください。

```json
"logConfiguration": {
    "logDriver": "awsfirelens"
}
```

logDriverを`awsfirelens`としています。
なお、fluentbitコンテナ自体のログドライバは`awslogs`としてください。

そして注目すべき定義はこちらです。

```json
"firelensConfiguration": {
    "type": "fluentbit",
    "options": {
        "config-file-type": "file",
        "config-file-value": "/fluent-bit/etc/fluent-bit_custom.conf"
    }
}
```

config-file-valueを指定しています。
この`/fluent-bit/etc/fluent-bit_custom.conf`に独自カスタマイズしたfluentbitの設定を記載しましょう。

## fluentbitの設定
### よくある設定
[サンプルコード](https://github.com/ryohei-takagi/firelens-sample)をもとに進めます。
先ほどの`/fluent-bit/etc/fluent-bit_custom.conf`を記載していきましょう。

```./docker/fluentbit/fluent-bit_custom.conf
[SERVICE]
    Flush 1
    Grace 30
    Log_Level info

[OUTPUT]
    Name cloudwatch
    Match *
    log_key log
    region ap-northeast-1
    log_group_name /ecs/firelens-sample
    log_stream_name container

[OUTPUT]
    Name firehose
    Match *
    delivery_stream my-firehose
    region ap-northeast-1
```

OUTPUTに`cloudwatch`と`firehose`を指定していることに注目してください。
後述しますが、`amazon/aws-for-fluent-bit`のコンテナをベースに進めますのでデフォルトで上記のプラグインがインストールされた状態になっています。

さて、この設定で`cloudwatch`と`firehose`にログが出力されるようになりますが、一点、`Match *`に注目しましょう。
これはFireLensが収集したログを全て出力されるようになっており、
例えばDataLakeのようなログの流し方であれば問題ありませんが、もう少し整形してから流したいというニーズがあるかもしれません。

そこで`Parser`を使って、ログをコンテナごとのCloudWatchストリームに分けるということをしてみましょう。

### Parser
先ほどのconfを以下のようにしました。

```./docker/fluentbit/fluent-bit_custom.conf
[SERVICE]
    Flush 1
    Grace 30
    Log_Level info
    Streams_File stream_processor.conf
    Parsers_File parser.conf

[FILTER]
    Name parser
    Match *
    Key_Name container_name
    Parser container
    Reserve_Data true

[OUTPUT]
    Name cloudwatch
    Match combine.nginx
    log_key log
    region ap-northeast-1
    log_group_name /ecs/firelens-sample
    log_stream_name nginx

[OUTPUT]
    Name cloudwatch
    Match combine.go
    log_key log
    region ap-northeast-1
    log_group_name /ecs/firelens-sample
    log_stream_name go

[OUTPUT]
    Name firehose
    Match container
    delivery_stream my-firehose
    region ap-northeast-1
```

以下の箇所でログのパースをおこないます。

```
[FILTER]
    Name parser
    Match *
    Key_Name container_name
    Parser container
    Reserve_Data true
```

さてここで、そもそも`FireLens`からはどのようなログが収集されているのでしょうか。
当然、自身で流したアプリケーション用のログ、Nginxのアクセスログなどをイメージしますが
実はそのほかのメタ情報を付与した状態で収集しています。
以下を確認してください。

```json
{
    "container_id": "abcde12345",
    "container_name": "/ecs-firelens-sample-1-nginx-xxxyyyzzz",
    "ecs_cluster": "arn:aws:ecs:ap-northeast-1:1234567890123:cluster/firelens-sample",
    "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:1234567890123:task/aaa-bbb-ccc-ddd-eee",
    "ecs_task_definition": "firelens-sample:1",
    "log": "10.0.0.0 - - [14/Mar/2020:10:00:00 +0000] \"GET /healthcheck HTTP/1.1\" 200 0 \"-\" \"ELB-HealthChecker/2.0\" \"-\"",
    "source": "stdout"
}
```

firelensが収集するログの例です。`log`というキーが出力されるログですが、それ以外にタスク定義の情報やコンテナ名などもあります。
今回はこれらの情報でパースをしてみます。

`container_name`をパースして`nginx`という部分を抜き出します。
Goコンテナの場合は`go`という文字を抜き出します。

```./docker/fluentbit/parser.conf
[PARSER]
    Name container
    Format regex
    Regex ^\/(?<task_name>(ecs-firelens-sample))-(?<task_revision>\d+)-(?<container_name>.+)-(?<target_name>.+)$
```

正規表現で抜き出してみました。以下のようにパースされました。

```json

{
    "task_name": "ecs-firelens-sample",
    "task_revision": 1,
    "container_name": "nginx",
    "target_name": "xxxyyyzzz",
}
```

`container_name`が「nginx」となっています。
これでコンテナ別にログの出力先を分けるということができそうです。

もちろん、ログそのものに対してパースをしてログを振り分けることができれば様々な要件にも対応できるかと思います。
いろいろなパターンを考慮して要件に沿った設計を考えてみてください。

### Stream Processor

ログの種類別にタグを振り分けることができます。
先ほどの`container_name`「nginx」「go」をそれぞれ「combine.nginx」「combine.go」というタグを付与してみましょう。

```./docker/fluentbit/stream_processor.conf
[STREAM_TASK]
    Name nginx
    Exec CREATE STREAM nginx WITH (tag='combine.nginx') AS SELECT log FROM TAG:'*-firelens-*' WHERE container_name = 'nginx';

[STREAM_TASK]
    Name go
    Exec CREATE STREAM go WITH (tag='combine.go') AS SELECT log FROM TAG:'*-firelens-*' WHERE container_name = 'go';

# すべてのログ
[STREAM_TASK]
    Name container
    Exec CREATE STREAM container WITH (tag='container') AS SELECT * FROM TAG:'*-firelens-*';
```

SQLのような形式で記載します。
FireLensのログは元々`*-firelens-*`にマッチするタグが付与されています。
それに`WHERE container_name = 'nginx'`などとすることでnginxの新しいタグを追加できます。

追加したタグは、`[OUTPUT]`セクションで指定しましょう。

```
[OUTPUT]
    Name cloudwatch
    Match combine.nginx
    log_key log
    region ap-northeast-1
    log_group_name /ecs/firelens-sample
    log_stream_name nginx
```

## Docker定義

続いてDocker定義です。

`amazon/aws-for-fluent-bit`コンテナをベースに使います。

```:./docker/fluentbit/Dockerfile
FROM amazon/aws-for-fluent-bit:2.1.1

COPY ./docker/fluentbit/fluent-bit_custom.conf /fluent-bit/etc/fluent-bit_custom.conf
COPY ./docker/fluentbit/stream_processor.conf /fluent-bit/etc/stream_processor.conf
COPY ./docker/fluentbit/parser.conf /fluent-bit/etc/parser.conf
```

これでECRにプッシュすれば立派なログ収集コンテナとして活躍できるでしょう。

```sh
$ aws ecr get-login-password | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com

$ docker build -t firelens-sample-fluentbit:latest -f docker/fluentbit/Dockerfile .
$ docker tag firelens-sample-fluentbit:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/firelens-sample/fluentbit:latest
$ docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/firelens-sample/fluentbit:latest
```

## おわり

fluentbitを使ったログ収集用コンテナのカスタマイズについて解説しました。
誰かの参考になれば幸いです。