【AWS FireLens 徹底解説】カスタムFluent Bitイメージで複数種類のログを扱う

本文

こんにちは、スタメンの松谷です。 最近、Ruby on Railsアプリケーション環境をECSへ移行しましたが、ログ管理には FireLens for Amazon ECS (以下FireLens)という仕組みを利用しました。

この記事ではFireLensについて説明し、実際の要件にどのように対応したのかを共有します。

FireLens とは

FireLensは2019年にリリースされたECSのログ管理機構で、ECSで管理しているコンテナの loging driver に awsfirelens を指定することで、サイドカーコンテナとして Fluentd または Fluent Bit を起動し、メインコンテナからログを転送することができます。

FireLensの登場前は、ECSで管理しているコンテナの loging driver に awslogs を指定することで、標準出力を CloudWatch Logs に出力させる方法が主流でしたが、ログの種類に応じた転送先の振り分けやフィルター処理などは CloudWatch Logs側で対応する必要がありました。

FireLensを利用すれば、ECSタスク定義ファイルのみで 、Fluentd や Fluent Bit をサイドカーとして利用できます。また、独自に用意したFluentdやFluent Bitの設定ファイルを読み込むことにより、より柔軟なログのフィルターやルーティングを実現することができます。

Fluent Bit は Fluentd に比べてリソース使用量が少なく軽量なため、AWS はFluent Bitを推奨しています。以下は、Fluent Bitを例にして話を進めます。

ログ収集の要件

今回のログ収集の要件は以下です。

  • ECSタスクでは、PumaとNginxの2コンテナを定義しており、Nginxコンテナは1種類、Pumaコンテナは2種類の異なるログを出しており、それぞれ別のKinesis Firehoseエンドポイントへ転送すること
    • Pumaコンテナ
      • 標準出力されているPumaのログ
      • /rails/log/access/配下のファイルに出力されているユーザーの利用ログ(JSON)
    • Nginxコンテナ
      • 標準出力されているNginxのログ
  • ECSタスクが終了した際に、損失する未転送のログが最小限となるようにすること

コンテナ内にファイル出力されたログを取り扱うことは、AWSが用意しているFluent Bitイメージでは対応することができないため、独自に用意したFluent Bitの設定ファイルを含めたカスタムFluent Bit イメージを利用します。

環境

今回検証した環境は以下です。

バージョン
aws-for-fluent-bit 2.19.1
Fluent Bit 1.8.6
ECS コンテナエージェント 1.52.2
Docker バージョン 19.03.13-ce

実現方法

PumaコンテナとNginxコンテナの loging driver に awsfirelens を指定し、ログ収集用のサイドカーとしてFluent Bitを起動します。このサイドカーを log- routerコンテナ とします。各ログは以下の方法で収集することにしました。

  • NginxコンテナとPumaコンテナから標準出力されたログは、unix domain socket 経由で log-router コンテナに転送する
  • Pumaコンテナ内でファイル出力されたログはVolume Mountを使ってホストのディレクトリをコンテナにマウントし、Pumaコンテナと log-routerコンテナ間で共有する

また、損失する未転送のログが最小限とするために、詳解 FireLens を参考に以下の対応をしました。

  • ECSタスクの依存関係を調整し、タスク起動時には log-router コンテナを最初に起動し、タスク終了時には最後に log-router コンテナを停止するようにする
  • 未転送のログが最小限となるように最適化されたFluent Bit設定にすること

以下、Firelensの設定内容について説明します。

ECSタスク定義

上記を満たすECSタスク定義は以下です。(説明に不要なパラメータは省略しています)

(Fluent Bit Official Manualによると、Kinesis Data Firehoseの新プラグイン(kinesis_firehose)が推奨されていますが、ここでは旧プラグイン(firehose)を利用しています。)

{
  "containerDefinitions": [
    {
      "name": "puma",
      "image": "puma-image",
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "firehose",
          "region": "ap-northeast-1",
          "delivery_stream": "puma"
        }
      },
      "mountPoints": [
        {
          "containerPath": "/rails/log/access",
          "sourceVolume": "log-volume"
        }
      ],
      "dependsOn": [ { "containerName": "log-router", "condition": "HEALTHY" } ]
    },
    {
      "name": "nginx",
      "image": "nginx-image",
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "firehose",
          "region": "ap-northeast-1",
          "delivery_stream": "nginx"
        }
      },
      "dependsOn": [ { "containerName": "log-router", "condition": "HEALTHY" } ]
    },
    {
      "name": "log-router",
      "image": "custom-fluent-bit-image",
      "healthCheck": { "command":[ "CMD-SHELL", "echo '{\"health\": \"check\"}' | nc 127.0.0.1 8877 || exit 1" ] },
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": {
          "config-file-type": "file",
          "config-file-value": "/fluent_conf/fluent-bit.conf"
        }
      },
      "mountPoints": [ {
        "containerPath": "/rails/log/access",
        "sourceVolume": "log-volume"
      } ]
    }
  ],
  "volumes": [ {
    "name": "log-volume"
  } ]
}

