22
14

More than 3 years have passed since last update.

FargateのログをFireLens(カスタマイズしたfluent-bit)を使ってCloudWatchとFirehoseの両方に送る

Last updated at Posted at 2020-09-17

やりたいこと

ECS Fargateで動いているRailsのアプリケーションログをFirelens(カスタマイズしたfluent-bitのイメージ)を使ってCloudWatchと2つのFirehoseにそれぞれ送りたい。

①ECS Fargate -> Firehose -> S3 (foo_log) -> Glue -> Athena
②ECS Fargate -> Firehose -> S3 (bar_log) -> Glue -> Athena
③ECS Fargate -> CloudWatch (全てのログ)

にログを送りたいという想定。
(fooとbarはある特定の条件のログのみを抽出したいという意)

構成図

image.png
今回は青い部分の作業手順をまとめます。

作業手順

  1. リソース作成
  2. CloudWatchと2つのFirehoseに送るようにfluent-bitイメージをカスタマイズする
  3. log-routerコンテナを定義
  4. カスタマイズしたfluent-bitイメージでlog-routerコンテナを動かしてログを収集する

1. リソース作成

  • firehose(今回は各firehoseごとにIAMロールを作成する想定)
    • foo_firehose
      • foo_firehose_role
    • bar_firehose
      • bar_firehose_role
  • cloudwatch
  • S3

    • foo_bucket
    • bar_bucket
  • ECRリポジトリ(画像にはないがカスタマイズしたfluent-bitイメージを管理する)
    https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/repository-create.html
    を参考に、カスタマイズしたfluent-bitのイメージを置くためのECRリポジトリを作成。

また各ロールに必要なポリシーをアタッチします。

ecs_task_role

  • firehoseにアクセスするためのPolicyをattach
ecs_task_firehose_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "firehose:PutRecordBatch",
            "Resource": [
                "arn:aws:firehose:{{region}}:{{account-id}}:deliverystream/foo-log",
                "arn:aws:firehose:{{region}}:{{account-id}}:deliverystream/bar-log"
            ]
        }
    ]
}
  • CloudWatchにアクセスするためのPolicyをattach arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy

foo_firehose_role

foo_firehoseがfoo_bucketにアクセスできるように、foo_firehose_roleにPolicyをattachする

foo_firehose_role_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",      
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [        
                "arn:aws:s3:::foo-bucket",
                "arn:aws:s3:::foo-bucket/*"         
            ]
        }
    ]
}

bar_firehose_role

bar_firehoseがbar_bucketにアクセスできるようbar_frehose_roleにPolicyをattachする

bar_firehose_role_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",      
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [        
                "arn:aws:s3:::bar-bucket",
                "arn:aws:s3:::bar-bucket/*"         
            ]
        }
    ]
}

2. CloudWatchと2つのFirehoseに送るようにfluent-bitイメージをカスタマイズする

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/using_firelens.html
ここを読むと、firelensではAWSが提供しているAWS for Fluent Bitを使うこともできます。
しかしこれではCloudWatchとFirehoseの両方にログを送ることができないので、今回はfluent-bit.confをカスタマイズして加えたDockerイメージを使うようにします。

カスタマイズしたconfが以下の2つです。

docker/fluent-bit/fluent-bit.conf
[SERVICE]
    Flush 1
    Parsers_File fluent-bit-parsers.conf

# logというkeyの中身をデコードする
[FILTER]
    Name parser
    Match *
    Key_Name log
    Parser rails
    # logの中身以外はいらないので消去する(container_idとか)
    Reserve_Data false

# ログにtagをつける
[FILTER]
    Name    rewrite_tag
    Match   *
    # nameがfooのもののタグをfoo_logにする
    Rule    $name ^(foo)$ foo_log true

[FILTER]
    Name    rewrite_tag
    Match   *
    # nameがbarのものはタグをbar_logにする
    Rule    $name ^(bar)$ bar_log true

# logの送信
# 全てのログ(rails-firelens-*というtagがついている)をCloudWatchに送る
[OUTPUT]
   Name                cloudwatch
   Match               rails-firelens-*
   region              {CloudWatchロググループのregion}
   log_group_name      {CloudWatchロググループの名前}
   # つけたいprefixを指定。ここではlatest/としている
   log_stream_prefix   latest/

# tagがfoo_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match foo_log
   region {foo_firehoseのregion}
   delivery_stream foo_firehose

# tagがbar_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match bar_log
   region {bar_firehoseのregion}
   delivery_stream bar_firehose
