検証の狙い
具体的には以下を実証します。
- 既定で
ecs_*メタデータがレコードに付与されること(Fargateではec2_instance_idは付かないこと)。 - メモリバッファ既定で高負荷をかけると
paused (mem buf overlimit)が出てログが欠落すること。(実測で OOMKill は発生せずバックプレッシャーによる欠落と判明) - forward input を
storage.type filesystemに切り替えると、検証2と同一負荷でも欠落が解消し到達行数が投入数に一致すること。 - FireLens標準のCloudWatchメトリクスが存在せず、観測はログルーター自身のログと宛先メトリクスに依存すること。
思想・設計判断(例「要件が単純なら awslogs でよい」「FireLensの実体はサイドカー+設定自動生成」)は概念であり観測対象外のため、本検証からは除外しています。
検証マップ(記事の背骨)
詳細版の主張と検証シナリオを1対1で対応させたものです。各行の詳細は後述の「シナリオ別検証」に対応します。
| # | 検証する主張(詳細版由来) | 再現方法 | 証拠(観測すべきもの) |
|---|---|---|---|
| 1 | 既定で ecs_* メタデータ付与(Fargateは ec2_instance_id なし) |
通常時に1行出し、CloudWatchイベントを確認 |
ecs_* キーあり/ec2_instance_id 無し |
| 2 | memory既定でメモリバッファ上限超過→input pause→欠落 | 大量ログを短時間に流す |
paused (mem buf overlimit)/流量>到達 |
| 3 |
storage.type filesystem 化で欠落が解消する |
検証2と同一負荷を filesystem 構成で流す |
overlimit 出ず/到達 ≈ 投入数 |
| 4 | FireLens標準のCloudWatchメトリクスは無い | メトリクスのネームスペースを確認 | FireLens専用NS不在/代替観測のみ |
検証環境
Terraform + Docker 構成を使います。完全な一式は以下のGitHubリポジトリより terraform/ および docker/ を参照してください。以下は記事用の HCL 抜粋です。
GitHub リポジトリリンク:
aws-ecs-firelens-verify
構成要素
- ECSクラスタ(Fargate)
-
タスク定義(2コンテナ、log_router を app より先に定義する)
-
log_router(ECR カスタムイメージ。ベース:public.ecr.aws/aws-observability/aws-for-fluent-bit:3、S3 OUTPUT を追加したextra.confを同梱。firelensConfiguration.type: fluentbit、essential: true) -
app(httpd:2.4、logConfiguration.logDriver: awsfirelens。大量ログを吐けるもの) -
log_router自身にもawslogsを設定し、Fluent Bit のログを CloudWatch に出す(観測の前提)
-
-
ECR リポジトリ(カスタム log_router イメージ置き場。memory 構成は
:latest、filesystem 構成は:fsタグ) - 宛先: CloudWatch Logs ロググループ × 2(app / log-router)+ 検証用 S3 バケット(複数宛先分岐の確認用)
- IAM: タスクロール=宛先書込権限(CloudWatch Logs / S3)/タスク実行ロール=ECR プル・log_router の awslogs 用権限
- セキュリティグループ: ポート 24224 の ingress を開けない
検証モードの切り替え: 検証1・2・4(memory バッファ)と検証3(filesystem バッファ)では log_router の構成が異なります。
terraform/terraform.tfvarsのenable_filesystem_bufferトグル1行で、memory 構成(:latest+extra.confを@INCLUDE)と filesystem 構成(:fs+ フル設定を CMD で読み込み)を切り替えます。コード本体は編集しません。
Terraform 概要
Terraform 概要(HCL 抜粋・クリックで展開)
# --- ECS クラスタ(Fargate) ---
resource "aws_ecs_cluster" "verify" {
name = var.project_name
}
# --- ロググループ(宛先 + log_router 自身のログ) ---
resource "aws_cloudwatch_log_group" "app" {
name = "/aws/ecs/${var.project_name}/app"
retention_in_days = 1 # 検証用に短く(コスト抑制)
}
resource "aws_cloudwatch_log_group" "log_router" {
name = "/aws/ecs/${var.project_name}/log-router"
retention_in_days = 1
}
# --- 検証用 S3(複数宛先分岐の確認) ---
resource "aws_s3_bucket" "verify_logs" {
bucket = "${var.project_name}-logs-${var.s3_bucket_suffix}"
force_destroy = true # teardown で中身ごと消す
}
# --- ECR リポジトリ(カスタム log_router イメージ置き場) ---
resource "aws_ecr_repository" "log_router" {
name = "${var.project_name}-log-router"
force_delete = true
}
# --- タスク定義(log_router + app の2コンテナ) ---
# log_router を app より先に定義することで FireLens サイドカーを先行起動させる。
resource "aws_ecs_task_definition" "verify" {
family = var.project_name
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.task_cpu
memory = var.task_memory
task_role_arn = aws_iam_role.task.arn
execution_role_arn = aws_iam_role.execution.arn
container_definitions = jsonencode([
{
name = "log_router"
# S3 OUTPUT を追加した extra.conf を同梱するためカスタムイメージを使用。
# ベース: public.ecr.aws/aws-observability/aws-for-fluent-bit:3
image = "${aws_ecr_repository.log_router.repository_url}:latest"
memory = 200 # log_router コンテナのハードリミット (MiB)
essential = true
firelensConfiguration = {
type = "fluentbit"
options = {
# Fargate は config-file-type = "s3" 非サポートのため file 方式でイメージに同梱する。
"config-file-type" = "file"
"config-file-value" = "/fluent-bit/configs/extra.conf"
}
}
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.log_router.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "fb"
}
}
environment = [
{ name = "AWS_REGION", value = var.aws_region },
{ name = "S3_BUCKET_NAME", value = aws_s3_bucket.verify_logs.id },
]
},
{
name = "app"
image = "httpd:2.4"
essential = true
logConfiguration = {
logDriver = "awsfirelens"
options = {
Name = "cloudwatch_logs"
region = var.aws_region
log_group_name = aws_cloudwatch_log_group.app.name
auto_create_group = "false"
log_stream_prefix = "app-"
}
}
}
])
}
# --- IAM(タスクロール:宛先書込 / 実行ロール:プル・awslogs) ---
# task role: logs:PutLogEvents, logs:CreateLogStream on app log group / s3:PutObject on verify bucket
# execution role: AmazonECSTaskExecutionRolePolicy + logs:PutLogEvents on log_router log group
# --- セキュリティグループ:24224 の ingress は定義しない(開けない) ---
resource "aws_security_group" "task" {
name = "${var.project_name}-task"
description = "No port 24224 ingress by design."
vpc_id = var.vpc_id
# ingress for 24224 は意図的に作らない(詳細版のセキュリティ注意を踏襲)
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
カスタムイメージのビルド・push 手順は docker/Dockerfile および docker/extra.conf を参照してください。
コスト注意
- Fargate タスク(vCPU/メモリ)・CloudWatch Logs 取り込み・S3 PUT/保管に課金されます。検証は短時間で完了させ、放置しないこと。
- ロググループの
retention_in_daysを短く(例: 1日)して保管コストを抑えています。 - 高負荷再現で大量ログを流すため、CloudWatch Logs 取り込み量が増えます。流す量は再現に必要な最小限にしてください。
Teardown 手順(検証後に確実に消す)
# 1. 実行中タスクがないことを確認
aws ecs list-tasks --cluster <cluster_name> --region <region>
# 2. Terraform で一式破棄(S3 は force_destroy で中身ごと削除)
aws-vault exec <profile> -- terraform destroy
# 3. 残存していないか確認(ロググループ・S3 バケット・タスク定義)
aws logs describe-log-groups --log-group-name-prefix /aws/ecs/<project_name>
aws s3 ls | grep <project_name>
セキュリティ注意(詳細版を踏襲)
- ポート 24224 の ingress は開けない。 forward input はタスク内のサイドカー間通信であり、外部公開は不要かつ危険です。
- 検証用に作った S3 バケットはパブリックアクセスをブロックしたまま使うこと。
シナリオ別検証
各検証は 仮説 → 手順 → 証拠 → 実測結果 の順で統一しています。
検証1 | 既定で ecs_* メタデータが付与される
Fargate では
ec2_instance_idが付かないことも併せて確認する。
仮説(詳細版) — 既定で各レコードに ecs_cluster / ecs_task_arn / ecs_task_definition が付与される。ec2_instance_id は EC2 起動タイプのみで、Fargate では付与されない。enable-ecs-log-metadata: false で無効化できる。
再現手順
-
上記 Fargate 構成でタスクを起動する。
-
appコンテナの stdout に 1 行ログを出す。aws ecs run-task \ --cluster <cluster_name> \ --task-definition <task_definition_arn> \ --launch-type FARGATE \ --region <region> \ --network-configuration "awsvpcConfiguration={subnets=[<SUBNET_ID>],securityGroups=[<SG_ID>],assignPublicIp=ENABLED}" \ --overrides '{"containerOverrides":[{"name":"app","command":["sh","-c","echo hello-firelens-verify && sleep 30"]}]}' -
宛先の CloudWatch Logs で該当ログイベントの JSON を確認する。
aws logs tail <app_log_group_name> --follow --region <region>
観測すべき証拠
- イベント JSON に
ecs_cluster/ecs_task_arn/ecs_task_definitionが含まれる -
ec2_instance_idが含まれない(Fargate のため)
実測結果 —
期待値通りのログ出力結果であることを確認。
検証2 | mem buf overlimit でログが欠落する
仮説 — input は既定 storage.type memory。メモリバッファが上限に達すると input が pause され新規レコードは失われる。Fluent Bit ログに [input] forward.1 paused (mem buf overlimit) が出る。
欠落点は直列2段: ログは app の stdout →Docker ログドライバ(awsfirelens)のバッファ→**Fluent Bit forward input(既定メモリバッファ)**→ CloudWatch と流れます。
① FB のメモリバッファ上限で input が pause され、
② pause 中に Docker ドライバのバッファ(log-driver-buffer-limit、既定 1,048,576 行)が溢れると古いメッセージが破棄されます。いずれもノンブロッキングな破棄で OOMKill は起こしません。再現には
terraform.tfvarsのapp_log_driver_buffer_limit(実測では8192)を設定し、FB のメモリバッファ容量より大きく・かつ投入量が両バッファ合計を十分超えるようにします。
再現手順
-
terraform.tfvarsでenable_filesystem_buffer = false(既定)・app_log_driver_buffer_limit = 8192を設定してapply済みとする。 -
memory バッファ構成のまま、
appから大量ログを短時間に流し続ける。N=100000 aws-vault exec <profile> -- aws ecs run-task \ --cluster "$CLUSTER" \ --task-definition "$TASKDEF" \ --launch-type FARGATE \ --region "$REGION" \ --network-configuration "awsvpcConfiguration={subnets=[$SUBNET],securityGroups=[$SG],assignPublicIp=ENABLED}" \ --overrides '{"containerOverrides":[{"name":"app","command":["sh","-c","P=$(printf %10000s | tr \" \" X); for i in $(seq 1 '"$N"'); do echo \"$P\"; done"]}]}' \ --query "tasks[0].taskArn" --output text -
log_routerのログ(CloudWatch)でpaused (mem buf overlimit)を観察する。aws logs tail <log_router_log_group_name> --follow --region <region> | grep -i "paused\|overlimit" -
タスク完了後、CloudWatch に到達した行数を数え、流した行数との差分(欠落)を確認する。
観測すべき証拠
-
log_routerログに[input] forward.1 paused (mem buf overlimit)が出る - 流した行数 > 宛先到達行数(欠落発生)
実測結果 —
流した100,000件に対して、到達件数が90,550であることから、仮説通り欠損があることを確認。
検証3 | storage.type filesystem 化でログ欠落が解消する
仮説 — forward input を storage.type filesystem に切り替えると、検証2と同じ負荷でも mem buf overlimit による pause/ドロップが起きず、到達行数が投入数 N に近づく。
前提: filesystem バッファは input 定義に
storage.type filesystemを書く必要がありますが、FireLens が自動生成する forward input には後付けできません(公式: カスタム設定で forward input を定義してはいけない)。そのため検証3だけはextra.confを@INCLUDEする方式ではなく、CMD を上書きして自前のフル設定を読ませ、その中で forward input を filesystem 化して再定義します(:fsイメージ)。
再現手順
-
:fsイメージ(fluent-bit-fs.confを CMD で読む構成)をビルドして ECR に push する。 -
terraform.tfvarsのenable_filesystem_buffer = trueにしてapplyし、TASKDEFを再度取り直す。 -
検証2と同一の
N・同一の負荷を流す(before = 検証2 / after = 検証3)。N=100000 # 検証2と必ず一致させる aws-vault exec <profile> -- aws ecs run-task \ --cluster "$CLUSTER" \ --task-definition "$TASKDEF" \ --launch-type FARGATE \ --region "$REGION" \ --network-configuration "awsvpcConfiguration={subnets=[$SUBNET],securityGroups=[$SG],assignPublicIp=ENABLED}" \ --overrides '{"containerOverrides":[{"name":"app","command":["sh","-c","P=$(printf %10000s | tr \" \" X); for i in $(seq 1 '"$N"'); do echo \"$P\"; done"]}]}' \ --query "tasks[0].taskArn" --output text -
log_routerにoverlimitが出ないことを確認し、タスク完了(flush 完了)後にIncomingLogEventsの Sum で到達総数を数え、検証2と比較する。
観測すべき証拠
-
log_routerログにmem buf overlimit/pausedが出ない - 到達行数(
IncomingLogEventsの Sum)≈N(検証2より明確に増える)
実測結果 —
同一負荷 N=100,000 行に対し、検証2(memory)は到達 90,550 行・約 9,450 行(≒9.5%)欠落だったのに対し、検証3(filesystem)では overlimit が一切発生せず到達行数が N と完全一致(100,000 行)した。
| 確認項目 | 検証2(memory・before) | 検証3(filesystem・after) |
|---|---|---|
mem buf overlimit |
発生(pause/resume) | 発生無し |
| 到達行数 | 90,550 | 100,000 |
検証4 | FireLens 標準の CloudWatch メトリクスは無い
仮説(詳細版) — FireLens / Fluent Bit 自体の CloudWatch メトリクスは AWS 標準では提供されない。観測はログルーター自身のログと宛先サービス側メトリクスで行う。
再現手順
-
CloudWatch メトリクスのネームスペース一覧を確認し、FireLens 専用のものが無いことを確認する。
aws cloudwatch list-metrics --namespace "FireLens" --region <region> aws cloudwatch list-metrics --namespace "ECS/FireLens" --region <region> -
観測手段として、
log_router自身のログ(awslogs)と宛先メトリクス(CloudWatch Logs のIncomingLogEvents等)が使えることを確認する。aws cloudwatch list-metrics \ --namespace "AWS/Logs" \ --metric-name "IncomingLogEvents" \ --dimensions "Name=LogGroupName,Value=<app_log_group_name>" \ --region <region>
観測すべき証拠
-
FireLens/ECS/FireLensネームスペースが空(メトリクスなし) - 観測が log_router 自身のログであることの確認
実測結果 —
上記は FireLens / ECS/FireLens 両ネームスペースともに空でした。
| 確認項目 | 実測 |
|---|---|
FireLens ネームスペース |
{"Metrics": []}(メトリクスなし) |
ECS/FireLens ネームスペース |
{"Metrics": []}(メトリクスなし) |
AWS/Logs IncomingLogEvents
|
取得可能(宛先側メトリクスとして利用) |
まとめ
| 検証 | 主張 | 裏付け |
|---|---|---|
| 1 |
ecs_* メタデータ付与 / Fargate で ec2_instance_id なし |
✅ 実測で確認 |
| 2 |
mem buf overlimit でログ欠落(OOMKill は発生せずバックプレッシャーで抑制) |
✅ 実測で確認(仮説修正済み) |
| 3 |
storage.type filesystem 化で欠落解消 |
✅ 実測で確認 |
| 4 | FireLens 標準メトリクス無し(観測は log_router 自身のログ) | ✅ 実測で確認 |