ECSタスク定義で記述したFireLensの設定が、どのようにFluent Bitの設定に関連しているのかを確認します。上記のECSタスクを起動すると、log-routerコンテナ内の /fluent-bit/etc/fluent-bit.conf は以下のようになっています。(説明に不要なパラメータは省略しています)

[INPUT]
    Name tcp
    Listen 127.0.0.1
    Port 8877
    Tag firelens-healthcheck

[INPUT]
    Name forward
    unix_path /var/run/fluent.sock

[INPUT]
    Name forward
    Listen 127.0.0.1
    Port 24224

[FILTER]
    Name record_modifier
    Match *
    Record ecs_cluster production
    Record ecs_task_arn arn:aws:ecs:ap-northeast-1:${AWS::AccountId}:task/production/${ECS TaskID}
    Record ecs_task_definition puma-task:${revision number}

@INCLUDE /fluent_conf/fluent-bit.conf

[OUTPUT]
    Name null
    Match firelens-healthcheck

[OUTPUT]
    Name firehose
    Match puma-firelens*
    delivery_stream puma
    region ap-northeast-1
    time_key datetime

[OUTPUT]
    Name firehose
    Match puma_nginx-firelens*
    delivery_stream puma_nginx
    region ap-northeast-1
    time_key datetime

以下では、上記のFluent Bit設定とECSタスク定義について説明し、ECSタスク内の各種ログがどのように収集されているのかを確認していきます。

NginxとPumaの標準出力ログの設定

標準出力されたログを収集するだけであれば、FireLens で自動的に生成されるFluent Bit設定を利用するだけで済むので独自のFluent Bit 設定ファイルは必要ありません。

以下設定は、FireLens で自動的に生成される Fluent Bit設定(/fluent-bit/etc/fluent-bit.conf)の一部です。

[INPUT]
    Name tcp
    Listen 127.0.0.1
    Port 8877
    Tag firelens-healthcheck

[INPUT]
    Name forward
    unix_path /var/run/fluent.sock

[INPUT]
    Name forward
    Listen 127.0.0.1
    Port 24224

...省略

詳解 FireLensによると、

ドライバーは TCP と Unix の両方のソケットをサポートしていますが、 より高速でパフォーマンスの高いオプションである Unix ソケットを選択しました。

と記載がありますが、 /fluent-bit/etc/fluent-bit.conf の上から2番目と3番目の[INPUT]をみると、確かにFireLensに自動生成される設定では TCP と unix domain socket の両方をリッスンしていることが分かります。

log-routerコンテナ側で、unix domain socketをリッスンしていることは分かりましたが、Nginx と Pumaコンテナからは、どのようにunix domain socketへアクセスしているのでしょうか。

ECSタスクが動いているEC2インスタンスにログインして、以下コマンドで log-routerコンテナを確認します。

$ sudo docker inspect log-router

# Source属性はホスト側のホストマシン側のディレクトリ
# Destinationはコンテナ側のディレクトリ

...省略
{
  "Type": "bind",
  "Source": "/var/lib/ecs/data/firelens/${taskid}/socket",
  "Destination": "/var/run",
  "Mode": "",
  "RW": true,
  "Propagation": "rprivate"
},
...省略

docker inspectコマンドの結果より、ホストマシン側のディレクトリをlog-routerコンテナ側の/var/runディレクトリにマウントしていることが分かりました。これにより、他コンテナのlogging driverから/var/run/fluent.sock へアクセスすることができます。

また、Pumaコンテナの logging driver を確認すると、fluentd-address に log-routerコンテナがマウントしている unix domain socket が指定されていることも確認できます。

$ sudo docker inspect puma

...省略
  "HostConfig": {
    "LogConfig": {
      "Type": "fluentd",
      "Config": {
        "fluentd-address": "unix:///var/lib/ecs/data/firelens/#{taskid}/socket/fluent.sock",
        "fluentd-async-connect": "true",
        "fluentd-sub-second-precision": "true",
        "tag": "puma-firelens-#{taskid}"
       }
    },
   }
...省略

上記より、NginxとPumaコンテナの標準出力ログは、logging driver から unix domain socket 経由で log-router コンテナに転送されていることが分かりました。

参考: FireLens を使って fluentd logging driver 起因の fluentd の負荷を分散させる

Pumaコンテナ内にファイルに出力されるログの設定

次に、Pumaコンテナ内にファイル出力しているログをどのように収集しているかを説明します。

前述のECSタスク定義を確認すると、 log-volume というVolumeを作成し、Pumaコンテナ側でファイル出力されたログをlog-routerコンテナと共有するようにしています。このlog-routerコンテナへ共有されたファイル末尾への追記イベントを読み取るためには Tail プラグインを利用する必要がありますが、FireLens で自動的に生成されるFluent Bit設定ファイルでは利用することができないため、独自にFluent Bit設定ファイルを用意する必要があります。

独自の設定ファイルをFireLensに指定する方法は2パターンあり、S3に置いたFluent Bit設定ファイルを指定する以下の方法("config-file-type": "s3")と

