ECS FargateでFireLens(AWS for Fluent Bit)の設定ファイルをS3から参照してみた

2023.07.26

CX事業部Delivery部の新澤です。

ECSでは、FireLens(AWS for Fluent Bit)を利用したカスタムログルーティングを行う設定をする際、FireLensで自動生成される設定ファイルに加えて、自分で作成したカスタム設定ファイルを指定することが可能です。 カスタム設定ファイルの指定方法には、S3バケットに設定ファイルを設置してファイルのARNを指定するs3方式と、コンテナイメージをビルドする際にカスタム設定ファイルをイメージにコピーして、ファイルパスを指定するfile方式の2通りがあります。

しかし、Fargateでのカスタム設定ファイルの指定については、公式ドキュメントにて

AWS Fargate でホストされるタスクは、file 設定ファイルタイプのみをサポートします。

と記載されています。

そのため、今まではFargateでカスタム設定ファイルを利用する際は自前のコンテナイメージを作成していたのですが、実はFargateでもS3バケットを参照可能ということを知る機会がありましたので、試してみました。

概要

FireLensの実体であるAWS for Fluent Bitは、AWSからPublic ECRDocker Hubで提供されていますが、今回の目的であるS3の設定ファイルを参照する機能は、イメージタグの先頭に"init-"が付いているイメージで提供されています。

例:

  • aws-for-fluent-bit:init-latest
  • aws-for-fluent-bit:init-2.27.0

"init-"付きのイメージは、Fluent Bit起動時に設定ファイルの初期化プロセスが実行されて、以下のようにコンテナの環境変数で指定したS3オブジェクトのARNやコンテナ内のファイルパスで指定した設定ファイルを参照してマージしてくれるようになっています。また、設定ファイルは複数指定することもできます。

"environment": [
                {
                    "name": "aws_fluent_bit_init_s3_1",
                    "value": "arn:aws:s3:::yourBucket/aaaaa.conf"
                },
                {
                    "name": "aws_fluent_bit_init_s3_2",
                    "value": "arn:aws:s3:::yourBucket/bbbbb.conf"
                },
                {
                    "name": "aws_fluent_bit_init_file_1",
                    "value": "/ecs/s3.conf"
                }
            ]

やってみた

では、さっそく試してみます。

システム構成

次のようなシステム構成をCDKを使ってデプロイしてみます。

API Gateway経由でECSサービスにリクエストします。ECSサービスではnginxコンテナが起動しており、アクセスログはFluent Bitに送られます。

Fluent Bitでは、エラーログをCloudWatch Logs、全てのログをFirehose経由S3バケットに出力するようルーティングします。

カスタム設定ファイルの作成

Firehose、CloudWatch Logsそれぞれに出力するカスタム設定ファイルを作成します。

出力先のFirehoseやCloudWatch Logsの情報はコンテナ定義の環境変数を参照するようにしています。

# nginxデフォルトログフォーマットをパースして構造化する
[FILTER]
    Name parser
    Match nginx-*
    Key_Name log
    Parser nginx

# ステータスコードが4xx,5xx、もしくは出力先がstderrのログのタグの先頭に"error"を付与
[FILTER]
    Name rewrite_tag
    Match nginx-*
    Rule $code ^[4-5]\d{2}$ error.$TAG false
    Rule $source ^stderr$ error.$TAG false

# 全てのログをFirehoseに送る
[OUTPUT]
    Name            kinesis_firehose
    Match           *
    delivery_stream ${FIREHOSE_DELIVERY_STREAM_NAME}
    region          ap-northeast-1
    time_key        time
    time_key_format %Y-%m-%dT%H:%M:%S.%3N
    compression     gzip

# タグ先頭が"error"のログのみCloudWatch Logsに送る
[OUTPUT]
    Name cloudwatch_logs
    Match   error.*
    region ap-northeast-1
    log_group_name ${LOG_GROUP_NAME}
    log_stream_prefix errorlog-

CDKの作成

サンプルCDKの全ソースはこちらにあります。ご参考まで。

