ECS Fargate + Fluent Bitでコンテナ内のログファイルをCloudWatchLogsに出力する

# 概要
ECS Fargate上のアプリケーションについて、   
ログフォワーダーとしてFluent Bitを利用したサイドカーパターン構成を考えてみます。

ECS Fargateのログ管理方法を検討する際、プラットフォーム標準機能であるDocker Logging Driverの活用が第一候補に挙がるとおもいますが、   
ここでは都合によりログの出力先をstdout, stderrに向けず、ファイル出力しているアプリケーションコンテナのログ管理を想定します。[^1]
[^1]: 仮想マシンで稼働しているアプリケーションをコンテナ化したい場合などを想定してください。   

# 設定内容

ECSタスク定義、Fluent Bitの設定ファイルの内容を確認していきます。
おおよそ以下のベストプラクティスの構成と同じような形になります。

https://cloud.google.com/architecture/best-practices-for-operating-containers?hl=ja#log_aggregator_sidecar_pattern

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189877/58661871-7c45-a08d-1b5f-fb57aae44a82.png)

## ECSタスク定義

アプリケーション + Fluent Bitの構成を設定したTerraformのリソース定義になります。   
いくつかパラメータを省略しています。

```tf
resource "aws_ecs_task_definition" "this" {
  family                   = "logforwarder-sidecar-pattern"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 1024
  memory                   = 2048

  container_definitions = jsonencode([
    {
      name      = "application"
      essential = true
      image     = "${var.AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/application:xxxxx"
      
      mountPoints = [
        {
          "sourceVolume"  = "log_directory",
          "containerPath" = var.log_output_dir_path
        }
      ]

      dependencies = [{
        containerName = "fluent-bit"
        condition     = "START"
      }]

      enable_cloudwatch_logging = false
      log_configuration = {
        logDriver = "awsfirelens"
        options = {
          "Name" : "cloudwatch",
          "region" : var.AWS_REGION,
          "log_stream_prefix" : "application",
          "log_group_name" : "/aws/ecs/${var.cluster_name}/${var.service_name}",
          "auto_create_group" : "true"
        }
      }
    },
    {
      name      = "fluent-bit"
      essential = true
      image     = "${var.AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/custom_aws-for-fluent-bit:xxxxx"
      firelens_configuration = {
        type = "fluentbit"
        options = {
          "config-file-type" : "file",
          "config-file-value" : var.fluent-bit_conf_absolute_path
        }
      }

      mountPoints = [
        {
          "sourceVolume" : "log_directory",
          "containerPath" : var.log_output_dir_path
        }
      ]
    },
    {
      name      = "logrotate"
      essential = true
      image     = "${var.AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/logrotate:xxxxx"

      enable_cloudwatch_logging = false
      mountPoints = [
        {
          "sourceVolume"  = "log_directory",
          "containerPath" = var.log_output_dir_path
        }
      ]
    }
  ])

  volume {
    name = "log_directory"
  }

  runtime_platform {
    operating_system_family = "LINUX"
    cpu_architecture        = "X86_64"
  }
}

```

applicationコンテナと併せてログ転送のためにfluent-bitコンテナを設定します。   
また、ログローテーションのためにlogrotateコンテナ[^2]を用意します。   
applicationコンテナのログディレクトリをバインドマウントさせることで、  
fluent-bitコンテナからファイルアクセスを可能にしています。

ついでにFireLensの設定をしていますが、扱いたいのは標準出力ではないので必須ではありません。    
もし設定する場合、この後確認するFluent Bitの設定ファイルの絶対パスを`config-file-value`に入れてください。   
参考: https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/using_firelens.html

```tf
firelens_configuration = {
        type = "fluentbit"
        options = {
          "config-file-type" : "file",
          "config-file-value" : var.fluent-bit_conf_absolute_path
        }
      }
```