{
   "containerDefinitions":[
      {
         "image":"fluent-bit-image",
         "name":"log-router",
         "firelensConfiguration":{
            "type":"fluentbit",
            "options":{
               "config-file-type":"s3",
               "config-file-value":"arn:aws:s3:::mybucket/fluent.conf"
            }
         }
      }
   ]
}

コンテナイメージ内またはコンテナにマウントされているボリューム上に存在するFluent Bit設定ファイルを読み込む以下の方法("config-file-type": "file")があります。

{
   "containerDefinitions":[
      {
         "image":"fluent-bit-image",
         "name":"log-router",
         "firelensConfiguration":{
            "type":"fluentbit",
            "options":{
               "config-file-type": "file",
               "config-file-value": "/custom.conf"
            }
         }
      }
   ]
}

現在(2021年11月時点)では、AWS Fargate でホストされるタスクは、file 設定ファイルタイプのみをサポートしているため、今後のFargateへの移行も見越して "config-file-type": "file" の方法を採用しています。

独自のFluent Bit設定ファイルは、以下のようにFluent Bitイメージ内に含めます。

FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:2.19.1
ADD fluent-bit.conf /fluent_conf/
ADD parsers.conf /fluent_conf/
[SERVICE]
    Parsers_File /fluent_conf/parsers.conf
    Flush 1
    Grace 30

[INPUT]
    Name tail
    Path /rails/log/access/access.log.*
    Tag access

[FILTER]
    Name parser
    Match access
    Key_Name log
    Parser accesslog_parser

[OUTPUT]
    Name firehose
    Match access
    region ap-northeast-1
    delivery_stream accesslog

また、Pumaコンテナ内でファイル出力されるログのフォーマットはJSONなので、以下のようにJSONパーサーの設定を追加し、fluent-bit.conf 側のParsers_Fileで指定します。

[PARSER]
    Name   accesslog_parser
    Format json

これで、Fluent Bitコンテナイメージ内に独自のFluent Bit設定ファイルを含めることができたので、ECSタスク定義のconfig-file-value属性でファイルの場所を指定すれば、/fluent-bit/etc/fluent-bit.conf 内の @INCLUDE /fluent_conf/fluent-bit.conf となっている箇所で読み込まれます。

これらにより、Pumaコンテナのファイル出力(/rails/log/access/access.log.production.*)が、log-router コンテナに共有され、Tailプラグインによってログが収集されるようになります。

ログ損失の最小化

ヘルスチェックの設定

以下は、/fluent-bit/etc/fluent-bit.conf の [INPUT] だけ切り出した内容です。 一番上の[INPUT]を見ると、port 8877 をリッスンしてFluent Bitのヘルスチェックを受け付けていることが分かります。

以下コマンドで、log-routerコンテナのヘルスチェックが可能です。

$ echo '{"health": "check"}' | nc 127.0.0.1 8877 || exit 1

ECSタスク定義のlog-routerコンテナの healthCheck属性のcommandに上記コマンドを設定し、以下のdependsOnオプションを設定すれば、log-routerコンテナが正常に起動してから、他コンテナを起動することができます。

"dependsOn": [ { "containerName": "log-router", "condition": "HEALTHY" } ]

Fluent Bit のパラメータ調整

前述のECSタスク定義のようにessentialパラメータを指定していない場合、全てのコンテナの essential パラメータは true となるため、PumaコンテナやNginxコンテナの終了はタスク終了のトリガーとなります。

ECSの StopTimeout パラメータはデフォルト値で30秒なので、StopTimeoutパラメータの指定がない場合、ECSは30秒の猶予期間をもって、Fluent Bit コンテナを終了します。

一方で、Fluent Bit側でGraceパラメータのデフォルト値は5秒です。Graceパラメータの指定がない場合、Fluent Bit はデフォルトでは SIGTERM を受け取ってから 5 秒しか待機せずにシャットダウンするため、ECSタスク定義側で設定されている30秒を全て利用していないことになります。[Service]の GraceパラメータをECSタスク定義側のStopTimeout値と合わせることで、ECS側の猶予期間を最大限に活用することができます。

また、Fluent Bit の Flushパラメータはデフォルトで5秒なので、この値を小さくすれば転送頻度を上げることができます。

以上より、Fluent BitのGraceパラメータとFlushパラメータを調整することで、タスクが終了したときに、ログが宛先に到達する可能性を高くすることができます。

[SERVICE]
    Flush 1
    Grace 30

参考: 詳解 FireLens

まとめ

以上の設定によりログ収集の要件を満たすことができました。FireLensを利用することで、ECSのタスク定義だけでFluent Bitのほとんどの設定が完了します。ログの扱いをカスタマイズしたい場合は、今回のように独自の設定ファイルを含んだFluent Bitイメージを用意して読み込む方法が用意されているので、様々なケースに簡単に対応できそうだと感じました。


スタメンでは、今回紹介したようなクラウド基盤の設計や構築を一緒に取り組む仲間を募集しています!