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 セット)

  • ステータス: 実装済み

  • 関連タスク: blog-api: Lambda + tsnet を VPC + Fargate Proxy 構成に移行

  • 関連実装: 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 しかない。本タスクで以下に切り替える。

  1. アプリのログを JSON 構造化する(forwarder は log/slog、squid はカスタム logformat

  2. タスクに 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、従来どおり障害調査用)

  3. Iceberg テーブルは Athena (WorkGroup tidb-proxy-logs) で検索する

設計判断の前提

  • カスタム Fluent Bit イメージは作らないaws-for-fluent-bitinit タグイメージは 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/ERROR

  • tsnet 内部ログ・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) が既に同居しているため、無条件に増やさず実測で決める。

  1. デプロイ前に AWS/ECS MemoryUtilization (ClusterName=tidb-proxy, ServiceName=tidb-proxy) の直近 7 日の Maximum を確認

  2. 60% 未満 (≈300MB)512 据え置き。Firehose 出力に Retry_Limit 2 を設定済みで、Firehose 長時間障害時もチャンクを溜め込まず破棄する(FireLens 自動生成の INPUT には mem_buf_limit を注入できないため、メモリ上限はこちらで担保)

  3. 60% 以上1024 に増量

  4. デプロイ後 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

apps/tidb-proxy/firelens/extra.confBucketDeployment で S3 に同期

log-router コンテナ定義

ecspresso

ecs-task-def.jsonnet (firelensConfiguration / environment)

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 日で expire

    • athena-results/ — Athena クエリ結果。7 日で expire

    • firelens-config/ — Fluent Bit 設定。BucketDeployment で git と同期

    • autoDeleteObjects: true のため cdk destroy でログ資産ごと消える(既存 ECR / LogGroup と同じ割り切り)

  • Glue: CfnDatabase (tidb_proxy_logs) + CfnTableopenTableFormatInput.icebergInput (metadataOperation: CREATE) で Iceberg テーブル logs を作成

    • ts カラムは string (ISO8601)。Firehose の JSON → Iceberg timestamp 型変換のフォーマット要求に確証が持てないため、変換の失敗余地がない string で開始。Athena では from_iso8601_timestamp(ts) で時刻演算する

    • パーティションなしで開始(string 列に day transform が適用できないため。量が増えたら 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 コンテナ: logConfigurationawsfirelens に変更、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 コンテナで squid -k parse + 実 CONNECT リクエストを流し、%{...}tg の ISO8601 時刻と %" エスケープ(User-Agent 内の ")が valid JSON になることを確認

aws-for-fluent-bit init タグの ARM64 対応

検証済みinit-2.34.3(stable 系列)が amd64/arm64 のマルチアーキで存在することを docker manifest inspect で確認し、このタグに pin

Fluent Bit 設定の構文

検証済みaws-for-fluent-bit:stable (Fluent Bit 1.9) の --dry-run で configuration test successful

Glue CfnTable の Iceberg テーブル作成方法

実デプロイで確定TableInput は CFN 必須のため、メタデータ (name / columns / location) は全て TableInput 側に書き、IcebergInputMetadataOperation: CREATE + Version のみとする。icebergTableInput (ネイティブスキーマ形式) を TableInput と併用すると Glue が "Table metadata is expected only via TableInput or via IcebergTableInputProperties" で CREATE_FAILED になる

Firehose→Iceberg でスキーマ外フィールドの挙動

record_modifierAllowlist_key でスキーマ列だけに絞る事前除去を実装済み。デプロイ後に firehose-errors/ が空であることを確認する

タスク分解

  1. 設計ドキュメント作成(本ドキュメント)

  2. forwarder の slog JSON 化 (apps/tidb-proxy/cmd/forwarder/)

  3. squid.conf の JSON logformat 化

  4. Fluent Bit extra.conf 作成

  5. CDK st-tidb-proxy-logs スタック実装 + cdk-nag / テスト

  6. ecspresso task def への log-router 追加

  7. 01_development.md へのデプロイ / 確認手順追記

  8. デプロイ(メモリ実測 → cdk deploy → イメージ push → ecspresso deploy)と E2E 検証