docker/fluent-bit/fluent-bit-parsers.conf
# ログがエンコードされてlogというkeyの中に入っているのでデコードするためのparser
# https://docs.fluentbit.io/manual/pipeline/parsers/decoders
[PARSER]
    Name         rails
    Format       json
    Time_Key     time
    Time_Format  %Y-%m-%dT%H:%M:%S %z
    # Command       |  Decoder  | Field | Optional Action   |
    # ==============|===========|=======|===================|
    Decode_Field_As    escaped     log

fluent-bitの公式ドキュメント
https://docs.fluentbit.io/manual/

基本的に公式ドキュメントに沿ってconfをカスタマイズしていきました。
ただドキュメントが全部英語なので、今回書いた部分だけ解説を残します。

解説

上から説明していきます。

[SERVICE]
    Flush 1
    Parsers_File fluent-bit-parsers.conf

[SERVICE]はログ全体の設定を定義するところです(多分)。

Flush 1ではドキュメントにあるようにログの出力間隔を指定しています。
https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/configuration-file

Flush
Set the flush time in seconds.nanoseconds. The engine loop uses a Flush timeout to define when is required to flush the records ingested by input plugins through the defined output plugins.

デフォルトだと5秒で、5秒だと例えばlog-routerコンテナが起動に失敗した時に、ログが出力される前にタスクがストップしてしまう、とかいうことがあったのでここでは1にしています。

Parsers_File fluent-bit-parsers.conf

ここではParserが書いてあるファイルパスを指定します。
独自でParserを作成した場合、このように別ファイルから読み込むようにするのがルール見たいです。Parserの解説は次にします。

ログの整形

Parser
https://docs.fluentbit.io/manual/pipeline/parsers/decoders を参考にして作成したParserのファイルです。
firelensは渡ってきたログに勝手に情報を付け加えるので、Parserに不要なログは切り落としてアプリケーションのログだけを収集するという設定をします。(実行はfluent-bit.confの[FILTER]で定義する)

fluent-bitは特にカスタマイズしないとこんな感じでログを送ります。

{
  "container_id": "3e638f00-c1a7-4794-b796-1d916dfa8cbc-1555792190",
  "container_name": "rails",
  "ecs_cluster": "{{cluster-name}}",
  "ecs_task_arn": "arn:aws:ecs:{{region}}:{{account-Id}}:task/{{task_id}}",
  "ecs_task_definition": "{{task-definition}}",
  "log": "{\"host\":\"xxxxxxxxx\",\"application\":\"xxxxxxxxxx\",\"environment\":\"xxxxxxx\",\"timestamp\":\"2020-01-01T00:00:00.00000Z\",\"level\":\"xxxx\",\"level_index\":x,\"pid\":xx,\"thread\":\"xxxxxx\",\"name\":\"xxxx\",\"message\":\"xxxxxx\"}

firelens側でcontainer_id とか container_name とか勝手につけてくれます。
そしてlogに実際にRailsアプリケーションが出したjsonのログがstringにエンコードされて入っています。
(今回はRailsアプリケーション側でもログはjsonで吐き出すように設定している)
これをjsonに戻すための設定を定義しているのが、このfluent-bit-parsers.confになります。

docker/fluent-bit/fluent-bit-parsers.conf
# ログがエンコードされてlogというkeyの中に入っているのでデコードするためのparser
# https://docs.fluentbit.io/manual/pipeline/parsers/decoders
[PARSER]
    Name         rails
    Format       json
    Time_Key     time
    Time_Format  %Y-%m-%dT%H:%M:%S %z
    # Command       |  Decoder  | Field | Optional Action   |
    # ==============|===========|=======|===================|
    Decode_Field_As    escaped     log

Name rails でこのParserに名前をつけています(自由です)。
一応命名規則としては、ログを吐き出したものの名前をつけるみたいで、今回扱うのはRailsが吐き出したログなのでrailsにしています。(ドキュメントではdockerになっている)

fluent-bit.confに戻ります。

docker/fluent-bit/fluent-bit.conf
# logというkeyの中身をデコードする
[FILTER]
    Name parser
    Match *
    Key_Name log
    Parser rails
    # logの中身以外はいらないので消去する(container_idとか)
    Reserve_Data false

ここで先ほど定義したParserを使って実行してね、という処理を書きます。
参考: https://docs.fluentbit.io/manual/pipeline/filters/parser

  • [FILTER]
    いろんな用途で使われるようで、用途ごとにNameを変えていきます。
    今回はParserを使いたいからName parserです。

  • Match *
    このFILTERに引っ掛けるログのタグを指定します。
    このタグについてはあとで解説します。
    *では全部のログがこのFILTERを通るという意味になります。

  • Key_Name log
    これがFILTERの対象にしたいログの中のKeyを指定するところですね。
    今回は log です。

  • Parser rails
    ここでどんなParserを使いたいか指定します。先ほど作成したrailsを指定。

  • Reserve_Data false
    これは Key_Name で指定した以外のKeyのデータはどうする?という意味になります。
    ここをfalseにすると指定した以外のKey、今回でいうとlogというKey以外は削除されます。
    よってログに実際のアプリケーションが出したログのみを出力することができます。
    もし、firelensが付与してくれたcontainer_idcontainer_nameとかも出力させたい!ということでしたらここをtrueにすれば出力されます。

ここまでがログをいい感じに整形する部分です。
次からはログを分割したい時の各設定です。
そこで必要な知識がさっき言ったログのタグです。

fluent-bitにおけるログのタグ

fluent-bit側で、ログの種類を分けるためにログにタグをつけることができます。
ただfluent-bitによって吐き出されたログがjsonの場合はそのタグの情報は見えません。
jsonでなければログは下のように出力されてタグも見えます。

[0] foo_log: [1598502349.876671400, {"host"=>"xxxxxxxxx", "timestamp"=>"2020-01-01T00:00:00.000000Z", "level"=>"xxxx", "level_index"=>0, "pid"=>00, "thread"=>"xxxxxxxxx", "name"=>"foo", "message"=>"xxxxxx"}}]

