FireLensでカスタムログルーティングをしてみよう!(Part 2)

FireLensでカスタムログルーティングをしてみよう!(Part 2)
この記事をシェアする

はじめに

こんにちは、スカイアーチHRソリューションズのsugawaraです。以前の記事、『FireLensでカスタムログルーティングをしてみよう!(Part 1)』では、FireLensを用いてアプリケーションログをデフォルトのCloudWatch Logsではなく、S3へ送るということを実施しました。

今回はログレベルによってS3やCloudWatch Logsへのログの出し分けを行っていきたいと思います。また、その過程で不要なログのフィルタリングも設定していきます。

構成

下記が今回の記事で扱う全体像になります。FireLensを用いて、ログレベルに応じてアプリログをS3とCloudWatch Logsに出力します。

前回の記事との違いは下記の3つです。

  • ログの送付先が複数ある
  • ログレベルによって送付先が異なる
  • ログ送付先の設定ファイル(extra.conf)をS3バケットへ格納する

今回はログの送付先にS3とCloudWatch Logsの2つを指定します。複数の宛先を指定する場合、前回のようなタスク定義に直接設定を記述するという方法は使えません。代わりに、今回はS3に送付先を指定する設定ファイルのextra.confを格納します。また、送付先とともに、出力されるログレベルに応じてログを出し分けるように設定もしていきます。

本来、この設定ファイルはdockerfileとともにECRへプッシュして独自のイメージを作成する必要がありますが、今回はinitというイメージを使います。initイメージを使うことで、S3に設定ファイルを格納するだけで設定を反映させることができます。詳細を知りたい方は下記のリンクへ!
https://github.com/aws/aws-for-fluent-bit/tree/mainline/use_cases/init-process-for-fluent-bit

前提

FireLensでカスタムログルーティングをしてみよう!(Part 1)』が完了した設定の状態から始めます。Pythonのコードのみ再掲します。main.pyは、Flaskのアプリケーションログを標準出力にリダイレクトし、/の後ろの文言によって異なるログメッセージを出力するコードです。/infoならINFO用のログメッセージ、/errorであればERROR用のログメッセージが出力されます。

import logging
import sys
from flask import Flask

app = Flask(__name__)

# Flaskのログを標準出力にリダイレクト
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
app.logger.addHandler(handler)

# /infoならINFOログを出力と表示
@app.route("/info")
def hello_info():
    log_message = "This is a INFO log"
    app.logger.info(log_message)
    return f"<p>{log_message}</p>"

# /warningならWARNINGログを出力と表示
@app.route("/warning")
def hello_warning():
    log_message = "This is a WARNING log"
    app.logger.warning(log_message)
    return f"<p>{log_message}</p>"

# /errorならERRORログを出力と表示
@app.route("/error")
def hello_error():
    log_message = "This is an ERROR log"
    app.logger.error(log_message)
    return f"<p>{log_message}</p>"

if __name__ == "__main__":
    app.debug = True
    app.run(host="0.0.0.0", port=5000)

まだFargateでアプリコンテナを構築していないよ、という方は『CDKでECS Fargateを構築してみよう!』を、アプリコンテナはあるけどFireLensコンテナはないよ、という方は『FireLensでカスタムログルーティングをしてみよう!(Part 1)』を参考に設定してみてください!

また、設定ファイルを格納するS3バケットも予め作成しておきます。こちらのバケットには、フォルダを分けてログファイルも格納する予定となります。

構築手順

タスク定義の修正

タスク定義を修正していきます。修正したい点は下記の2ヶ所です。

  • FireLensコンテナのECRイメージ
  • FireLensコンテナの送付先

コンソール画面から修正していきます。更新したいタスク定義を開いたら、新しいリビジョンの作成を押下します。

前回の記事で使用していたECRイメージはAWSが公式に提供する、public.ecr.aws/aws-observability/aws-for-fluent-bit:stableというものでした。今回は、public.ecr.aws/aws-observability/aws-for-fluent-bit:init-latestを使用します。

