AWSECS

AWS FargateのFireLens(FluentBit)でログをCloudWatchとS3へ出力する

ECSにはFireLensというログルーティング機能があり、これを使えばログをCloudWatchやS3へ同時に流すことが簡単にできるようになります。

Firehoseのコストが高くついてしまう場合や、ただ流すだけでなく途中でログを整形したい場合などもFireLensは選択肢としてありだと思います。

今回はFireLensを使い、NginxのログをJSONに整形しつつCloudWatchとS3へ同時に出力してみます。

スポンサーリンク

FireLensの概要説明

FireLensはECSタスクにおけるシンタックスシュガーであり、データ・ログ収集アプリケーションのFluentd/FluentBitを指します。

Fluentd/FluentBitの比較はこちらにまとめられており、ECSにおいては軽量なFluentBitのほうが推奨されています

FluentBitを利用する場合、以下の図のようにメインとなるコンテナ(今回はNginx)と同じタスクにサイドカーコンテナ(メインのコンテナの補助的な役割をするコンテナ)としてFluentBitを配置します。

例えばこの図は、Nginxコンテナから出力されるNginxのログをFluentBit経由でCloudWatchとS3へ出力し、FluentBitコンテナから出力されるFluentBit自体のログをawslogsログドライバーでCloudWatchへ出力するという構成になります。

このインフラを構築するためのTerraformソースコード一式をこちらのGitHubへコミットしましたので動作確認にご利用ください。(S3のバケット名は適宜変更してください。)

FargateのFireLensでFluentBitを利用する場合、大きく分けて以下の2種類の設定方法があります

  1. ECSタスク定義のみでFireLensを設定します。設定が簡単な分、FluentBitに関してはシンプルな設定しかできません。
  2. ECSタスク定義と自分でビルドしたカスタムFluentBitイメージを使ってFireLensを設定します。FluentBitの理解とイメージの準備が手間ですが、その分複雑な設定ができます。

今回は、まず前者の説明をしてから応用として後者という流れで進めます。

FireLensをECSタスク定義のみで設定

ECSタスク定義のみでFireLensの設定を行い、CloudWatchとS3へNginxログを出力してみます。このやり方ではFluentBit設定ファイルを直接触ることなくAWSの設定のみでFluentBitの設定が行えます。(実際はFluentBitコンテナ内のFluentBit設定ファイルへ内容が反映されます。)

ただタスク定義のみでの設定方法では、CloudWatchとS3へ同時にログを出すことができないため、まずは別々に出力して基本的な設定を確認します。

FluentBitのイメージはこちらのAWSの公式Dockerイメージを使用します。Amazon LinuxにFluentBitやAWS用のプラグインがインストールされているイメージとなります。

CloudWatchへログを出力

ECSタスクロールにCloudWatchまわりの権限を付与

タスクロールに割り当てているIAMロールにCloudWatchへの書き込み権限を付与します。FluentBitはログ出力時に自動でロググループを作成することもできます。その場合は以下のようにCreateLogGroupの権限も与えます。

...

        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        },
...

ECSタスク定義のJSONでFireLens設定

ECSタスク定義のJSONのコンテナ部分を以下のように定義します。公式ドキュメントにJSONのサンプルがあるのでECSタスク定義をJSONで編集したほうがやりやすいと思います。

  ...

  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx",
      ...

      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "log_group_name": "/ecs/demo/nginx-from-fluent-bit",
          "auto_create_group": "true",
          "log_stream_prefix": "ecs/",
          "region": "ap-northeast-1",
          "Name": "cloudwatch_logs"
        }
      },
      ...

    },
    {
      "name": "fluent-bit",
      "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable",
...

      "logConfiguration": {
        "logDriver": "awslogs",
        "secretOptions": null,
        "options": {
          "awslogs-group": "/ecs/demo/fluent-bit",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": null
      },
    }
  ],
...

NginxコンテナでlogDriverをawsfirelensに指定し、optionでCloudWatchの詳細を設定します。

FluentBitコンテナではlogDriverをawslogsに指定し、FluentBit自体のログをCloudWatchへ出力します。そしてfirelensConfigurationでtypeをfluentbitとします。

CloudWatchへ出力されるログ

元となるNginxのログはデフォルトでは以下のように出力されます。

10.1.2.83 - - [18/Dec/2022:12:13:55 +0000] "GET / HTTP/1.1" 200 615 "-" "ELB-HealthChecker/2.0" "-"

これがFluentBit経由でCloudWatchへ出力すると以下のようになります。

{
    "source": "stdout",
    "log": "10.1.2.83 - - [30/Dec/2022:07:49:04 +0000] \"GET / HTTP/1.1\" 200 615 \"-\" \"ELB-HealthChecker/2.0\" \"-\"",
    "container_id": "fa2xxx",
    "container_name": "nginx",
    "ecs_cluster": "demo",
    "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:123456789012:task/demo/fa2xxx",
    "ecs_task_definition": "demo:53"
}

logがNginxの本来のログで他の箇所はFluentBitによって自動的に設定されるECSメタデータの内容になります。