まず、カスタム設定ファイルを配置するS3バケットと、カスタム設定ファイルをデプロイする定義を作成します。

    // S3 bucket for Fluentbit config files
    const configBucket = new s3.Bucket(this, 'FluentbitConfigBucket', {
      autoDeleteObjects: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
    new s3deployment.BucketDeployment(this, 'BucketDeployment', {
      destinationBucket: configBucket,
      sources: [s3deployment.Source.asset(path.resolve('fluentbit-config'))],
      retainOnDelete: false,
    });

ECSタスク定義では、nginxとFireLensのコンテナ定義を作成していますが、CDKでFireLensを構成する場合、Fluent Bitのコンテナイメージはデフォルトでは"latest"タグが使われるので明示的に"init-latest"タグのコンテナイメージを指定する必要があります。

同時に環境変数で上記で作成したS3バケットに配置されるカスタム設定ファイルの参照も指定します。

先頭に"aws_fluent_bit_init_s3"を付与した環境変数に、ファイルのARNを入れればOKです。 (例では後ろに連番を付与していますが、付与する文字列は任意です)

また、このサンプルではnginxのログを構造化ログにパースしたいので、Fluent Bitコンテナイメージに同梱されているパーサー設定ファイルも"aws_fluent_bit_init_file_1"で指定しています。

    const taskdef = new ecs.FargateTaskDefinition(this, 'ExampleTaskDef', {
      cpu: 256,
      memoryLimitMiB: 512,
    });
    taskdef.addContainer('nginx', {
      image: ecs.ContainerImage.fromRegistry('public.ecr.aws/nginx/nginx:latest'),
      portMappings: [{ containerPort: 80 }],
      logging: ecs.LogDrivers.firelens({}),
    });
    taskdef.addFirelensLogRouter('FireLensLogRouter', {
      image: ecs.ContainerImage.fromRegistry('public.ecr.aws/aws-observability/aws-for-fluent-bit:init-latest'),
      firelensConfig: {
        type: ecs.FirelensLogRouterType.FLUENTBIT,
      },
      environment: {
        FIREHOSE_DELIVERY_STREAM_NAME: deliveryStream.deliveryStreamName,
        LOG_GROUP_NAME: logGroup.logGroupName,
        aws_fluent_bit_init_s3_1: `${configBucket.bucketArn}/extra.conf`,
        aws_fluent_bit_init_file_1: '/fluent-bit/parsers/parsers.conf',
      },
      logging: ecs.LogDrivers.awsLogs({
        logGroup: new logs.LogGroup(this, 'FluentbitLogGroup'),
        streamPrefix: 'fluentbit',
      }),
    });

デプロイ

サンプルのCDKでデプロイしてみます。

$ npx cdk deploy --profile <AWS CLIプロファイル名>
(略)
Outputs:
ApiGatewayFargateServiceStack.ECSClusterName = ApiGatewayFargateServiceStack-ClusterEB0386A7-XXXXXXXXXXXX
ApiGatewayFargateServiceStack.HttpApiEndpoint = https://XXXXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com
ApiGatewayFargateServiceStack.LogGroupName = ApiGatewayFargateServiceStack-ErrorLogGroupB9A57448-XXXXXXXXXXXX
ApiGatewayFargateServiceStack.S3Bucket = apigatewayfargateservicestack-logbucketcc3b17e8-XXXXXXXXXXXX
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/ApiGatewayFargateServiceStack/eb1a52a0-2b46-11ee-88ae-0abdce72a65d

ログを出力させてみる

API Gatewayにリクエストしてみます。

nginxの応答が返ってきたらOKです。

$ curl https://XXXXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

次はエラーログを出力させるために存在しないURLにアクセスしてみます。

$ curl https://XXXXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/hoge
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.25.1</center>
</body>
</html>

ログを確認してみます。

S3バケットに両方のリクエストのログが出力されていました。

{
  "remote": "xxx.xxx.xxx.xxx",
  "host": "-",
  "user": "-",
  "method": "GET",
  "path": "/",
  "code": "200",
  "size": "615",
  "referer": "-",
  "agent": "curl/7.88.1",
  "time": "2023-07-26T00:02:51.0"
}
{
  "source": "stderr",
  "log": "2023/07/26 00:05:51 [error] 35#35: *2 open() \"/usr/share/nginx/html/hoge\" failed (2: No such file or directory), client: xxx.xxx.xxx.xxx, server: localhost, request: \"GET /hoge HTTP/1.1\", host: \"xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com\"",
  "container_id": "4a31cb911313401d96101e6eec3892d0-2531612879",
  "container_name": "nginx",
  "ecs_cluster": "ApiGatewayFargateServiceStack-ClusterEB0386A7-XXXXXXXXXXXX",
  "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/ApiGatewayFargateServiceStack-ClusterEB0386A7-XXXXXXXXXXXX/4a31cb911313401d96101e6eec3892d0",
  "ecs_task_definition": "ApiGatewayFargateServiceStackExampleTaskDef975C0BA0:3",
  "time": "2023-07-26T00:05:51.958"
}
{
  "remote": "xxx.xxx.xxx.xxx",
  "host": "-",
  "user": "-",
  "method": "GET",
  "path": "/hoge",
  "code": "404",
  "size": "153",
  "referer": "-",
  "agent": "curl/7.88.1",
  "ecs_cluster": "ApiGatewayFargateServiceStack-ClusterEB0386A7-XXXXXXXXXXXX",
  "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/ApiGatewayFargateServiceStack-ClusterEB0386A7-XXXXXXXXXXXX/4a31cb911313401d96101e6eec3892d0",
  "ecs_task_definition": "ApiGatewayFargateServiceStackExampleTaskDef975C0BA0:3",
  "time": "2023-07-26T00:05:51.0"
}

CloudWatch Logsのほうにはカスタム設定ファイルの指定通りエラーログのみが出力されていました。

Fluent Bit起動後の設定ファイルを確認してみる

Fluent Bitコンテナ内では設定ファイルがどのように取り込まれているか確認してみます。

ECS ExecでFluent Bitコンテナにログインしてみます。

$ aws ecs execute-command --cluster <ECSクラスター名> --profile <AWS CLIプロファイル名> \
    --task `aws ecs list-tasks --cluster <ECSクラスター名> --profile <AWS CLIプロファイル名> | jq -r '.taskArns[]' | cut -d'/' -f3` \
    --container fluentbit \
    --interactive \
    --command "/bin/sh"

Fluent Bitプロセスが読み込んでいる設定ファイルを確認してみます。

/init/fluent-bit-init.confを読み込んでいることがわかります。また、環境変数で指定したパーサーファイルも読み込まれていますね。

sh-4.2# ps axwww | grep fluent | grep -v grep
    1 ?        Ssl    0:00 /fluent-bit/bin/fluent-bit -e /fluent-bit/firehose.so -e /fluent-bit/cloudwatch.so -e /fluent-bit/kinesis.so -c /init/fluent-bit-init.conf -R /fluent-bit/parsers/parsers.conf

S3を参照したファイルは、/init/fluent-bit-init-s3-filesにダウンロードされてインクルードされていました。

sh-4.2# cat /init/fluent-bit-init.conf 
@INCLUDE /fluent-bit/etc/fluent-bit.conf
@INCLUDE /init/fluent-bit-init-s3-files/extra.conf

sh-4.2# cat /init/fluent-bit-init-s3-files/extra.conf
# nginxデフォルトログフォーマットをパース
[FILTER]
    Name parser
    Match nginx-*
    Key_Name log
    Parser nginx
(略)

最後に

今回はFargateでFireLensを利用する際にS3にカスタム設定ファイルを配置する方法について試してみました。

コンテナイメージ内にカスタム設定ファイルを保持する方式だと、設定変更の際にはコンテナイメージのビルド&プッシュ、タスク定義の変更&デプロイと結構手間でしたが、S3を参照することでECSサービスの再起動だけで設定変更可能になるので、とても便利ですね。