やりたいこと
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はある特定の条件のログのみを抽出したいという意)
構成図
作業手順
- リソース作成
- CloudWatchと2つのFirehoseに送るようにfluent-bitイメージをカスタマイズする
- log-routerコンテナを定義
- カスタマイズしたfluent-bitイメージでlog-routerコンテナを動かしてログを収集する
1. リソース作成
- firehose(今回は各firehoseごとにIAMロールを作成する想定)
- foo_firehose
- foo_firehose_role
- bar_firehose
- bar_firehose_role
- foo_firehose
- 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
{
"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する
{
"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する
{
"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つです。
[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
# ログがエンコードされて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になります。
# ログがエンコードされて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に戻ります。
# 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_id
やcontainer_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
のようなタグが最初に全てのログにつけられています。
これをうまく使ってログの分割を行なっていきます。
ログのタグ付け
[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にfoo
かbar
という文字が入っているという想定です。$name ^(foo)$ foo_log true
これはnameというKeyの中身がfoo
ならfoo_log
というタグをつける、という意味になります。
(^(foo)$
は正規表現です)true
はタグをつけた後、タグをつける前のログをとっておくかどうか、を表します。
具体的にいうと、もともとはrails-firelens-*
というタグがついていたけれど、foo_log
というタグを上書きしたので、元のrails-firelens-*
のタグがついている方は取っておくか?という意味です。
今回はこの後の用途にて必要になるのでtrue
にして取っておくようにしています。
false
にすれば元のログは削除されfoo_log
とついたもののみ、残ります。
ここまでで、ログをいい感じに整形して、タグをつけるところまできました。
あとはタグを使って出力先を指定するだけです。
ログの出力
# 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
[OUTPUT]
Name cloudwatch
Match rails-firelens-*
今回はCloudWatchに「applicationが吐き出した全てのログ」を送りたいと考えています。
タグがfoo_log、bar_log、そうでないもの全てです。
ここで、さっきrails-firelens-*
というタグのログを残しておいたのが生きてきます。
foo_logやbar_logというタグとは別にrails-firelens-*
というタグがついた状態で全てのログが残っているので、このタグのログを送ってあげれば全てのログが送られることになります。ですのでMatch rails-firelens-*
と書きます。
- Firehose
# 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.conf
、fluent-bit-parsers.conf
をコンテナのルートに配置するように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はこのように書きます。
{
"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
のパスを指定しています。
# カスタマイズした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_log
とlatest/bar_log
というログファイルも作られます。