FireLens fluent bit でログを振り分けたい場合、 fluent bit の設定ファイル内で Parsers_File などで指定した別のファイルを用いて、カスタム docker イメージを作成するサンプルが多いかと思いますが、カスタムイメージを作成することなく( Parsers_File 無しで)ささやかながら実現した例を紹介したいと思います。

前置き

コンテナのログを簡単に取り扱うためのツールをとして AWS は FireLens を発表しました (AWS Blog) 。それまではログドライバに awslogs を指定することで、コンテナの標準出力に書き出せば CloudWatch Logs にログが出力されるようになっていましたが、一方コンテナから出力されるログは、アクセスログやアプリケーションログ、エラーログといった複数の種類があり、種類に応じて出力先を振り分けたいといったニーズもあったのではないかと思います(そのために CloudWatch Logs にサブスクリプションフィルタを設定して Kinesis Firehose に送ったり、、、とかしていたかと思います)。

FireLens の登場により、共に動作する Fluent Bit や Fluentd を利用することでログの振り分けが簡単に実装できるようになり、実際、 AWS Blog などでも紹介されています。しかし前述の通り、多くの実装例では fluent bit の設定ファイルに Parsers_FileStreams_File で指定したさらに別のファイルを用意し、さらにそれらの設定ファイルを同梱した fluent bit のカスタムイメージを作成する必要があります。fluent bit の設定ファイルは s3 から読み込むことができますが、残念ながら Parsers_FileStreams_File はローカルから読み込む必要があり、そのためカスタムイメージを作成する必要が出てきます。

カスタムイメージをメンテしていくめんどくささも考慮すると、極力デフォルトのイメージのままでコンテナログを取り扱いたいものです。そこで本記事では rewrite_tag を使ったログの振り分け方法を紹介したいと思います。あまり難しいことをしない、シンプルなログ振り分けであればこちらの方が簡単ではないかと思います。

アーキテクチャ

今回は以下のような構成で試してみました(バージニア北部リージョンを使用しています)。
Amazon ECS 上に Nginx コンテナと Fluent Bit コンテナでタスクを構成し、Nginx のログの内容に応じて、CloudWatch Logs、Kinesis Firehose への振り分けと、ちょうどつい先日、Fluent Bit から Amazon Elasticsearch Service へのログルーティングがサポート しましたので、Elasticsearch への振り分けも設定してみたいと思います。
訳あって EC2 ベースですが、Fargate でもだいたい同じようにできるのではないかと思います。

アーキテクチャ図

ログ振り分けのルール

(意味があるのかどうかはさておき)以下のように振り分けてみたいと思います。

  • Nginx のログに error や 4xx、5xx 系の出力があれば CloudWatch Logs へ
  • ALB からのヘルスチェックによるアクセスログは CloudWatch Logs にも Elasticsearch にも出力されないように
  • エラーでもヘルスチェックでもないログ(正常なリクエスト)であれば Elasticsearch に
  • 全てのログは Firehose(から S3)に
  • (※ Fluent Bit コンテナ自身のログは CloudWatch Logs に出力されます)

設定方法など

タスク定義やタスクロール、タスク実行ロール、そして Fluent Bit の設定について以下に記載していきます。Firehose や Elasticsearch などの設定、デプロイメントについては触れていない点ご了承ください。

タスク定義

