NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

AWS FireLensでECSコンテナのログをS3とCloudWatch logsに出力する

本記事は  【Advent Calendar 2023】  18日目の記事②です。
🎄  17日目  ▶▶  18日目記事① ▶▶  本記事 ▶▶  19日目  🎅

こんにちは。梅原です。

ECSコンテナのログは通常CloudWatch logsに出力されます。長期保管用としてS3にも出力したいといったこともあると思います。複数サービスへ出力したいときに使うのがログドライバーの1つであるAWS FireLensです。
今回はnginxのログをAWS FireLensを使ってS3とCloudWatch logsにログを出力してみます。

fluent bitのロゴはhttps://github.com/fluent/fluent-bit/blob/master/fluentbit_logo.pngより引用

ECSコンテナのログについて

コンテナでは、アプリケーションログの出力先は標準出力と標準エラー出力に設定しておきます。 そのログをログドライバーが処理し、様々なサービスに出力します。
AWSのコンテナサービスであるAmazon ECSでは、タスク定義でログドライバーを指定します。

CloudWatch logsのみへ出力するときは、タスク定義でCloudWatch logsへの出力を選択します。 ロググループやリージョン、ストリームのプレフィックスは出力先情報をオプションで渡します。

以下がタスク定義のコンテナ部分の例です。

