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

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

はじめに

こんにちは、スカイアーチHRソリューションズのsugawaraです。今回はFireLensを用いてアプリケーションログの送付先をいじってみたいと思います。

以前の記事にて、CDKを利用してECS Fargateを作成しました。今回はそれに設定を追加することで、デフォルトのCloudWatch Logsではなく、S3にログを送っていきます。

対象読者

  • FireLensをはじめて触る人
  • FireLensにちょっと興味がある人
  • Fargateのログルーティングをしてみたい人

構成図

Fargateのアプリケーションログは、デフォルトではCloudWatch Logsに出力されます。今回は出力先をS3に変更していきます。そのために利用するのがFireLensです。FireLensは、fluentbitもしくはfluentdを用いて、ECS コンテナのログを任意の宛先に転送する機能のことです。下記の構成図のように、FireLens用のコンテナを構築することでカスタムログルーティングが可能となります(サイドカー構成)。

作業の流れ

  1. ECS Fargateのデプロイ(以前の記事を参照)
  2. ECRリポジトリの作成
  3. ECRリポジトリへプッシュ
  4. Dockerfileなどの作成とプッシュ
  5. CloudWatch Logsのログ確認
  6. FireLensの設定
  7. S3のログ確認

構築手順

ECS Fargateのデプロイ

以前の記事で作成したECS Fargateを利用します。したがって、AWSの提供するサンプルPHPページが見れるところからスタートします。今回も引き続き、CDK周りはCloud9環境で実行していきます。

ECRリポジトリの作成

現在はAWS提供のイメージを使用して画面表示していますが、アプリケーションのログ出力のために、こちらで用意したイメージを使用してFargateを構築していきます。まずは新たにECRリポジトリを作成し、そちらにDockerfileをプッシュします。

ECRのコンソール画面よりリポジトリへ進み、「リポジトリを作成」を押下します。

リポジトリ名のみ任意の名前をつけ、あとはデフォルトのまま作成します。自分はecr-repo-for-firelensで作成しています。

作成したリポジトリからプッシュコマンドの表示を押下すると、ECRへプッシュするためのコマンドが確認できます。後ほどプッシュコマンドは使用します。

ECRリポジトリへプッシュ

以前の記事の/bin/sample-fargate.tsは下記のようになっています。

import ec2 = require('aws-cdk-lib/aws-ec2');
import ecs = require('aws-cdk-lib/aws-ecs');
import ecs_patterns = require('aws-cdk-lib/aws-ecs-patterns');
import cdk = require('aws-cdk-lib');

export { SampleFargateStack };

class SampleFargateStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Create VPC and Fargate Cluster
    // NOTE: Limit AZs to avoid reaching resource quotas
    const vpc = new ec2.Vpc(this, 'MyVpc', { maxAzs: 2 });
    const cluster = new ecs.Cluster(this, 'Cluster', { vpc });

    // Instantiate Fargate Service with just cluster and image
    new ecs_patterns.ApplicationLoadBalancedFargateService(this, "FargateService", {
      cluster,
      taskImageOptions: {
        image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
     },
    });
  }
}

下記のように、タスクイメージの取得先を公式のものから先ほど作成したものへ修正します。

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as ecr from 'aws-cdk-lib/aws-ecr';

export { SampleFargateStack };

class SampleFargateStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Create VPC and Fargate Cluster
    // NOTE: Limit AZs to avoid reaching resource quotas
    const vpc = new ec2.Vpc(this, 'MyVpc', { maxAzs: 2 });
    const cluster = new ecs.Cluster(this, 'Cluster', { vpc });
    
    // Create a reference to an existing ECR repository
    const repository = ecr.Repository.fromRepositoryAttributes(this, 'ExistingRepository', {
      repositoryName: 'ecr-repo-for-firelens', 
      repositoryArn: "arn:aws:ecr:ap-northeast-1:008458347550:repository/ecr-repo-for-firelens",
    });

    // Instantiate Fargate Service with just cluster and image
    new ecs_patterns.ApplicationLoadBalancedFargateService(this, "FargateService", {
      cluster,
      taskImageOptions: {
        image: ecs.ContainerImage.fromEcrRepository(repository),
        containerPort: 5000,
     },
    });
  }
}