まずタスク定義です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
{
    "family": "test-firelens",
    "taskRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/taskRole_with_FireLens",
    "executionRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole_with_FireLens",
    "cpu": "256",
    "memory": "256",
    "networkMode": "bridge",
    "requiresCompatibilities": [
        "EC2"
    ],
    "containerDefinitions": [
        {
            "name": "nginx",
            "image": "xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/my-nginx:latest",
            "cpu": 128,
            "memoryReservation": 128,
            "portMappings": [
                {
                    "containerPort": 80,
                    "protocol": "tcp"
                }
            ],
            "essential": true,
            "environment" : [
                { "name" : "APPVER", "value" : "1.7" },
                { "name" : "ENV", "value" : "dev" }
            ],
            "healthCheck": {
                "command": [ "CMD-SHELL", "echo 'Hello' || exit 1" ]
            },
            "logConfiguration": {
                "logDriver":"awsfirelens"
            },
            "dependsOn": [
                {
                    "containerName": "log-router",
                    "condition": "HEALTHY"
                }
            ]
        },
        {
            "name": "log-router",
            "image": "906394416424.dkr.ecr.us-east-1.amazonaws.com/aws-for-fluent-bit:latest",
            "essential": true,
            "environment" : [
                { "name" : "APPVER", "value" : "1.5" },
                { "name" : "ENV", "value" : "dev" }
            ],
            "healthCheck": {
                "command": [ "CMD-SHELL", "echo 'Hello' || exit 1" ]
            },
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/test-firelens",
                    "awslogs-region": "us-east-1",
                    "awslogs-stream-prefix": "log-router",
                    "awslogs-create-group": "true"
                }
            },
            "firelensConfiguration": {
                "type": "fluentbit",
                "options": {
                    "config-file-type": "s3",
                    "config-file-value": "arn:aws:s3:::my-bucket/test-firelens.conf"
                }
            }
        }
    ]
}

ハイライトしたところを補足しますと、

  • 32行目 : nginx コンテナのログドライバを awsfirelens に設定しています。
  • 34-39行目 : 訳あってネットワークモードが bridge であるために、コンテナの起動順序の依存関係を設定しています(参考)。また、依存関係設定のためコンテナのヘルスチェック設定が必要になりますが、今回は簡単に echo 'Hello' を設定しています。本番での利用の際には適切なヘルスチェックを設定しましょう。
  • 53行目 : fluent bit のコンテナである log_router には ログドライバとして awslogs に設定しています。
  • 64-65行目 : fluent bit の設定ファイルを s3 上から読み取るようにしています。

このタスク定義を以下のようなコマンドで登録します。

aws ecs register-task-definition --cli-input-json file://./taskdef.json

IAM ロール

続いて必要な IAM Role について。タスクロールとタスク実行ロールを紹介します。

タスクロールのポリシー

タスク定義の 3 行目( arn:aws:iam::xxxxxxxxxxxx:role/taskRole_with_FireLens )で指定しているロールのポリシーです。
Resource の対象ロググループ名、ストリーム名は環境に合わせて変更してください。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "firehose:PutRecordBatch",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:us-east-1:xxxxxxxxxxxx:log-group:/ecs/test-firelens:log-stream:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "es:*",
            "Resource": "*"
        }
    ]
}

タスク実行ロールのポリシー

タスク定義の 4 行目( arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole_with_FireLens )で指定しているロールのポリシーです。
簡単のため、タスク実行ロールにはあらかじめ以下の管理ポリシーを付与しておきます。

  • CloudWatchAgentServerPolicy
  • AmazonECSTaskExecutionRolePolicy

上記の管理ポリシーに加えて以下のポリシーを付与します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "firehose:PutRecordBatch",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": [
                "arn:aws:s3:::my-bucket/test-firelens.conf"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "s3:GetBucketLocation",
            "Resource": "arn:aws:s3:::my-bucket"
        }
    ]
}
  • 13 行目 : Fluent Bit の設定ファイルを読み取るための権限をここで付与しています。タスク定義の 63行目と同じファイルが相当します。

Fluent Bit の設定

本題となる Fluent Bit の設定ファイルです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
[SERVICE]
    Flush 1
    Log_Level info

#
# ログの振り分けとは関係ないのですが、modify を使って log_router コンテナに
# 設定された環境変数を `ENV` という名前で追加してみます
#
[FILTER]
    Name modify
    Match *
    Add ENV ${ENV}

#
# Nginx のアクセスログの ALB からのヘルスチェックにマッチするログについては
# "healthcheck.<year>" というタグに書き換えます
#  ・ <year> : アクセスログから取得した年
# (西暦年に意味はないのですが、マッチさせた文字をタグに指定できる例として設定してみました)
#
[FILTER]
    Name          rewrite_tag
    Match         ^nginx-firelens-.*
    Rule          $log \[\d{2}\/(\w+)\/(\d{4})\:\d{2}\:\d{2}\:\d{2}\s\+\d{4}\].*(ELB-HealthChecker\/2\.0) healthcheck.$2 false