次に環境変数には設定ファイルを格納するS3の情報を入力します。キーにはaws_fluent_bit_init_s3_1、値には設定ファイルをいれるS3バケットのARNを指定します。
※S3バケット自体は複数の指定が可能であり、入力したキーの末尾の数字が被らなければOKです。

JSONで直接編集する場合には、下記のようになります。

"environment": [
    {
        "name": "aws_fluent_bit_init_s3_1",
        "value": "arn:aws:s3:::sample-firelens-bucket/extra.conf"
    }
],

タスク定義の修正が完了したら、作成を押下します。これでS3にある設定ファイルを読み取り、指定したログの送付先にログをルーティングできます。次はカスタムログルーティングを可能にする設定ファイルを作成、格納していきます。

設定ファイルの作成

予め作成しておいたS3バケットに下記のextra.confを格納します。内容を簡単に説明すると、ERRORの文言を含むログは指定したS3バケットのフォルダに送付するというものです。

[FILTER]
    Name          rewrite_tag
    Match         *-firelens*
    Rule          $log (ERROR) error-$container_id true

[OUTPUT]
    Name s3
    Match error-*
    region ap-northeast-1
    bucket sample-firelens-bucket/error-log
    total_file_size 1M
    upload_timeout 1m
    use_put_object On
    s3_key_format  /%Y%m%d_%H:%M:%S

今回もこれまでと同様に、ALBのDNS名でアクセスして必要に応じて/errorや/warningなどでログの出し分けをします。ECSサービスにてサービスの更新を押下し、最新のタスク定義でデプロイします。

/errorで何度かアクセスした後にS3バケットを確認すると、設定ファイル内で指定したようにフォルダが作成されています。

下記がバケットに保存されたログとなります。仕込んでいたエラーメッセージ以外に、コンテナIDやコンテナ名、ECSクラスター名などもいっしょに出力されています。

{"date":"2023-09-18T07:49:19.464286Z","source":"stderr","log":"[2023-09-18 07:49:19,464] ERROR in main: This is an ERROR log","container_id":"c657190c744e4d88902602fd0a30a90d-265927825","container_name":"web","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/c657190c744e4d88902602fd0a30a90d","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:15"}
{"date":"2023-09-18T07:49:19.464284Z","container_id":"c657190c744e4d88902602fd0a30a90d-265927825","container_name":"web","source":"stdout","log":"This is an ERROR log","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/c657190c744e4d88902602fd0a30a90d","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:15"}
{"date":"2023-09-18T07:49:22.146875Z","container_id":"c657190c744e4d88902602fd0a30a90d-265927825","container_name":"web","source":"stderr","log":"[2023-09-18 07:49:22,146] ERROR in main: This is an ERROR log","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/c657190c744e4d88902602fd0a30a90d","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:15"}
{"date":"2023-09-18T07:49:22.146912Z","log":"This is an ERROR log","container_id":"c657190c744e4d88902602fd0a30a90d-265927825","container_name":"web","source":"stdout","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/c657190c744e4d88902602fd0a30a90d","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:15"}
{"date":"2023-09-18T07:49:23.592898Z","log":"[2023-09-18 07:49:23,592] ERROR in main: This is an ERROR log","container_id":"c657190c744e4d88902602fd0a30a90d-265927825","container_name":"web","source":"stderr","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/c657190c744e4d88902602fd0a30a90d","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:15"}
{"date":"2023-09-18T07:49:23.592960Z","log":"This is an ERROR log","container_id":"c657190c744e4d88902602fd0a30a90d-265927825","container_name":"web","source":"stdout","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/c657190c744e4d88902602fd0a30a90d","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:15"}

ECSのメタ情報が多くてログが見にくいため少しシンプルにしてみます。extra.confに下記のFILTERセクションを追加します。指定したコンテナ名など5つのキーをフィルタリングするという設定です。

[FILTER]
    Name record_modifier
    Match *
    Remove_key container_name
    Remove_key ecs_cluster
    Remove_key ecs_task_definition
    Remove_key ecs_task_arn
    Remove_key source

