さてさて前回 AWS CDK で Infrastructure as Code する: SpringBoot編2 までで
- SpringBootのDockerイメージを作成して
- ECRとかAWS環境を整備して
- CDKを使ってECSにデプロイする
などをやりましたが、今回は Fluent Bit についてです。
Fluent Bit ってなに??って話ですが、いわゆるログを収集・処理・転送するためのツールです。AWS では 「ECSで動くアプリが出力するログ(標準出力とかに出してるログ)」をCloudWatch LogsやS3やKinesisなどに転送(保存)するために使われてます。
CloudWatch Logsに送るだけなら特別な設定をしなくてもでデフォルトでそうなったりするのですが「エラーログだけはCloudWatch Logsにおくる1」とか「その他のログは課金の安いS3へ送る」とかカスタマイズしようとするとFluent Bitなどが必要になってきます。
記事の前提条件・前提知識
- AWS CDK で Infrastructure as Code する: SpringBoot編2 をざっとでも読んでくれていること
- Fluent Bit ってなんだっけ?って方
- Fluent Bit をとにかく動かしてみたい方
Fluent Bit 使い方概要
(画像は公式サイト より引用させてもらいました)
定石の使い方では、アプリのECSの横に「サイドカー」としてFluent Bitのコンテナを配置し、そのサイドカーにログの処理をお願いする、みたいな方式になります。
もうすこし具体的に書くと、まず前回の記事のようなHelloworld 程度の ECSのタスク定義では、下記のようにFluent Bitなどがでてこない記述をしていました。
logConfiguration: {
logDriver: 'awslogs',
options: {
'awslogs-create-group': 'true',
'awslogs-group': `/ecs/app-taskdefinition`,
'awslogs-region': `${region}`,
'awslogs-stream-prefix': 'ecs',
},
},
このように logDriverを 「awslogs
」 と指定していますが、こうするとECSアプリのコンソールログは直接 Cloudwatch Logsの上記のロググループに送信されます。
Fluent Bitを使うばあいはこのへんの記述を「ログをサイドカーに送るよ」という記述、
"logConfiguration": {
"logDriver": "awsfirelens",
"options": {}
},
logDriverを awsfirelens
にし、さらに下記のようにサイドカー用のコンテナを合わせて設定しておきます。
"containerDefinitions": [
{
"name": "log_router",
"image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:latest",
...
},
"firelensConfiguration": {
"type": "fluentbit",
"options": {
"config-file-type": "file",
"config-file-value": "/fluent-bit/etc/fluent-bit-custom.conf"
}
}
},
で、実際のログの送信先とかは上記で指定した設定ファイル fluent-bit-custom.conf
でおこなう、そんな仕組みです。
やってみる
AWS ECS上でいろいろ動かそうとするとそれなりに環境設定が必要なので、今回はローカルのDocker上で挙動を確認してみます。
この場合もちろん、Fluent Bit から他のサービス( CloudWatch Logs、S3、Kinesisなど) への稼働確認はできないのですが、アプリが出力したログがFluent Bitにどのように連携されて、どう加工されたかを見るには十分な環境だと思います。
環境の確認とソースの取得
JavaなどPC上のインフラ環境はこんな感じ。手元の環境はLinux(Ubuntu) でしたが、WSLでも問題なく動くはずです2。
$ java --version
openjdk 21.0.7 2025-04-15 LTS
OpenJDK Runtime Environment Temurin-21.0.7+6 (build 21.0.7+6-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.7+6 (build 21.0.7+6-LTS, mixed mode, sharing)
$ mvn --version
Apache Maven 3.9.2 (c9616018c7a021c1c39be70fb2843d6f5f9b8a1c)
Maven home: /opt/JavaTools/apache-maven-3.9.2
Java version: 21.0.7, vendor: Eclipse Adoptium, runtime: /home/sysmgr/.sdkman/candidates/java/21.0.7-tem
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-216-generic", arch: "amd64", family: "unix"
$ docker --version
Docker version 24.0.2, build cb74dfc
$
DockerでSpringBootを起動する
アプリはいままでの記事でも使っているspring-boot-sample-tomcat を使います。このアプリの横(?)にFluent Bitのコンテナを作成し、アプリのログをFluent Bitに流し込むようにします。
特に理由はないですが、今回は docker compose をつかってSpringBootを起動することにしました。
$ git clone --branch fluentbit https://github.com/masatomix/spring-boot-sample-tomcat.git
$ cd spring-boot-sample-tomcat/
spring-boot-sample-tomcat $ mvn package
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.745 s
[INFO] Finished at: 2025-08-24T13:47:53Z
[INFO] ------------------------------------------------------------------------
ここまででまず、Dockerのイメージに入れるjarファイルができました。今後ソースを修正した場合はjarの再作成を忘れないようにしてください。
つづいて docker compose の設定を見ておきましょう。まず docker-compose.yaml は以下の通り。
- ./docker-compose.yaml
version: "3.7"
services:
spring-boot-sample-tomcat:
build:
context: .
dockerfile: Dockerfile
platforms:
- "linux/amd64"
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: production
環境設定で、プロファイルを「production」に指定しています。
SpringBoot側にはあらかじめ、プロファイルがproduction
の場合はログ出力がJSON形式になるような設定が入れてあります。
参考: ログのフォーマットを環境によって変更したい
つづいてDockerfileです。
- ./Dockerfile
FROM openjdk:17-alpine
VOLUME /tmp
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]
特に前回と変わったところはなく、さきほどの mvn package
で生成されたjarを指定しているだけです。
SpringBootを起動します。
spring-boot-sample-tomcat$ docker compose up
[+] Building 0.0s (0/0)
[+] Running 1/0
✔ Container spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 Created
Attaching to spring-boot-sample-tomcat-spring-boot-sample-tomcat-1
...
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | . ____ _ __ _ _
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | \\/ ___)| |_)| | | | | || (_| | ) ) ) )
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | ' |____| .__|_| |_|_| |_\__, | / / / /
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | =========|_|==============|___/=/_/_/_/
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | :: Spring Boot :: (v3.1.2)
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 |
...
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | {"@timestamp":"2025-08-24T13:54:59.814799039Z","@version":"1","message":"Tomcat started on port(s): 8080 (http) with context path ''","logger_name":"org.springframework.boot.web.embedded.tomcat.TomcatWebServer","thread_name":"main","level":"INFO","level_value":20000}
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | {"@timestamp":"2025-08-24T13:54:59.82463053Z","@version":"1","message":"Started SampleTomcatApplication in 3.178 seconds (process running for 3.648)","logger_name":"nu.mine.kino.SampleTomcatApplication","thread_name":"main","level":"INFO","level_value":20000}
起動できたので、アクセスしてみましょう。
$ curl 'http://localhost:8080/echoLogger' -i
HTTP/1.1 200
vary: accept-encoding
Content-Type: text/plain;charset=UTF-8
Content-Length: 16
Date: Sun, 24 Aug 2025 14:26:26 GMT
Default Message.
$
起動できましたね。サーバ上(Docker composeしたコンソール)にも下記のようなJSON形式のログが出力されたと思います。
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | {"@timestamp":"2025-08-24T15:12:23.322356857Z","@version":"1","message":"hostname: 6468584bff0a","logger_name":"nu.mine.kino.web.EchoController","thread_name":"http-nio-8080-exec-6","level":"INFO","level_value":20000}
spring-boot-sample-tomcat-spring-boot-sample-tomcat-1 | {"@timestamp":"2025-08-24T15:12:23.322824362Z","@version":"1","message":"Default Message.","logger_name":"nu.mine.kino.web.EchoController","thread_name":"http-nio-8080-exec-6","level":"INFO","level_value":20000}
Fluent Bit の設定を修正する
さて、取得したgit のブランチには Fluent BitのDockerの設定も入れてあるので、Fluent BitもDockerから起動してみます。
別のコンソールを使って以下を実行していきます。
spring-boot-sample-tomcat$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6468584bff0a spring-boot-sample-tomcat-spring-boot-sample-tomcat "java -jar /app.jar" 7 hours ago Up 53 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp spring-boot-sample-tomcat-spring-boot-sample-tomcat-1
SpringBootのログファイルを特定するために CONTAINER IDが必要になるので、psで調べています。具体的には、Dockerのコンテナごとのログファイルは/var/lib/docker/containers/[CONTAINER ID]*/*-json.log
に記録されますので、その値つまり6468584bff0a
の値をメモしておきましょう。
次にFluent Bit の設定ファイルに、先ほどのIDを記述します。具体的には下記の箇所です。
sysmgr@vscode-server:/tmp/spring-boot-sample-tomcat$ cd fluentbit/test/
sysmgr@vscode-server:/tmp/spring-boot-sample-tomcat/fluentbit/test$ cat fluent-bit-test.conf
[SERVICE]
Flush 1
Log_Level info
Parsers_File /fluent-bit/etc/parsers.conf
... 省略
[INPUT]
Name tail
Tag -firelens-
Path /var/lib/docker/containers/6468584bff0a*/*-json.log <- ここを先のIDに変更しておく
parser docker
... 省略
$
今回はこのように、Docker上のコンテナが出力するコンソールログファイルを[INPUT]に設定して、Fluent Bitにアプリのログが流れ込んでくるようにしました3。
また一応ですが、この[INPUT]を使ってアプリのログをFluent Bitに渡すのは今回がローカル環境だからで、AWS ECS上では AWS側(の FireLens機能 )にお任せすることになります。この辺は続きでまた記事にしようと思います。
Fluent Bitの設定を少し見てみる
このタイミングですこしFluent Bitの設定を見てみます。
- INPUTのセクション
[INPUT]
Name tail
Tag -firelens-
Path /var/lib/docker/containers/6468584bff0a*/*-json.log
parser docker
先ほど見たとおり、Fluent Bitに転送されてくる(Fluent Bitが読み込む) アプリのログファイルを指定しています。また、データ種類を識別するために Tagをつけるのですが、Tag名を-firelens-
としています4。
- FILTERのセクション
[FILTER]
Name parser
Match *-firelens-*
Key_Name log
Parser json
# Preserve_Key ON
Reserve_Data ON
今回説明は割愛しますが、-firelens-
というTagがついたJSONデータのlogというプロパティの値を取り出す、みたいなことをやっています。
参考: https://docs.fluentbit.io/manual/pipeline/filters/parser
[FILTER]
Name rewrite_tag
Match *-firelens-*
Rule $level (ERROR) error-$container_id true
-firelens-
というTagがついたJSONデータについて、 level
プロパティがERROR
という文字の場合にTag名を「error-$container_id
」に変更しています。ERRORログについてはTag名を変更して識別できるようにしているってことですね。
末尾の true
は、既存データを元Tagにも残し、新Tag名のデータはコピーで作成する、みたいな意味です。エラーログの取扱を完全に分けたい場合は false
にしておきましょう。
- OUTPUT のセクション
[OUTPUT]
Name file
Match error*
Path /fluent-bit/etc/output_dir
tag名がerrorで始まるデータについて(さきほどERRORログですね)、/fluent-bit/etc/output_dir/tag名
ファイルに出力します。
[OUTPUT]
Name stdout
Match *
全てのtagのデータについて、Fluent Bitのコンソールにデータを出力します。
などなどがFluent Bitがやってくれる機能です。流れてくるデータに対して、通常はコンソールに出力しつつ、エラーログだけ別の宛先に転送している、などのイメージがつかめればよいとおもいます。
Docker で Fluent Bitを起動、正常系ログを確認
docker-compose.yaml も、少しだけみてみます。
spring-boot-sample-tomcat/fluentbit/test$ cat docker-compose.yaml
version: "3.7"
services:
fluent-bit:
image: amazon/aws-for-fluent-bit:latest
volumes:
- ./fluent-bit-test.conf:/fluent-bit/etc/fluent-bit.conf
- ./input.txt:/fluent-bit/etc/input.txt
- ./output_dir:/fluent-bit/etc/output_dir
- /var/lib/docker/containers:/var/lib/docker/containers:ro
AWSが用意してくれているFluent Bitのイメージを利用しつつ、./output_dir:/fluent-bit/etc/output_dir
の記述で、./output_dir
のディレクトリがコンテナ内の/fluent-bit/etc/output_dir
のディレクトリにマウントされるようにしてあります。
さて、Fluent Bit側のコンテナも起動します。
spring-boot-sample-tomcat/fluentbit/test$ docker compose up
[+] Building 0.0s (0/0)
[+] Running 1/0
✔ Container test-fluent-bit-1 Created 0.0s
Attaching to test-fluent-bit-1
test-fluent-bit-1 | Fluent Bit v1.9.10
test-fluent-bit-1 | * Copyright (C) 2015-2022 The Fluent Bit Authors
test-fluent-bit-1 | * Fluent Bit is a CNCF sub-project under the umbrella of Fluentd
test-fluent-bit-1 | * https://fluentbit.io
test-fluent-bit-1 |
test-fluent-bit-1 | [2025/08/24 15:10:41] [ info] [fluent bit] version=1.9.10, commit=a2eaf59628, pid=1
test-fluent-bit-1 | [2025/08/24 15:10:41] [ info] [storage] version=1.4.0, type=memory-only, sync=normal, checksum=disabled, max_chunks_up=128
test-fluent-bit-1 | [2025/08/24 15:10:41] [ info] [cmetrics] version=0.3.7
test-fluent-bit-1 | [2025/08/24 15:10:41] [ info] [sp] stream processor started
test-fluent-bit-1 | [2025/08/24 15:10:41] [ info] [input:tail:tail.0] inotify_fs_add(): inode=1100314 watch_fd=1 name=/var/lib/docker/containers/7c9d4cfb1ace6faeda294fb7c38ee95ea8b737dd7bd0e945fe00be721b1e7b81/7c9d4cfb1ace6faeda294fb7c38ee95ea8b737dd7bd0e945fe00be721b1e7b81-json.log
test-fluent-bit-1 | [2025/08/24 15:10:41] [ info] [output:stdout:stdout.1] worker #0 started
test-fluent-bit-1 | [2025/08/24 15:10:41] [ info] [output:file:file.0] worker #0 started
サイドカーとなるコンテナが起動したので、再度疎通をしてみましょう。
$ curl 'http://localhost:8080/echoLogger' -i
HTTP/1.1 200
vary: accept-encoding
Content-Type: text/plain;charset=UTF-8
Content-Length: 16
Date: Sun, 24 Aug 2025 15:57:39 GMT
Default Message.
$
SpringBootのコンソールとは別に、Fluent Bit側のコンテナのコンソールにも
test-fluent-bit-1 | [0] -firelens-: [1756048334.786843777, {"@timestamp"=>"2025-08-24T15:12:14.786605637Z", "@version"=>"1", "message"=>"hostname: 7c9d4cfb1ace", "logger_name"=>"nu.mine.kino.web.EchoController", "thread_name"=>"http-nio-8080-exec-4", "level"=>"INFO", "level_value"=>20000, "stream"=>"stdout", "time"=>"2025-08-24T15:12:14.786843777Z"}]
test-fluent-bit-1 | [1] -firelens-: [1756048334.788296016, {"@timestamp"=>"2025-08-24T15:12:14.787033992Z", "@version"=>"1", "message"=>"Default Message.", "logger_name"=>"nu.mine.kino.web.EchoController", "thread_name"=>"http-nio-8080-exec-4", "level"=>"INFO", "level_value"=>20000, "stream"=>"stdout", "time"=>"2025-08-24T15:12:14.788296016Z"}]
などが出力されたと思います。よさそうですね。
エラーログは別のファイルに出力される
続いてエラーが発生するリクエストを投げてみます。細かい説明は割愛しますが、下記で呼ばれるAPIはLog LevelがERRORのログが出力されるようにコーディングしてあります。
$ curl 'http://localhost:8080/error-sim/throw-exception' -i
HTTP/1.1 500
vary: accept-encoding
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Aug 2025 15:21:40 GMT
Connection: close
{"timestamp":"2025-08-24T15:21:40.092+00:00","status":500,"error":"Internal Server Error","path":"/error-sim/throw-exception"}
$
エラーが返ってきました。
先ほどFluent Bitの設定で見たとおり、エラーは別のログファイル ( コンテナ内だと/fluent-bit/etc/output_dir/
、外の世界では./output_dir) に出力する設定だったので、それを見てみます。
spring-boot-sample-tomcat/fluentbit/test$ tail ./output_dir/error-
error-: [1756048900.091458643, {"@timestamp":"2025-08-24T15:21:40.090477238Z","@version":"1","message":"Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: わざと例外をスローしました] with root cause","logger_name":"org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet]","thread_name":"http-nio-8080-exec-1","level":"ERROR","level_value":40000,"stack_trace":"java.lang.RuntimeException: わざと例外をスローしました\n\tat nu.mine.kino.web.ErrorController.throwException(ErrorController.java:42)\n\tat ...java.base/java.lang.Thread.run(Thread.java:831)\n","stream":"stdout","time":"2025-08-24T15:21:40.091458643Z"}]
spring-boot-sample-tomcat$
エラーログだけ、別のファイルにちゃんと転送されていることが確認できました。
おつかれさまでした。
まとめ
Fluent Bitなどのログ転送とかって、なんとなくつかみどころがなくてとっつきにくい機構ではありますが、手元で動かすことである程度イメージをつかむことはできたでしょうか。
さて、いったんまとめます。
- ローカルで動いているDocker上でFluent Bitの挙動を確認しました。
- Dockerのコンテナ単位のログファイルの情報を指定してあげることで、ECS上でなくローカルのDocker環境でもFluent Bitの稼働確認を行うことができました。
- ログデータのtagをうまく使い分けることで、転送先を複数指定できることを確認しました。
- AWS ECSの標準機能では、こういう振り分けはできなかったですね
- 今回は割愛しましたが、[FILTER]セクションでparserを用いる事で、ネストされたログデータを抽出したり、などができたりしますが、この辺はまたの機会に説明したいと思います!
お疲れさまでしたー
関連リンク
- Fluent Bit 公式
- Amazon ECS の Fluent Bit イメージリポジトリの AWS
- Amazon ECS タスク定義の例: FireLens にログをルーティングする
- AWS CDK で Infrastructure as Code する: SpringBoot編2
- spring-boot-sample-tomcat 今回のソース
- 詳解 FireLens – Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る
-
さらにはそのエラーログ用のロググループの出力カウント数をCloudWatchメトリクスに設定して(CloudWatch Logsのメトリクスフィルタ機能を使う)、そのメトリクスがカウントアップしたら通知する(CloudWatch Alarms機能)、とかやったりします。 ↩
-
MacのDockerだけは、ログファイルの場所が仕様が違うようで、正しく動かすことができませんでした、、。 ↩
-
この[INPUT]で入力データを指定する方式を使うことで、任意のデータをFluent Bitに渡して挙動を確認することができたりします。具体的には[INPUT] で所定のファイル名を指定して、そのファイルにFluent Bitに渡したいログデータを書き込んで保存、とかやる感じです。 ↩
-
AWS上でFirelens経由でFluent Bitに流れてくるデータのtag名がコレなので、それにあわせた ↩