このfoo_logの部分がタグです。

また今回とっても大事なのが、firelensに入ってきたログにはデフォルトで
{container_name}-firelens-{task_id} というタグがつけられているということです。
今回だとrails-firelens-89caedfc-07fd-41f3-8239-5313a7d10ca7のようなタグが最初に全てのログにつけられています。
これをうまく使ってログの分割を行なっていきます。

ログのタグ付け

docker/fluent-bit/fluent-bit.conf
[FILTER]
    Name    rewrite_tag
    Match   *
    # nameがfooのもののタグをfoo_logにする
    Rule    $name ^(foo)$ foo_log true

[FILTER]
    Name    rewrite_tag
    Match   *
    # nameがbarのものはタグをbar_logにする
    Rule    $name ^(bar)$ bar_log true

ここも[FILTER]ですがName rewrite_tagとなっています。
その名の通りここでタグの上書きを行います。

  • Rule $name ^(foo)$ foo_log true
    参考: https://docs.fluentbit.io/manual/pipeline/filters/rewrite-tag#rules
    ドキュメントの通りRuleでは$KEY REGEX NEW_TAG KEEPを記述しています。
    [FIRTER]は記述した順番に処理されるらしく、ここに来るログはすでにlogというkeyの中身(applicationが吐き出したログそのもの)がやってきます。
    今回は nameというkeyにfoobarという文字が入っているという想定です。

  • $name ^(foo)$ foo_log true
    これはnameというKeyの中身がfooならfoo_logというタグをつける、という意味になります。
    (^(foo)$は正規表現です)

  • true
    はタグをつけた後、タグをつける前のログをとっておくかどうか、を表します。
    具体的にいうと、もともとは rails-firelens-* というタグがついていたけれど、foo_log というタグを上書きしたので、元の rails-firelens-* のタグがついている方は取っておくか?という意味です。
    今回はこの後の用途にて必要になるのでtrueにして取っておくようにしています。
    false にすれば元のログは削除され foo_log とついたもののみ、残ります。

ここまでで、ログをいい感じに整形して、タグをつけるところまできました。
あとはタグを使って出力先を指定するだけです。

ログの出力

docker/fluent-bit/fluent-bit.conf
# logの送信
# 全てのログ(*-firelens-*というtagがついている)をCloudWatchに送る
[OUTPUT]
   Name                cloudwatch
   Match               rails-firelens-*
   region              {CloudWatchロググループのregion}
   log_group_name      {CloudWatchロググループの名前}
   # つけたいprefixを指定。ここではlatest/としている
   log_stream_prefix   latest/

# tagがfoo_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match foo_log
   region {foo_firehoseのregion}
   delivery_stream foo_firehose

# tagがbar_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match bar_log
   region {bar_firehoseのregion}
   delivery_stream bar_firehose

このように[OUTPUT]を複数かくと、複数の場所に出力されます。

  • CloudWatch
docker/fluent-bit/fluent-bit.conf
[OUTPUT]
    Name                cloudwatch
    Match               rails-firelens-*

今回はCloudWatchに「applicationが吐き出した全てのログ」を送りたいと考えています。
タグがfoo_log、bar_log、そうでないもの全てです。
ここで、さっきrails-firelens-*というタグのログを残しておいたのが生きてきます。
foo_logやbar_logというタグとは別にrails-firelens-*というタグがついた状態で全てのログが残っているので、このタグのログを送ってあげれば全てのログが送られることになります。ですのでMatch rails-firelens-*と書きます。

  • Firehose