ECSタスク定義をマネジメントコンソールから確認

参考までに、上記JSONがAWSマネジメントコンソール上では、以下のとおり設定されていることが確認できます。

タスク定義のNginxコンテナのストレージとログ

タスク定義の下部のログルーターの統合

S3へログを出力

今度はS3へログを出力してみます。

ECSタスクロールにS3まわりの権限付与

タスクロールに割り当ててるIAMロールにS3の当該バケットへの書き込み権限を付与します。

...

        {
            "Action": [
                "s3:PutObject"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::zoo200-demo-fluent-bit/*"
            ]
        },
...

ECSタスク定義 JSONへFireLens設定

ECSタスク定義のJSONのコンテナ部分を以下のように定義します。

  ...

  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx",
      ...

      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "bucket": "zoo200-demo-fluent-bit",
          "total_file_size": "1M",
          "use_put_object": "On",
          "region": "ap-northeast-1",
          "Name": "s3"
        }
      },
      ...

    },

... fluent-bit の設定はCloudWatch出力の場合と同じ
...  

NginxコンテナのlogConfigurationのoptionをS3用の設定にします。

なおFargateにおけるFluentBitのS3用推奨設定についてはこちらのドキュメントに記載されており、PutObject API を使用して頻繁にデータをS3へ送ることが重要とされています。

S3へ出力されるログ

S3の当該バケットへCloudWatchのケースと同様のログが出力されます。

デフォルトでは以下のようにS3バケットへ、<コンテナ名>-firelens-<ECSタスクID>/%Y/%m/%d/%H/%M/%S-ランダム文字列 でオブジェクトが生成されます。(キーはs3_key_formatを設定することで変更できます。)

$ aws s3 ls --recursive s3://zoo200-demo-fluent-bit | sort
2022-12-30 20:56:11      20079 fluent-bit-logs/nginx-firelens-0aexxx/2022/12/30/11/46/10-objectBCzDBZOh
2022-12-30 21:00:49       3230 fluent-bit-logs/nginx-firelens-0aecxxx/2022/12/30/12/00/36-objectiulqIwqU
2022-12-30 21:03:40      25116 fluent-bit-logs/nginx-firelens-e57xxx/2022/12/30/11/53/39-objecthekfkVuz
2022-12-30 21:14:02      18867 fluent-bit-logs/nginx-firelens-e57xxx/2022/12/30/12/04/00-objecti7hL7QCl
...
$

以上、FireLensをECSタスク定義のみで設定する基本的なやり方でした。

FireLensをECSタスク定義とカスタムFluentBitで設定

以下のような要件に対応するには、自分でFluentBitの設定ファイルを作成する必要があります

  • NginxのログをCloudWatchとS3へ同時に出力したい
  • Nginxのログをパースして日付、メソッドやステータスコードへ分割したい
  • FluentBitの様々な機能を使ってログをフィルター、整形したい

FluentBitイメージには/fluent-bit/etc/fluent-bit.confという基本となる設定ファイルがあるのですが、これとは別にカスタムしたFluentBit設定ファイルを作成します。

そのカスタム設定ファイルをFluentBitイメージに埋め込んでオリジナルのDockerイメージをビルドして、それをFireLensで使用します。(ECSの起動タイプがEC2であればS3からカスタム設定ファイルを読み込むこともできます。)

ここではNginxのログをパースして日付やメソッド、ステータスコードへそれぞれ分けつつ、CloudWatchとS3へ同時に出力してみます。

ECSタスクロールにCloudWatchとS3まわりの権限付与

CloudWatchとS3へ同時にログ出力するため両方まとめたIAMポリシーをECSタスクロールへ付与します。

        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        },
        {
            "Action": [
                "s3:PutObject"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::zoo200-demo-fluent-bit/*"
            ]
        }
    ],

カスタムFluentBitイメージの作成

こちらのGitHubにカスタムFluentBitイメージ作成用のDockerfileとカスタムFluentBit設定ファイルを置きました。

カスタムFluentBit設定ファイル

カスタムFluentBit設定ファイルは以下の内容になっています。

[SERVICE]
    Parsers_File    parsers.conf

[FILTER]
    Name parser
    Match *
    Key_Name log
    Parser nginx

[OUTPUT]
    Name cloudwatch_logs
    Match *
    region ap-northeast-1
    auto_create_group true
    log_group_name /ecs/demo/nginx-from-fluent-bit
    log_stream_prefix ecs/

[OUTPUT]
    Name s3
    Match *
    region ap-northeast-1
    bucket zoo200-demo-fluent-bit
    total_file_size 1M
    upload_timeout 1m
    use_put_object On

SERVICEディレクティブで、Docker内にあらかじめ用意されているparsers.confを読み込みます。

FILTERディレクティブでNginxのパーサーを設定し、Nginxの生ログが入っているlogというキーを指定して日付、メソッドやステータスコードへ分割します。

最後に2つのOUTPUTディレクティブでCloudWatchとS3へログを出力します。(FluentdのときはCOPYで複製する必要がありましたがFluentBitでは不要です。)

カスタムFluentBitイメージのビルドとECRへの登録

以下のようにDockerイメージをビルドしてECRへプッシュします。

## ECRリポジトリとAWSアカウントをシェル変数へセット
$ REPO="demo-fluent-bit"
$ ACCOUNT="123456789012"
$
## ECRへログイン
$ aws ecr get-login-password  | docker login --username AWS --password-stdin https://${ACCOUNT}.dkr.ecr.ap-northeast-1.amazonaws.com
Login Succeeded
$ 
## DockerイメージをビルドしてECRへプッシュ
$ docker build --platform linux/amd64 -t ${REPO} .
...
$ docker tag ${REPO}:latest ${ACCOUNT}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPO}
$
$ docker push ${ACCOUNT}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPO}
...
$

ECSタスク定義のJSONでFireLens設定

ECSタスク定義のJSONのコンテナ部分を以下のように定義します。

  ...

  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx",
      ...

      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": null
        }
      },
      ...

    },
    {
      "name": "fluent-bit",
      "image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/demo-fluent-bit",
...

      "logConfiguration": {
        "logDriver": "awslogs",
        "secretOptions": null,
        "options": {
          "awslogs-group": "/ecs/demo/fluent-bit",
          "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"
        }
      },
    }
  ],
...

NginxコンテナのlogConfigurationのoptionsをnullとします。

FluentBitコンテナのimageでECRへ登録したイメージを指定し、firelensConfigurationのoptionsでカスタムFluentBit設定ファイルのパスを記述します。(カスタムFluentBitイメージのビルド時に設定ファイルを/fluent-bit/etc/fluent-bit-custom.confへコピーしています。)

CloudWatchとS3へ出力されるログの確認

以下のようにNginxのログがJSON形式となった状態でCloudWatchとS3へ出力されます。

{
    "remote": "10.1.0.119",
    "host": "-",
    "user": "-",
    "method": "GET",
    "path": "/",
    "code": "200",
    "size": "615",
    "referer": "-",
    "agent": "ELB-HealthChecker/2.0"
}

FILTERディレクティブのPreserve_Key Onとすることでパース前の元データを残すこともできます。

{
    "remote": "10.1.2.83",
    "host": "-",
    "user": "-",
    "method": "GET",
    "path": "/",
    "code": "200",
    "size": "615",
    "referer": "-",
    "agent": "ELB-HealthChecker/2.0",
    "container_id": "a60xxx",
    "container_name": "nginx",
    "source": "stdout",
    "ecs_cluster": "demo",
    "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:123456789012:task/demo/a60xxx",
    "ecs_task_definition": "demo:56"
}

以上、FireLensをECSタスク定義とカスタムFluentBitで設定するやり方でした。

Tips

オリジナルのログフォーマットのパース

今回はNginxのデフォルトのログ形式であったため、はじめから用意されていたパーサーを使うことができましたが、アプリケーションログなどは自分でパーサーを作成する必要があります。

PARSERディレクティブのRegexでアプリケーションログを正規表現で引っ掛けるようにします。

正規表現はこちらのサイトでてっとり早く確認することができます。

FluentBitのトラブルシューティング

例えばECSタスクロールのIAMポリシーの権限が不足していると、FluentBitコンテナ自体のログ(今回の構築例ではCloudWatchの/ecs/demo/fluent-bitに出力)に以下のように出力されます。

[2022/12/18 23:27:03] [ info] [sp] stream processor started
[2022/12/18 23:27:04] [ info] [output:s3:s3.1] worker #0 started
[2022/12/18 23:37:05] [error] [aws_client] auth error, refreshing creds
[2022/12/18 23:37:05] [error] [aws_credentials] Shared credentials file /root/.aws/credentials does not exist
[2022/12/18 23:37:05] [error] [output:s3:s3.1] PutObject API responded with error='AccessDenied', message='Access Denied'
[2022/12/18 23:37:05] [error] [output:s3:s3.1] Raw PutObject response: HTTP/1.1 403 Forbidden
x-amz-request-id: 8XDxxx
x-amz-id-2: o7rxxx
Content-Type: application/xml
Transfer-Encoding: chunked
Date: Sun, 18 Dec 2022 23:37:04 GMT
Server: AmazonS3
Connection: close

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>8XDxxx</RequestId><HostId>o7rxxx</HostId></Error>
[2022/12/18 23:37:05] [error] [output:s3:s3.1] PutObject request failed
[2022/12/18 23:37:05] [error] [output:s3:s3.1] Could not send chunk with tag

FluentBit設定ファイルの内容でミスがあった場合も同様にエラーログが出力されるので、うまく動かなかった場合はFluentBitコンテナのログを確認してください。

今回は以上です〜ノシ

参考

(`・ω・´)ノ アリガトウゴザイマス!!

AWS FargateでFireLensを使って同じログを3箇所に送ってみた
詳解 FireLens – Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る
ECS カスタムログルーティング
What is Fluent Bit?