[^2]: なにかしらの組織にメンテされているイメージが見当たらなかったため自前で用意します。   
参考: https://qiita.com/minarai/items/45121d5c06a1098e9a1c

## FluentBitのDockerfile

```Dockerfile: Dockerfile
FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:init-2.32.0.20231205
# https://gallery.ecr.aws/aws-observability/aws-for-fluent-bit

ARG FLUENT_BIT_CONF_PATH="/usr/local/etc/fluent-bit"

COPY ./fluent-bit.conf ${FLUENT_BIT_CONF_PATH}

```

AWSがメンテしているFluent Bitのイメージを利用します。Fluent Bitの設定中でECS Metadataを使用するためinitタグのイメージを選択しています。   
参考: https://github.com/aws/aws-for-fluent-bit/blob/mainline/use_cases/init-process-for-fluent-bit/README.md
以下のFluent Bitの設定ファイルを用意しイメージに取り込みます。${LOG_BASEDIR}等の変数は適宜変更してください。   

`CMD`はベースイメージから引き継がれます。fleunt-bit起動時の設定ファイルが先のタスク定義の`config-file-value`で指定した設定ファイルを呼び出すことで設定が有効化されます。   
FireLens設定しない場合は`CMD`を書き換えて用意した設定ファイルを直接読み込んで起動するようにしてみてください。   
参考: https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/firelens-taskdef.html#firelens-taskdef-customconfig

```text: fluent-bit.conf
@SET LOG_BASEDIR=${LOG_BASEDIR}
@SET AWS_REGION=ap-northeast-1

[SERVICE]

[INPUT]
    Name        tail
    Tag         tail.app_logs.*
    Path        ${LOG_BASEDIR}/*
    Path_Key    application_log_file_path
    Read_from_Head     true
    
[OUTPUT]
    Name cloudwatch_logs
    Match tail.app_logs.*
    region ${AWS_REGION}
    log_group_name fallback-group
    log_stream_prefix fallback-stream
    auto_create_group true
    log_group_template /aws/ecs/${ECS_CLUSTER}/${ECS_FAMILY}/APPLICATION_LOGS
    log_stream_template $application_log_file_path

```
Fluent Bitの設定の詳細は公式ドキュメントに記載があるため割愛します。   
参考: https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/classic-mode

注目したいのは以下の2点です。これらを適切に設定することで任意のパスのログファイルを任意のCloudWatchLogsのロググループ, ストリームへの転送が可能になります。   

 - INPUTセクションのPathパラメータでログ転送対象のログファイル名をマッチしていること
 - OUTPUTセクションのlog_group_templateパラメータ、log_stream_templateパラメータでCloudWatchLogsへのログ転送先を設定していること

ロググループの指定に利用した${ECS_CLUSTER}等の利用可能な変数については以下を参考にしました。
参考:  https://github.com/aws/aws-for-fluent-bit/blob/mainline/use_cases/init-process-for-fluent-bit/README.md#how-to-use-ecs-task-metadata-in-fluent-bit-config


# この構成のメリット・デメリット

例えばEC2で稼働していたアプリケーションをコンテナ化する際にログ出力の見直しが必要となりますが、
この構成のメリットはアプリケーション側の改修を発生させずログ管理の外出しが可能な点にあります。   

一方、Docker標準のロギング方法に沿っていないことや管理対象のコンテナが増えるため保守性が悪化すること、アプリケーションコンテナに割り当て可能なコンピューティングリソースが圧迫されてしまうことはデメリットになります。

複数種類のログが別々のフォーマットで出力されていたり、ログの識別情報がなく出力先のファイルでしか判別が難しい場合は、   
標準出力に切り替えるだけではなくフィルタリングのためのログレコードの改修が必要になります。   

原則としてはDocker標準の標準出力によるロギング方法に沿うのが幸せになれそうですが、   
上で挙げたような改修を選択できない場合はこのような構成を検討してみてはいかがでしょうか。