docker/fluent-bit/fluent-bit.conf
# tagがfoo_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match foo_log
   region {foo_firehoseのregion}
   delivery_stream foo_firehose

# tagがbar_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match bar_log
   region {bar_firehoseのregion}
   delivery_stream bar_firehose

ここも難しくはなく、Matchで送りたいログのタグを指定して、delivery_streamに送るfirehose名をきます。

カスタマイズしたconfを配置するDockerfileを書く。

作成した、fluent-bit.conffluent-bit-parsers.conf をコンテナのルートに配置するようにDockerfileを書きます。(ルートでなくても問題ない)

docker/fluent-bit/Dockerfile.
FROM amazon/aws-for-fluent-bit:latest
COPY ./docker/fluent-bit/fluent-bit.conf /fluent-bit.conf
COPY ./docker/fluent-bit/fluent-bit-parsers.conf /fluent-bit-parsers.conf

3.log-routerコンテナを定義

ecs-task-definition.jsonはこのように書きます。

ecs-task-definition.json
{
  "containerDefinitions": [
    # railsコンテナを定義
    {
      "command": [
        "bundle",
        "exec",
        "unicorn",
        "-p",
        "3000",
        "-c",
        "/rails/config/unicorn_ecs.rb"
      ],
      "cpu": 0,
      "dnsSearchDomains": [],
      "dnsServers": [],
      "dockerSecurityOptions": [],
      "entryPoint": [],
      "environment": [],
      "essential": true,
      "image": "Railsサービスを動かすイメージを指定する"
      "links": [],
      # ここでrailsコンテナのログドライバーをfirelensにする
      "logConfiguration": {
        "logDriver": "awsfirelens"
      },
      "mountPoints": [],
      "name": "rails",
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000,
          "protocol": "tcp"
        }
      ],
      "volumesFrom": []
    },
    # log-routerコンテナを定義
    {
      "essential": true,
      "image": "カスタマイズしてbuild,pushしたECRにあるfluent-bitのイメージを指定",
      "name": "log-router",
      "environment": [],
      # カスタマイズしたconfをパス指定
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": {
          "config-file-type": "file",
          "config-file-value": "/fluent-bit.conf"
        }
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "log-router",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "latest"
        }
      },
      "memoryReservation": 50
    }
  ],
  "cpu": "2048",
  "taskRoleArn": "taskRoleArnを書く",
  "executionRoleArn": "taskExecutionRoleArnを書く",
  "family": "hoge",
  "memory": "4096",
  "networkMode": "awsvpc",
  "placementConstraints": [],
  "requiresCompatibilities": ["FARGATE"],
  "volumes": []
}

ここで使いたい fluent-bit.conf のパスを指定しています。

ecs-task-definition.json
# カスタマイズしたconfをパス指定
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": {
          "config-file-type": "file",
          "config-file-value": "/fluent-bit.conf"
        }

今回はルートに配置したので"config-file-value"/fluent-bit.conf です。
(ルート以外に配置した場合はそのパスを指定)
ここを指定しない場合はAWS側で /fluent-bit/etc/fluent-bit.conf にあるデフォルトのconfを見にいくようになっています。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/using_firelens.html#firelens-example-firehose

重要
カスタム設定ファイルを使用する場合は、FireLens が使用するパスとは異なるパスを指定する必要があります。Amazon ECS では /fluent-bit/etc/fluent-bit.conf (Fluent Bit) と /fluentd/etc/fluent.conf (Fluentd) のファイルパスは予約されています。

4.カスタマイズしたfluent-bitイメージでlog-routerコンテナを動かしてログを収集する

後はfluent-bitのイメージをbuildし、ECRリポジトリへpushした後、ECS Fargateでコンテナを立ち上げます。
railsコンテナとlog-routerコンテナが正しく起動したのを確認し、

  • CloudWatch
  • S3
    • foo_bucket
    • bar_bucket

を見て、ログが正しく送られているかを確認します。

補足

特にCloudWatchでログのファイル名をよく見てみると、latest/rails-firelens-${ランダムな数字}となっているかと思います。
${ランダムな数字}{task_id}になっているはずです。
つまり、fluent-bit.confのCloudWatchの[OUTPUT]log_stream_prefix latest/と指定したprefixが使われ、{prefix}/{ログのタグ名}というファイル名になっているのです。
仮に Match rails-firelens-*ではなく、Match * にすると、全てのタグのログがCloudWatchに送られることになるので、このファイルの他に latest/foo_loglatest/bar_log というログファイルも作られます。

22
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
14