#
# Nginx のアクセスログもしくはエラーログに `4xx`、や `5xx`、`error` の文字があれば
# "error.<Container ID>" というタグに書き換えます
# ・<Container ID> : ECS から渡されてくるコンテナ ID
#
[FILTER]
    Name          rewrite_tag
    Match         ^nginx-firelens-.*
    Rule          $log (error|\s4\d{2}\s|\s5\d{2}\s) error.$container_id false

#
# CloudWatch Logs への出力
# `Match` にあるように、タグが `error.*` であるログを CloudWatch に出力するようにしてみます。
# アスタリスクがワイルドカードとして使えます
# 正規表現を使いたい場合は `Match_Regex` を使います。
#
# `log_key` を指定すると JSON 中のどのキーをログとして出力するか指定できます
# 指定しない場合は JSON が全て出力されます
#
# ログストリームは `log_stream_prefix + タグ` となるようで、今回の例だと以下のようになります。
# "nginx-error-1.5.xxxxxxxxxxxx"#
#
[OUTPUT]
    Name cloudwatch
    Match error.*
    log_key log
    region us-east-1
    log_group_name /ecs/test-firelens
    log_stream_prefix nginx-
    auto_create_group false

#
# Kinesis Firehose への出力
# ストリーム名が firelens である firehose に全てのログ(`Match` が *)を転送
#
[OUTPUT]
    Name firehose
    Match *
    region us-east-1
    delivery_stream firelens

#
# Elasticsearch への出力
# `Match_Rexex` で指定しているタグは `コンテナ名 + firelens-*` であり、デフォルトのタグになります
# つまりはどちらの rewrite_tag FILTER にもマッチしなかったログが Elasticsearch に転送されます
#
# `Logstash_〜` のあたりを設定してあげることで、Elasticsearch の index を日付に応じて分割できたりもします
# この時の時刻は Fluent Bit が受け取った時の時刻(ログ発生時刻ではない)になる点が注意です
#
[OUTPUT]
    Name es
    Match_Regex ^nginx-firelens-.*
    Host vpc-xxxxx-xxxxxxxxxx.us-east-1.es.amazonaws.com
    Port  443
    Type doc
    AWS_Auth On
    AWS_Region us-east-1
    tls On
    Generate_ID On
    Logstash_Format On
    Logstash_Prefix firelens
    Logstash_DateFormat %Y-%m
    Include_Tag_Key On
    # デバッグ用
    #Trace_Output On

#
# デバッグ用に標準出力(すなわち Fluent Bit の標準出力なので CloudWatch Logs へ出力)する場合
#
#[OUTPUT]
#    Name stdout
#    Match *

CloudWatch Logs への Output について、52行目で log_key を指定しています。log_key を指定すると JSON の中の該当するキー項目のみを出力します。
その JSON の一例ですが下記のようになっています。

{
    "container_id": "xxxxxxxxxxxxxxx",
    "container_name": "/ecs-test-firelens-1-nginx-xxxxxxxxxxxxxx",
    "ec2_instance_id": "i-xxxxxxxxxx",
    "ecs_cluster": "cluster-name",
    "ecs_task_arn": "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:task/cluster-name/xxxxxxxxxx",
    "ecs_task_definition": "test-firelens:1",
    "log": "10.254.1.125 - - [19/Jul/2020:08:19:41 +0000] \"GET /firelens/abc HTTP/1.1\" 404 555 \"-\" \"Mozilla/5.0 .....\"",
    "source": "stdout",
    "ver": "1.5"
}

今回 log_keylog を指定していますので、以下が出力されます。 log_key を指定しない場合は上記の JSON が出力されます。

10.254.1.125 - - [19/Jul/2020:08:19:41 +0000] "GET /firelens/abc HTTP/1.1" 404 555 "-" "Mozilla/5.0 ....."

出力結果

CloudWatch Logs

下図のようにログストリームが作成されました。

ログストリーム