{
    "containerDefinitions": [
        {
            "logConfiguration": {
                "logDriver": "awslogs",  # ログドライバーをawslogsに設定
                "options": {
                    "awslogs-group": "firelens-container",
                    "awslogs-region": "us-west-2",
                    "awslogs-create-group": "true",
                    "awslogs-stream-prefix": "firelens"
                }
            }
    ]
}

awslogs ログドライバーを使用する - Amazon Elastic Container Serviceより引用

CloudWatch logsへの出力は上記の通り簡単に設定できますが、標準出力と標準エラー出力が同じロググループとストリームに出力されてしまいます。 また、CloudWatch logsのログ取り込みや保存料金はS3と比較して高めです。

そこでアプリケーションのすべてのログを保管用途でS3に、ログ監視用で標準エラー出力をCloudWatch logsに出力したいといった要件が出ると思います。 この要件に対応できるのがawsfirelensログドライバーです。

AWS FireLensとは

AWS FireLensはログドライバーの1つで、OSSであるfluentbitもしくはfluentdと連携してAWS内外のサービスへログを出力することができます。
AWS FireLensを使うときは、fluentbitもしくはfluentdをログルーターコンテナとしてデプロイします。
アプリケーションログを標準出力と標準エラー出力に出しておくだけで、AWS FireLensがログルーターコンテナへ転送してくれます。アプリケーション側で対応する必要はありません。
ログを受け取ったログルーターコンテナがCloudWatch logsやS3、SplunkといったAWS内外のサービスへログを出力してくれます。

fluentbitやfluentdを使うことで

  • CloudWatch logsとS3といった複数サービスへの出力
  • 特定のログ形式にパース
  • 標準出力と標準エラー出力の出力先の分離
  • 不要なログを削ぎ落とす
  • ログにECSのメタデータ追加

といった多くのことができます。

FireLensではログルーターのOSSであるfluentbitとfluentdを選択できますが、AWSではより軽量であるfluentbitが推奨されています。 今回のブログでもfluentbitを例に話します。 docs.aws.amazon.com

AWS FireLensを使うときの方法

FireLensログドライバーを使うときの方法は3パターンあります。

1. タスク定義のみで完結

タスク定義でアプリケーションコンテナのログドライバーにawsfirelensを指定して、出力先情報をオプションに記載します。
オプションのNameで出力したいサービスを設定します。以下はFIreLensからCloudWatch logsに出力する例です。

環境変数に入れるだけの簡単設定ですが、複数出力先といった複雑な設定はできません。

{
    "containerDefinitions": [
        {
            "logConfiguration": {
                "logDriver": "awsfirelens",  # ログドライバーにawsfirelensを指定
                "options": {
                    "Name": "cloudwatch_logs", # 出力先はCloudWatch logs
                    "region": "ap-northeast-1",
                    "log_group_name": "/ecs/demo/nginx-from-fluent-bit",
                    "auto_create_group": "true",
                    "log_stream_prefix": "ecs/"
                }
            }
    ]
}

2. カスタムイメージを作成する

自前で設定ファイルを埋め込んだdockerイメージをビルドし、タスク定義でそのイメージを指定する方法です。
AWSはfluentbit用のdockerイメージを公開しています。
このdockerイメージをベースイメージとして、fluentbitの設定ファイルをCOPYしてイメージをビルドします。 ビルドしたイメージをECRへプッシュし、タスク定義の中でイメージのURIを指定することで、作成した設定ファイルから起動するfluentbitを使用する事ができます。

この方法ではイメージの管理の手間がかかってしまい、デプロイの方法も考えなければいけません。

3. S3から設定ファイルを取得する

AWSが提供しているfluentbitのdockerイメージのタグにはinit-から始まるものがあります。
このイメージを使うことで、ログルーターコンテナが起動時にS3に置かれた設定ファイルを取得し、自動的に設定を読み込みます。設定ファイルを埋め込むためにイメージからビルドをする必要がなくなります。

起動時間が遅くなったり、getobjectの料金もかかる点には注意が必要です。

設定ファイルのオブジェクト情報は、タスク定義のログルーターコンテナの環境変数で、nameにaws_fluent_bit_init_file_{連番}を、valueにS3オブジェクトのARNを指定します。そうするだけで起動時に設定ファイルのS3オブジェクトを取得し、fluentbitが設定ファイルを読み込みます。

github.com

どのパターンを採用すべきか

fluentbitで複数出力先といった設定が不要な場合は、環境変数だけで完結するパターン1をおすすめします。
パターン2は先述の通りイメージの管理やデプロイ方式の検討が必要になります。そのため、S3から設定ファイルを読み込むパターン3をおすすめします。
パターン3で設定を変更したい場合は、S3に新しい設定ファイルをアップロードし、タスクの更新だけでよくなります。

fluentbitの設定ファイルの書き方

fluentbitの設定ファイルは4つのセクションに分かれます。

  • Service
    Fluent Bitの全体の動作に関わる設定
  • Input
    入力プラグインの指定
  • Filter
    ログのパースなどの加工
  • Output
    マッチするログの出力設定

設定できるパラメータはドキュメントの参照お願いします。

Configuration File - Fluent Bit: Official Manual
S3へ出力するときのOutput設定
Amazon S3 - Fluent Bit: Official Manual
CloudWatch logsへ出力するときのOutput設定
Amazon CloudWatch - Fluent Bit: Official Manual

また設定ファイルが長くなるのを回避するために、@INCLUDEで外部の設定ファイルを読み込むことができます。

少し話が逸れますが、
initタグがついているfluentbitイメージでは、S3から取得してきた設定ファイルを@INCLUDEで読み込んでいます。

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

fluentbitの実行コマンドで上記の設定ファイルが渡され、@INCLUDEで基本設定がされたファイルとS3に置かれたファイルが読み込まれています。 基本設定のファイルは以下です。INPUTセクションがあるので自分で設定ファイルには書く必要はありません。

sh-4.2# cat /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 <クラスター名>
    Record ecs_task_arn <taskのARN>
    Record ecs_task_definition <タスク定義名:リビジョン>

[OUTPUT]
    Name null
    Match firelens-healthcheck

コンテナを起動したときに、クラスター名などが取得され設定ファイルに埋め込まれています。

FireLensを使うときの注意点

  • ECSタスクの権限
    ログルーターとしてデプロイされたECSタスクからCloudWatch logsやS3に出力するので、タスクロールに権限を付与する必要があります。

  • ECSのコンテナリソース
    先述の通りECSタスクがデプロイされるので、コンテナのリソースはそれを見越して大きくしてあげる必要があります。

  • サイズが大きいログの取り扱い
    16KB以上のログは分割されて送られてしまいます。そういったログはfluentbit内でMultiline Filter Pluginを使用して再結合してあげる必要あります。
    スタックトレースといった複数行のログもMultiline Filter Pluginで結合が可能です。

実際にやってみる

今回はnginxのすべてのログをcloudWatch logsとS3に出力したいと思います。

fluentbitの設定

fluentbitの設定ファイルを作成し、S3へアップロードしておきます。

# グローバルな設定
[SERVICE]
    # nginx用でパースするためパーサーを読み込む
    Parsers_File fluent-bit/parsers/parsers.conf
    Flush 1
    Grace 30

# nginx用にパースする
[FILTER]
    Name parser
    Match *
    Key_Name log
    Parser nginx 

# CloudWatch logsへの出力
[OUTPUT]
    Name cloudwatch_logs
    Match  *
    region ap-northeast-1
    log_group_name /ecs/k-umehara-nginx-from-fluent-bit
    log_stream_prefix nginx-
    auto_create_group true

# S3への出力
[OUTPUT]
    Name s3
    Match *
    bucket k-umehara-log-output
    region ap-northeast-1
    total_file_size  1M
    use_put_object On
    compression gzip
    s3_key_format /$TAG/%Y/%m/%d/%H/%M/%S/$UUID.gz

タスク定義の設定

タスク定義のコンテナ部分は以下です。

"containerDefinitions": [
        {
            "name": "web",
            "image": "nginx", # アプリケーションはnginx
            "cpu": 0,
            "portMappings": [
                {
                    "name": "nginx-80-tcp",
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "essential": true,
            "environment": [],
            "environmentFiles": [],
            "mountPoints": [],
            "volumesFrom": [],
            "ulimits": [],
            "logConfiguration": {
                "logDriver": "awsfirelens", # ログドライバーにawsfirelens
                "options": {},
                "secretOptions": []
            }
        },
        {
            "name": "log_router",
            # initタグがついたもの指定
            "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:init-latest",
            "cpu": 0,
            "memoryReservation": 50,
            "portMappings": [],
            "essential": true,
            "environment": [
                {
                    # nameにaws_fluent_bit_init_s3_{連番}
                    "name": "aws_fluent_bit_init_s3_1",
                    # valueに設定ファイルのS3オブジェクトのARN
                    "value": "arn:aws:s3:::k-umehara-log-conf/extra.conf" 
                }
            ],
            "mountPoints": [],
            "volumesFrom": [],
            "user": "0",
            "logConfiguration": {
                "logDriver": "awslogs", # fluentbit自体のログはCloudWatch logsに出力
                "options": {
                    "awslogs-create-group": "true",
                    "awslogs-group": "/ecs/k-umehara-firelens-log",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs"
                },
                "secretOptions": []
            },
            "firelensConfiguration": {
                "type": "fluentbit"
            }
        }
    ],

デプロイして確認

このタスク定義を元にECSサービスをデプロイします。nginxにアクセスし出力されたログを確認します。

CloudWatch logsの確認

CloudWatch logsにログが出力されていますね。 指定したロググループに出力され、プレフィックスもついてる事がわかります。

ログの中身を見てみると、nginxのログにparseされています。

S3バケットの確認

指定したS3バケットにもコンテナ毎で出力されていることがわかります。

アップロードされたオブジェクトも指定した形式で出力されています。

最後に

今回はAWS FireLensの説明から実際にやってみるところまで紹介しました。
AWS FireLensについての基礎的な話は理解できるようになったのではないでしょうか。
今回は基本的なことしかしてないですが、Filterを用いることでここでお伝えしたようなこともできるようになります。 ぜひ試してみてください。

お役に立てる部分があったら嬉しいです。

執筆者: 梅原 航 インフラエンジニア。 主にAWSを使ってます。