修正したら、cdk deployしましょう。これでCDK部分は終わりです。

Dockerfileなどの作成とプッシュ

実際のアプリケーションコンテナの中身を準備していきます。今回はPythonのFlaskを用いてログの出力をさせます。下記はローカルPCで作成するディレクトリ構造です。

sample-app/
├── Dockerfile
├── main.py
└── requirements.txt     

Dockerfileでは、Python 3の入ったAlpine Linuxにrequirement.txtで指定したFlaskをインストールしています。

FROM python:3-alpine
WORKDIR /usr/src/code
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
flask

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)

上記が準備できたら、ECRへプッシュします。作成したリポジトリのプッシュコマンドを順番に実行します。下記は自分の環境でのプッシュコマンドとなります。

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com
$ docker build -t ecr-repo-for-firelens .
$ docker tag ecr-repo-for-firelens:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-repo-for-firelens:latest
$ docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-repo-for-firelens:latest

CloudWatch Logsのログ確認

実際にFargateを起動させ、ALBのDNS名からFargateにアクセスしてみましょう。下記のような画面が表示されたらOKです。

デフォルトでは、FargateのログはCloudWatch Logsへ出力されます。起動されているタスクの設定より、ログ出力の情報が確認できます。

ログのタブを押下すると、CloudWatch Logsへ出力されたログが確認できます。現在はステータス200が返ってきている状態です。

この状態で、ALBのDNS名の最後に/errorをつけてアクセスします。すると、画面は下記のような画面になります。

ログにもエラーログが出力されます。同様に、/infoや/warningも実行してみてください。main.pyで定義したログメッセージがそれぞれ出力されます。

FargateのアプリケーションログがCloudWatch Logsへ出力されることが確認できました。ここからFireLensの設定に入っていきます。

FireLensの設定

再度構成図で構築するものを確認します。アプリケーションログはFireLensコンテナを経由してS3へ出力されます。そのため、FireLensコンテナを追加し、ログルーティングの設定を行います。

タスク定義から、「新しいリビジョンの作成」を押下します。

アプリケーションコンテナとして、すでにコンテナ1のwebコンテナがあります。

オプションのログ記録にて、ログ収集方法としてCloudWatch Logsが指定されていますが、「AWS FireLens経由でS3にログをエクスポートする」を選択します。デフォルトの設定が出てきますので、ログを出力するバケット名だけ追加で入力します。

すると、AWSの公式イメージを使用するFireLens用のコンテナが表示されます。

下へスクロールしていって、ログ収集の使用にチェックを入れ、再度「Amazon CloudWatch」を選択します。これはFireLens自体のログの出力先の設定となります。FireLensの動作ログやエラーログを確認するために設定します。

最後に「作成」を押下して、新しいタスク定義のリビジョンを作成します。

作成後、ECSサービスを更新します。「新しいデプロイの強制」にチェックを入れて、リビジョンが最新であることを確認後、更新してください。現在のタスクが破棄され、最新のタスク定義をもとにタスクが起動されます。

S3のログ確認

新しくタスクが起動されたら、再度ALBのDNS名に/errorや/warningをつけてアクセスしてS3を確認してみましょう。なお、FireLensのデフォルトの設定では、バケット名/fluent-bit-logs/コンテナ名_コンテナID/yyyy/MM/dd/HH/mm/ファイル名 で格納されます。

下記はS3に出力されたログの一部です。/warningと/errorにアクセスした際のログメッセージがそれぞれ出力されているのがわかります。