log-router/log-router/... は Fluent Bit コンテナのログにになります。)

nginx-error..... の中を見てみると、下図のように error4xx にマッチしたログのみ出力されています。

エラーログ

Elastisearch (Kibana)

ヘルスチェックにも、エラー系にもマッチしなかった通常のアクセスのみが記録されています。

Elastisearch

Firehose (S3)

Firehose の配信先である S3 には以下の通り、全てのログが出力されています(見やすいように prettify しています)。

{
    "ENV": "dev",
    "container_id": "9fc09b.....",
    "container_name": "/ecs-test-firelens-1-nginx-b0cdb.....",
    "ec2_instance_id": "i-.....",
    "ecs_cluster": "<cluster_name>",
    "ecs_task_arn": "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:task/<cluster_name>/38a4f.....",
    "ecs_task_definition": "test-firelens:1",
    "log": "10.254.1.125 - - [19/Jul/2020:08:19:22 +0000] \"GET /firelens/ HTTP/1.1\" 200 131 \"-\" \"ELB-HealthChecker/2.0\"",
    "source": "stdout"
}
{
    "ENV": "dev",
    "container_id": "9fc09b.....",
    "container_name": "/ecs-test-firelens-1-nginx-b0cdb.....",
    "ec2_instance_id": "i-.....",
    "ecs_cluster": "<cluster_name>",
    "ecs_task_arn": "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:task/<cluster_name>/38a4f.....",
    "ecs_task_definition": "test-firelens:1",
    "log": "10.254.0.151 - - [19/Jul/2020:08:19:22 +0000] \"GET /firelens/ HTTP/1.1\" 200 131 \"-\" \"ELB-HealthChecker/2.0\"",
    "source": "stdout"
}
{
    "ENV": "dev",
    "container_id": "9fc09b.....",
    "container_name": "/ecs-test-firelens-1-nginx-b0cdb.....",
    "ec2_instance_id": "i-.....",
    "ecs_cluster": "<cluster_name>",
    "ecs_task_arn": "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:task/<cluster_name>/38a4f.....",
    "ecs_task_definition": "test-firelens:1",
    "log": "10.254.1.125 - - [19/Jul/2020:08:19:39 +0000] \"GET /firelens/ HTTP/1.1\" 200 131 \"-\" \"Mozilla/5.0 .....\"",
    "source": "stdout"
}
{
    "ENV": "dev",
    "container_id": "9fc09b.....",
    "container_name": "/ecs-test-firelens-1-nginx-b0cdb.....",
    "ec2_instance_id": "i-.....",
    "ecs_cluster": "<cluster_name>",
    "ecs_task_arn": "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:task/<cluster_name>/38a4f.....",
    "ecs_task_definition": "test-firelens:1",
    "log": "10.254.1.125 - - [19/Jul/2020:08:19:41 +0000] \"GET /firelens/abc HTTP/1.1\" 404 555 \"-\" \"Mozilla/5.0 .....\"",
    "source": "stdout"
}
{
    "ENV": "dev",
    "container_id": "9fc09b.....",
    "container_name": "/ecs-test-firelens-1-nginx-b0cdb.....",
    "ec2_instance_id": "i-.....",
    "ecs_cluster": "<cluster_name>",
    "ecs_task_arn": "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:task/<cluster_name>/38a4f.....",
    "ecs_task_definition": "test-firelens:1",
    "log": "2020/07/19 08:19:41 [error] 7#7: *29 open() \"/usr/share/nginx/html/abc\" failed (2: No such file or directory), client: 10.254.1.125, server: , request: \"GET /firelens/abc HTTP/1.1\", host: \"xxx.xxx.xxx\"",
    "source": "stderr"
}

まとめ

FireLens Fluent Bit で rewrite_tag によるログの振り分けを試してみました。シンプルなユースケースであれば使えるのではないかと思います。Fluent Bit のファイルを S3 から読み取るようにすることで、カスタムイメージを作ることなく利用できますので運用面にも寄与できるのではないかと思います。

参考リンクなど

最後に・・・

この投稿は個人的なものであり、所属組織を代表するものではありません。ご了承ください。