tidb-proxy: ログを FireLens で振り分けて S3/Iceberg + Athena で検索可能にする
起票日: 2026-07-10
移行元: 全コンテナログを awslogs driver で CloudWatch Logs
/ecs/tidb-proxyに集約移行先: FireLens (Fluent Bit) で INFO 系を Firehose → Iceberg (S3 + Glue) へ、WARN/ERROR・非JSON行を CloudWatch Logs へ振り分け
対象環境: dev / prd 共用(tidb-proxy 自体が共用 1 task のため、ログ基盤も 1 セット)
ステータス: 実装済み
関連実装:
apps/tidb-proxy/,iac/aws/lib/analytics/,iac/aws/ecspresso/tidb-proxy/
概要
tidb-proxy (squid + Go forwarder 同居の Fargate task) のログは現状 awslogs driver で CloudWatch Logs に全量流れるだけで、squid のアクセス記録を後から分析する手段が実質 CloudWatch Logs Insights しかない。本タスクで以下に切り替える。
アプリのログを JSON 構造化する(forwarder は
log/slog、squid はカスタムlogformat)タスクに FireLens (Fluent Bit) サイドカーを追加し、
levelで宛先を振り分けるINFO/ squid アクセスログ → Amazon Data Firehose → Iceberg テーブル (S3 + Glue Data Catalog)WARN/ERROR/ JSON パース不能行 (tsnet 内部ログ・squid cache_log) → CloudWatch Logs (/ecs/tidb-proxy、従来どおり障害調査用)
Iceberg テーブルは Athena (WorkGroup
tidb-proxy-logs) で検索する
設計判断の前提
カスタム Fluent Bit イメージは作らない。
aws-for-fluent-bitのinitタグイメージは Fargate でも起動時に S3 から設定ファイルを取得できるため、設定は S3 配置 (BucketDeploymentで git と同期) で済ませ、ECR リポジトリ追加とイメージビルドのパイプラインを持たないS3 Tables は使わない。マネージドコンパクションは魅力だが Lake Formation 統合の設定が増える。このデータ量 (月数十〜数百MB) ではコンパクション自体が当面不要なため、通常 S3 + Glue Catalog の Iceberg テーブルで開始する
Kinesis Data Streams は使わない。Firehose Direct PUT で十分(シャード時間課金なし、完全従量)
アーキテクチャ
Before (現行)
Task: tidb-proxy
├─ tidb-proxy コンテナ (squid + forwarder)
│ └─ stdout/stderr ── awslogs driver ──→ CloudWatch Logs /ecs/tidb-proxy (全量)
└─ otel-collector (ADOT)
└─ stdout/stderr ── awslogs driver ──→ CloudWatch Logs /ecs/tidb-proxy
After (本タスク)
Task: tidb-proxy
├─ tidb-proxy コンテナ (squid + forwarder, JSON ログ化)
│ └─ stdout/stderr ── awsfirelens driver ──┐
├─ log-router (aws-for-fluent-bit:init-*) │
│ ├─ 起動時に S3 から extra.conf を取得 ←──┼── s3://tidb-proxy-logs-<account>/firelens-config/
│ ├─ level=WARN/ERROR・非JSON行 ──────────→ CloudWatch Logs /ecs/tidb-proxy
│ └─ level=INFO・squid_access ────────────→ Firehose tidb-proxy-logs (Direct PUT)
│ └→ Iceberg: tidb_proxy_logs.logs
│ (s3://tidb-proxy-logs-<account>/iceberg/logs/)
│ ← Athena WorkGroup tidb-proxy-logs で検索
└─ otel-collector (ADOT, awslogs のまま無変更)
squid アクセスログ (stdout) は logformat で
log_type=squid_access/level=INFOを埋め込むforwarder は slog JSON (
log_type=forwarder) で、通常ログは INFO、エラー系は WARN/ERRORtsnet 内部ログ・squid cache_log (stderr) は非 JSON のまま → Fluent Bit の JSON パースに失敗した行として CloudWatch Logs にフォールバック(安全側)
コスト試算
東京リージョン、ログ量は月 1GB と多めに見積もった場合(実際は月数十〜数百MB 想定)。
要素 |
単価 |
月額 (USD) |
|---|---|---|
Firehose Direct PUT → Iceberg 配信 |
~$0.075/GB(5KB 切り上げなし) |
~$0.08 |
S3 (Parquet 保存 + PUT/GET リクエスト) |
$0.025/GB-月 + リクエスト |
~$0.05 |
Glue Data Catalog |
100万オブジェクト/リクエスト無料 |
$0 |
Athena |
$5/TB スキャン(最低 10MB/query) |
月数円 |
CloudWatch Logs 取り込み |
INFO 分 ($0.76/GB) が消える |
削減方向 |
VPC Endpoint |
task に public 疎通があり不要 |
$0 |
合計 |
月十数円 |
条件付きの増分として、タスクメモリを 512MB → 1024MB に上げる場合のみ +約$1.6/月 (ARM $0.0044/GB-h × 0.5GB × 730h)。
メモリの判断基準
Fluent Bit 自体の RSS は 30〜60MB 程度(AWS の目安予約は 100〜250MB)。512MB に squid + forwarder (tsnet ~50-100MB) + ADOT (~70-150MB) が既に同居しているため、無条件に増やさず実測で決める。
デプロイ前に
AWS/ECS MemoryUtilization(ClusterName=tidb-proxy, ServiceName=tidb-proxy) の直近 7 日の Maximum を確認60% 未満 (≈300MB) →
512据え置き。Firehose 出力にRetry_Limit 2を設定済みで、Firehose 長時間障害時もチャンクを溜め込まず破棄する(FireLens 自動生成の INPUT にはmem_buf_limitを注入できないため、メモリ上限はこちらで担保)60% 以上 →
1024に増量デプロイ後 1〜2 日観察し、80% 超が出るようなら
1024に引き上げ(OOM は task ごと再起動 = dev/prd 両方のブログ DB 経路が 1 分程度切れる)
IaC 構成
責任分界は 2026-06-29 タスクの方針を踏襲(CDK = インフラのスキャフォールド、ecspresso = task/service)。
層 |
ツール |
管理対象 |
|---|---|---|
ログ基盤インフラ |
CDK |
S3 バケット / Glue DB・Iceberg テーブル / Firehose / Athena WorkGroup / IAM |
Fluent Bit 設定 |
CDK |
|
log-router コンテナ定義 |
ecspresso |
|
CDK 側 (iac/aws/lib/analytics/)
st-tidb-proxy-logs スタック(dev / prd 共用なので stage prefix なし、st-tidb-proxy と同じ流儀)。
S3 バケット
tidb-proxy-logs-<account>: プレフィックスで用途分離iceberg/— テーブル本体。ライフサイクルルールを設定しない(Iceberg のマニフェストが参照するファイルを S3 側で blind に消すとメタデータ整合性が壊れるため。データ削減が必要になったら Athena のOPTIMIZE/VACUUMを先に実行してから検討する)firehose-errors/— Firehose 配信失敗レコード。30 日で expireathena-results/— Athena クエリ結果。7 日で expirefirelens-config/— Fluent Bit 設定。BucketDeploymentで git と同期autoDeleteObjects: trueのためcdk destroyでログ資産ごと消える(既存 ECR / LogGroup と同じ割り切り)
Glue:
CfnDatabase(tidb_proxy_logs) +CfnTableのopenTableFormatInput.icebergInput(metadataOperation: CREATE) で Iceberg テーブルlogsを作成tsカラムは string (ISO8601)。Firehose の JSON → Icebergtimestamp型変換のフォーマット要求に確証が持てないため、変換の失敗余地がない string で開始。Athena ではfrom_iso8601_timestamp(ts)で時刻演算するパーティションなしで開始(string 列に
daytransform が適用できないため。量が増えたらlog_typeの identity パーティション等を検討)
Firehose
tidb-proxy-logs: DirectPut、appendOnly: true、buffering 60s/64MB、失敗レコードのみfirehose-errors/へ (s3BackupMode: FailedDataOnly)Athena WorkGroup
tidb-proxy-logs: engine v3、結果出力athena-results/Athena Named Queries: よく使う3本を
CfnNamedQueryで登録 (recent-activity/destination-summary-7d/denied-or-error-access)。ECS ヘルスチェック (127.0.0.1 から30秒ごとのnc -z) が squid_access のノイズ行になるため、client_ipでの除外を基本形にしているタスクロールへの後付け権限:
st-tidb-proxyの SSM 出力 (/tidb-proxy/proxy/task-role) からRole.fromRoleArn(..., { mutable: true })でインポートし、firehose:PutRecordBatch/s3:GetObject(firelens-config) /logs:CreateLogStream・PutLogEventsを付与。blog-api-construct.tsが proxy SG にaddIngressRuleするのと同型のパターンで、稼働中のst-tidb-proxyは変更しないSSM 出力:
/tidb-proxy/logs/delivery-stream-name,/tidb-proxy/logs/firelens-config-s3-arn-prefix(ecspresso が参照)
CD (.github/workflows/deploy.yaml)
st-tidb-proxy-logsを Deploy ワークフローの stack 選択肢とallの実行順(st-tidb-proxy → st-tidb-proxy-logs → main)に追加。preview / main への push でも自動デプロイされ、FireLens 設定の S3 同期もここで走るデプロイロール (
deploy-role-stack.ts) にglue:*/firehose:*/athena:*を追加(既存の service wildcard と同じ流儀)。この権限が入る前の環境では deploy-role の再デプロイが先に必要workflow_dispatch はデフォルトブランチにワークフローが登録されていれば
--ref <PRブランチ>で PR の内容のまま実行できるため、マージ前の検証デプロイも可能
ecspresso 側 (iac/aws/ecspresso/tidb-proxy/)
tidb-proxyコンテナ:logConfigurationをawsfirelensに変更、dependsOn: [{log-router, START}]log-routerコンテナ追加:aws-for-fluent-bit:init-2.34.3(stable 系列、arm64 対応確認済み)、firelensConfiguration: { type: 'fluentbit' }、環境変数aws_fluent_bit_init_s3_1/_2に extra.conf / parsers.conf の S3 ARN。log-router 自身のログは awslogs で/ecs/tidb-proxyへotel-collector: 無変更
Fluent Bit 設定 (apps/tidb-proxy/firelens/extra.conf)
FireLens が生成する INPUT (tag
tidb-proxy-firelens-*) を受け、logキーを JSON パース →rewrite_tagで$levelによりcwlogs.*/firehose.*に分岐パース不能・level 不明の行はデフォルトタグのまま CloudWatch Logs へフォールバック
設定変更時は S3 更新だけでは反映されない。init プロセスはコンテナ起動時に一度だけ設定を取得するため、
cdk deploy st-tidb-proxy-logs後にaws ecs update-service --cluster tidb-proxy --service tidb-proxy --force-new-deploymentが必要(task def が変わらないため ecspresso diff でも検知されない)
要検証事項と結果
項目 |
結果 |
|---|---|
squid の JSON logformat |
検証済み。alpine 3.24 (squid 7.5) の Docker コンテナで |
aws-for-fluent-bit init タグの ARM64 対応 |
検証済み。 |
Fluent Bit 設定の構文 |
検証済み。 |
Glue CfnTable の Iceberg テーブル作成方法 |
実デプロイで確定。 |
Firehose→Iceberg でスキーマ外フィールドの挙動 |
|
タスク分解
設計ドキュメント作成(本ドキュメント)
forwarder の slog JSON 化 (
apps/tidb-proxy/cmd/forwarder/)squid.conf の JSON logformat 化
Fluent Bit
extra.conf作成CDK
st-tidb-proxy-logsスタック実装 + cdk-nag / テストecspresso task def への log-router 追加
01_development.mdへのデプロイ / 確認手順追記デプロイ(メモリ実測 → cdk deploy → イメージ push → ecspresso deploy)と E2E 検証