{"date":"2023-08-21T14:12:48.212929Z","log":"[2023-08-21 14:12:48,212] WARNING in main: This is a WARNING log","container_id":"0579350cfbb3412693300bf1cd9c3427-265927825","container_name":"web","source":"stderr","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:008458347550:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/0579350cfbb3412693300bf1cd9c3427","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:13"}
{"date":"2023-08-21T14:12:48.212970Z","log":"This is a WARNING log","container_id":"0579350cfbb3412693300bf1cd9c3427-265927825","container_name":"web","source":"stdout","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:008458347550:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/0579350cfbb3412693300bf1cd9c3427","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:13"}
{"date":"2023-08-21T14:12:48.213313Z","container_id":"0579350cfbb3412693300bf1cd9c3427-265927825","container_name":"web","source":"stderr","log":"10.0.73.6 - - [21/Aug/2023 14:12:48] \"GET /warning HTTP/1.1\" 200 -","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:008458347550:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/0579350cfbb3412693300bf1cd9c3427","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:13"}
{"date":"2023-08-21T14:12:52.808454Z","container_id":"0579350cfbb3412693300bf1cd9c3427-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:008458347550:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/0579350cfbb3412693300bf1cd9c3427","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:13"}
{"date":"2023-08-21T14:12:52.808537Z","container_id":"0579350cfbb3412693300bf1cd9c3427-265927825","container_name":"web","source":"stderr","log":"[2023-08-21 14:12:52,808] ERROR in main: This is an ERROR log","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:008458347550:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/0579350cfbb3412693300bf1cd9c3427","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:13"}
{"date":"2023-08-21T14:12:52.810268Z","container_id":"0579350cfbb3412693300bf1cd9c3427-265927825","container_name":"web","source":"stderr","log":"10.0.73.6 - - [21/Aug/2023 14:12:52] \"GET /error HTTP/1.1\" 200 -","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:008458347550:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/0579350cfbb3412693300bf1cd9c3427","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:13"}
{"date":"2023-08-21T14:13:12.607001Z","container_name":"web","source":"stderr","log":"10.0.73.6 - - [21/Aug/2023 14:13:12] \"GET / HTTP/1.1\" 200 -","container_id":"0579350cfbb3412693300bf1cd9c3427-265927825","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:008458347550:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/0579350cfbb3412693300bf1cd9c3427","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:13"}
{"date":"2023-08-21T14:13:12.610997Z","source":"stderr","log":"10.0.32.194 - - [21/Aug/2023 14:13:12] \"GET / HTTP/1.1\" 200 -","container_id":"0579350cfbb3412693300bf1cd9c3427-265927825","container_name":"web","ecs_cluster":"SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:008458347550:task/SampleFargateStack-ClusterEB0386A7-VwOsG718oq1x/0579350cfbb3412693300bf1cd9c3427","ecs_task_definition":"SampleFargateStackFargateServiceTaskDefDBF5484B:13"}

これでアプリケーションのログをデフォルトのCloudWatch Logsではなく、S3へ出力することができました。上記のログは、特に絞っていないため、余分なログメッセージも多くてとても見づらい状態です。それもFireLensを用いることで、不要なログのフィルタリングが可能です。
※現在の設定では毎分S3へログがアップロードされる設定のため、コンテナの起動時間には注意してください。

なお、今回はECSコンソールの画面上から設定をしましたが、タスク定義はJSON形式で直接編集することも可能です。下記はアプリケーションコンテナとFireLensコンテナのログ設定部分の抜粋となります。

{
    ...
        {
            "name": "web",
            "image": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-repo-for-firelens:latest",
            "portMappings": [
                {
                    "name": "web-5000-tcp",
                    "containerPort": 5000,
                    "hostPort": 5000,
                    "protocol": "tcp"
                }
            ],
            "essential": true,
            "logConfiguration": {
                "logDriver": "awsfirelens",
                "options": {
                    "Name": "s3",
                    "bucket": "firelens-test-sugawara",
                    "region": "ap-northeast-1",
                    "total_file_size": "1M",
                    "upload_timeout": "1m",
                    "use_put_object": "On"
                },
        },
        {
            "name": "log_router",
            "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable",
            "essential": true,
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-create-group": "true",
                    "awslogs-group": "SampleFargateStack-FargateServiceTaskDefwebLogGroup71FAF541-tRRpF8UO8LcK",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "FargateService"
                },
            },
            "firelensConfiguration": {
                "type": "fluentbit"
            }
        }
   ...
}

おわりに

FireLensによるログのルーティングはいかがでしたか?今回はただS3にログを出力しましたが、次回はCloudWatch LogsとS3でログレベルによる出し分けや不要なログのフィルタリングなどをしていきたいと思います!

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