[FILTER]
    Name          rewrite_tag
    Match         *-firelens*
    Rule          $log (ERROR) error-$container_id true

[OUTPUT]
    Name s3
    Match error-*
    region ap-northeast-1
    bucket sample-firelens-bucket/error-log
    total_file_size 1M
    upload_timeout 1m
    use_put_object On
    s3_key_format  /%Y%m%d_%H:%M:%S

S3にextra.confをアップロードしたら、再デプロイして/errorで何度かアクセスします。すると、さきほどよりも情報量が少なくなり、ログメッセージやコンテナIDだけになりました。

{"date":"2023-09-18T11:38:30.349312Z","log":"This is an ERROR log","container_id":"30721c4a76ca4a03b627ff1930c8334b-265927825"}
{"date":"2023-09-18T11:38:30.349413Z","container_id":"30721c4a76ca4a03b627ff1930c8334b-265927825","log":"[2023-09-18 11:38:30,349] ERROR in main: This is an ERROR log"}
{"date":"2023-09-18T11:38:31.971962Z","log":"This is an ERROR log","container_id":"30721c4a76ca4a03b627ff1930c8334b-265927825"}
{"date":"2023-09-18T11:38:31.972065Z","log":"[2023-09-18 11:38:31,971] ERROR in main: This is an ERROR log","container_id":"30721c4a76ca4a03b627ff1930c8334b-265927825"}
{"date":"2023-09-18T11:38:34.218705Z","log":"This is an ERROR log","container_id":"30721c4a76ca4a03b627ff1930c8334b-265927825"}
{"date":"2023-09-18T11:38:34.218801Z","log":"[2023-09-18 11:38:34,218] ERROR in main: This is an ERROR log","container_id":"30721c4a76ca4a03b627ff1930c8334b-265927825"}

このような形で、収集不要なログ部分はフィルタリングが可能となります。上記の例はFargateのメタ情報の一部をフィルタリングしましたが、他にもALBのアクセスログなどもフィルタリングが可能です。

次に、warningのログをCloudWatch Logsへ出力してみます。設定ファイルを下記のように修正します。WARNINGの文言があれば、指定したロググループ名とプレフィックスでCloudWatch Logsへと出力します。

[FILTER]
    Name          rewrite_tag
    Match         *-firelens*
    Rule          $log (ERROR) error-$container_id true

[FILTER]
    Name          rewrite_tag
    Match         *-firelens*
    Rule          $log (WARNING) warning-$container_id false

[OUTPUT]
    Name s3
    Match error-*
    region ap-northeast-1
    bucket sample-firelens-bucket/error-log
    total_file_size 1M
    upload_timeout 1m
    use_put_object On
    s3_key_format  /%Y%m%d_%H:%M:%S

[OUTPUT]
    Name cloudwatch
    Match warning-*
    region ap-northeast-1
    log_group_name /ecs/firelens/
    log_stream_prefix test/
    auto_create_group true
    log_key log

CloudWatch Logsへ移動すると、指定したロググループが新たに新規作成されます。

ログストリームを確認すると、WARNINGを含むログが出力されていることがわかります。

このように、設定ファイルをいじるだけで簡単にアプリログの出し分けが可能になります。また、出力先もKinesis Data FirehoseやNew RelicなどのSaaSサービスに送ることもできます。

アプリログは肥大化しがちであり、デフォルトの設定のままだと割高なCloudWatch Logsにどんどん出力されてしまいます。今回紹介したようなフィルタリングを用いて、不要なログは送らずに必要なログのみを送るようにする。そして割高だけど検索性に優れたCloudWatch Logsとコストの低いS3などでログに応じて使い分けるといいでしょう。

おわりに

FireLensによるログのルーティングはいかがでしたか?設定自体はシンプルではありますが、ログのコストカットに便利な機能です。CloudWatch Logsのコストが気になる、アプリログの収集を柔軟に行いたい場合にはご活用ください!

この記事をシェアする
著者:sugawara
元高校英語教員。2023 Japan AWS All Certifications Engineers。IaCやCI/CD、Platform Engineeringに興味あり。