📊

S3 の時系列データを低コストで可視化する

に公開

はじめに

こんにちは。サイボウズ株式会社 生産性向上チームの星にゃーん(@takoeight0821)です。

生産性向上チームでは、社内向けに GitHub Actions のセルフホストランナーを提供しています。ランナーは AWS 上の EC2 インスタンス群で構成されています。github-aws-runners/terraform-aws-github-runner導入記事)を使い、ジョブの実行数に応じてオートスケールしています。

このオートスケールによって、ジョブが増えるほどインスタンス数や NAT Gateway 経由の通信量も膨らみます。ランナーの供給が追いつかなければ、キュー待ち時間も伸びていきます。
つまり、コストの面では NAT Gateway の通信量の内訳を、開発者の使い勝手の面ではジョブの待ち時間がどこで詰まっているかを知りたくなります。幸い、ランナー基盤の運用の中で、VPC Flow Logs、DNS Query Logs、EC2 インスタンス情報、Workflow Jobs といった必要なデータは既に S3 に保存されていました。

そこで、S3 上の時系列データを Grafana から実用的な速度でクエリできるようにしました。この記事では、Athena、Iceberg、Grafana の組み合わせ方と、この基盤を実際の運用判断にどう使っているかを紹介します。


特定の分析に最適化しない設計にする

最初のきっかけは NAT Gateway のコストスパイクでした。VPC Flow Logs を分析して、どのジョブがどこへどれだけ通信しているかを把握したいと考えました。

調査したいのは通信量の変化です。一度きりのクエリで終わらせるより、状況を継続的に監視できるダッシュボードにした方がよさそうです。

はじめに触れたとおり、気になっていたのは NAT Gateway の通信量だけではありません。セルフホストランナーは台数に限りがあり、ジョブが増えれば空きを待つ時間が延びます。待ち時間が延びれば、その間 CI/CD は進まず、開発者を待たせてしまいます。台数が足りているのか、どこで詰まっているのかを、継続的に把握したくなります。

最小の要求を整理すると、こうなります。

  1. S3 バケットの中身をクエリする
  2. 結果をダッシュボードで表示する

欲しいのは特定のログ専用の仕組みではなく、S3 上のデータを横断的に扱える土台です。データソースへの依存がない構成であれば、新しい分析要件が出ても基盤を作り直さずに済みます。

ただし、扱うログは量が多く、素朴にクエリすると遅くて実用になりません。まず解くべき問題は、このデータ規模です。


大量のログをまともな速度でクエリする

DuckDB でローカル分析し、パターンを固める

まず DuckDB(組み込み型の列指向データベース)と Grafana を Docker 上で動かし、クラウドには何もデプロイせずに S3 上のログを直接クエリできるローカル環境を用意しました。

VPC Flow Logs、DNS Query Logs、EC2 インスタンス情報、Workflow Jobs のデータを SQL で突き合わせました。「どの粒度の集計があればダッシュボードに使えるか」を探る過程で、溜まったデータを相互に参照してクエリするためのパターンがいくつか見つかりました。

DuckDB での実験をもとに、続くセルフホストの Grafana を設計しました。なぜセルフホストなのか、他の選択肢との比較を先に説明します。

Grafana をセルフホストする判断

ダッシュボードを見せたい相手は、生産性向上チームだけではありません。全社員に公開するなら、対象は 1000 人を超えます。開発系のメンバーに絞っても、開発本部を含む複数の本部にまたがり、正確な人数は把握できていませんでした。

まず候補になったのは、AWS のマネージドサービスである Amazon Managed Grafana(AMG)です。構築やパッチ適用の手間はかかりません。
ただ、課金はワークスペースごとのアクティブユーザー単位です。AWS 公式の料金ページによると、Editor は $9、Viewer は $5(ユーザー / 月)です。1000 人に Viewer ライセンスを配るだけで、単純計算でも月額 $5,000 を超えます。この規模のライセンス費用は現実的な範囲を超えており、見送りました。

次に候補になったのは、Neco(サイボウズの Kubernetes 基盤)上で運用されている Grafana です。Neco では、テナントごとにダッシュボードを自由に追加できる仕組みが整っています。Athena 用のデータソースプラグインを使えば、データを S3 に置いたまま Neco の Grafana から見せられる可能性がありました。
ただ、今回の用途ではネットワーク上の制約から選定を見送りました。

残る選択肢はセルフホストです。以前ならインフラの構築や運用に人手がかかり、避けたい選択肢だったかもしれません。
しかし、AI コーディングツールの発展で、この開発や運用のコストは下がっています。ライセンス費用がかからないこと、データソースを自由に選べることと合わせて、セルフホストを選びました。

クエリバックエンドに Athena を選んだ理由

クエリのバックエンドには Athena(S3 上のデータに直接 SQL でクエリできるサーバーレスのクエリサービス)を採用しました。DuckDB をそのまま本番運用する案も検討しましたが、DuckDB で S3 を読むには httpfs 拡張のような追加プラグインが必要です。Athena は S3 を直接読める分、軽量です。

チームでは Athena を分析用途で使った実績が既にあったことも決め手でした。DuckDB で固めたクエリパターンは、Athena の SQL にほぼそのまま持ち込めました。

Partition Projection でスキャン範囲を絞る

VPC Flow Logs は日付パーティションで S3 に保存されています。クエリ対象のテーブル定義やパーティション情報は AWS Glue Data Catalog で管理しますが、通常は増え続ける日次パーティションを都度登録し続ける必要があります。

Partition Projection は、日付条件から対象パーティションをその場で計算する Athena の機能です。AWS 公式ドキュメントでも紹介されている、日次パーティションを持つテーブル向けの定番の高速化手法です。これを使うと Glue への個別登録が要らなくなり、WHERE 句の日付範囲で必要なパーティションだけをスキャンできます。

全期間を相手にすると相当なスキャン量になりますが、1 日分に絞れば桁違いに小さくなります。

Iceberg テーブルで事前集計する

Partition Projection でスキャン量を絞っても、ダッシュボード用途には物足りません。ダッシュボードの各パネルは、NAT 経由か社内網経由かの判定、DNS Query Logs によるホスト名解決、EC2 インスタンス情報との紐付けを生ログに対して毎回 JOIN で行う必要があります。この JOIN を都度実行すると、1 クエリに 10-30 秒かかります。

そこで、ダッシュボード向けの事前集計を導入しました。EventBridge は、スケジュールやイベントをきっかけに Lambda などの処理を起動できる AWS のサーバーレスサービスです。EventBridge が Lambda を起動し、Lambda が Athena 経由で生ログを集計します。

Apache Iceberg は、S3 のようなオブジェクトストレージ上のデータを、増分更新に対応したテーブルとして扱える OSS のテーブルフォーマットです。集計結果はこの Iceberg のテーブル(Parquet + Snappy)として S3 に書き出します。初回はテーブルを作成し、以降は対象期間のデータを DELETE + INSERT INTO で差し替える差分更新をします。

集計済みテーブルへのクエリは 1-3 秒で返ります。これらの JOIN と集計は Lambda が毎時 1 回だけ行い、ダッシュボードのクエリは集計済みテーブルを絞り込んで合計するだけで済むためです。

Iceberg を選んだ理由は、Athena が行単位の更新(DELETE / UPDATE)を Iceberg 形式のテーブルにしかサポートしていないためです。通常の Hive 形式の外部テーブルでは、追記(INSERT のみ)か、CTAS によるテーブル全体の再作成しかできません。Iceberg なら DELETE + INSERT INTO だけで対象期間分を更新でき、対象期間を丸ごと置き換える操作なので、同じ時間帯を再実行しても安全です。

各段階のスキャン量とクエリ時間

ステップ スキャン量 クエリ時間
素の Athena クエリ 全期間 数十秒
+ Partition Projection 1 日分 10-30 秒
+ Iceberg 事前集計 集計済みデータのみ 1-3 秒

日次から毎時へ

しばらくは日次集計で運用していました。EventBridge から毎朝 Lambda を起動し、前日 1 日分の各テーブルを同じ方法で差し替えていました。

運用しているうちに、もっと新しいデータが見たくなりました。日次集計では、ダッシュボードに映るのは前日までの状況にとどまります。トラフィック系のテーブルは単位時間あたりの変動が大きく、いま詰まっているのか、直前のスパイクが収まったのかを日次集計では判断できません。リアルタイム性が低いと、運用判断の材料にしづらくなります。

そこで、集計を毎時に切り替えました。EventBridge から毎時 Lambda を起動し、直前 1 時間ぶんの各テーブルを差し替えます。

差分更新を毎時実行するたびに新しいデータファイルとスナップショットが積み上がるため、Iceberg テーブルに小さなファイルや古いスナップショットが溜まります。これは別の日次ジョブで OPTIMIZEVACUUM をかけて整理しています。後述するコストのうち S3 が小さく収まっているのは、この後片付けが効いているためです。


全体構成

生データは別アカウントの S3 に置いたまま、分析アカウント側で Athena と Iceberg に寄せ、Grafana から参照します。

全体構成図

ポイントは 3 つあります。

  1. 生データを持つアカウントと分析基盤を分け、読み取り専用のクロスアカウントアクセスにする。データを生む側と分析する側が独立して運用できる。
  2. 毎時のバッチで Iceberg テーブルを更新し、Grafana からはその集計済みデータだけを引く。
  3. Grafana をインターネットに公開せず、VPN と Internal ALB の内側に閉じる。認証は GitHub OAuth で、Organization のチームに応じて Editor / Viewer ロールを割り当てる。

実際に何が見えるようになったか

VPC Flow Logs でトラフィックを分析する

分析対象は、冒頭で述べたセルフホストランナー(EC2 インスタンス群)が行う通信です。VPC Flow Logs から、インバウンドとアウトバウンドのトラフィックを NAT 経由(public)と社内網経由(internal)に分類して可視化しています。ENI ID と CIDR でネットワーク経路を判定し、DNS Query Logs と突き合わせて通信先のホスト名を解決しています。さらに、ジョブ単位の通信量まで紐付けています。

次の図は、匿名化済みの固定データでジョブ別通信量を表示したダッシュボードです。

匿名化済みの固定データによるジョブ別通信量ダッシュボード

Organization、リポジトリ、ネットワーク種別、集計粒度を切り替えて、ジョブ別の通信量を比較できます。

分析の結果、トラフィックの大半は AWS と github.com の間の通信でした。当初問題視していたコストスパイク自体は、ジョブのリトライが落ち着いて解消しました。
ただ、ダッシュボードとして残しておけば、何がどの程度支配的な通信なのかをいつでも確認できます。NAT Gateway のコストが妥当か、まだ削れる余地があるかを見る材料になります。

キュー待ち時間からランナー運用を判断する

Workflow Job イベントは、GitHub Actions がジョブのキュー登録・実行開始・完了のたびに送る workflow_job Webhook イベントです。ジョブの作成時刻・実行開始時刻・完了時刻のほか、Organization・リポジトリ・ランナーラベルなどが含まれます。S3 に保存されているこのイベントも、そのまま分析に使えます。キュー待ち時間の Iceberg テーブルを 1 つ作るだけで、ジョブがキューに入ってから実行開始されるまでの待ち時間を P50 / P95 / P99 で確認できます。

次の図は、匿名化済みの固定データでキュー待ち時間とジョブ数の推移を表示したダッシュボードです。

匿名化済みの固定データによるキュー待ち時間ダッシュボード

Organization、リポジトリ、ランナーラベル別にフィルタできます。パーセンタイルごとにしきい値を設定しており(P50 なら 30 秒で黄色、60 秒で赤)、ランナー数が足りていないリポジトリが一目で分かります。

これが最も活用されているダッシュボードです。待ち時間の悪化を見つけたら、その場で対象のリポジトリやラベルを絞り込みます。ランナー数を増やすべきか、ジョブの偏りを解消すべきかを判断できます。


コスト内訳

以下は 2026 年 6 月 5 日から 7 月 4 日までの 30 日間の実測です(Cost Explorer で集計)。Grafana は 1 タスク(0.5 vCPU / 1 GB)を常時稼働させ、トラフィック系テーブルを毎時集計し、ダッシュボードからは事前集計済みの Iceberg テーブルを参照します。

コンポーネント 月間コスト 備考
ALB $17.5 時間課金 + LCU の固定費が大半
ECS Fargate Spot (1 タスク / 0.5 vCPU / 1 GB) $6.3 On-Demand 比で約 7 割引(実測 71%)
Athena $3.9 毎時バッチ + ダッシュボードの参照クエリ
Secrets Manager $1.2 OAuth 認証情報などの保管
S3 (Iceberg テーブル) $0.9 日次の OPTIMIZE / VACUUM で小ファイルを整理
Lambda $0.5 毎時実行、1 回あたり平均 50 秒ほど
その他 (CloudWatch / ECR / EFS) < $0.1
合計 約 $30

コストの約 6 割は ALB です。Grafana を常時稼働させるための固定費で、分析するデータ量にはほとんど左右されません。
一方、Athena と Lambda は従量で、毎時集計の実行回数とスキャン量に応じて増えます。

Fargate Spot は中断されることがありますが、Grafana のデータは EFS に置いているので、再起動しても消えません。


まとめ

S3 に時系列データがあり、それを手軽に可視化したいと考えました。特定の分析専用ではなく、S3 上のデータを横断的に扱える汎用の基盤にしました。AI コーディングツールを活用し、セルフホストで構築しています。
規模の問題は、Partition Projection と Iceberg 事前集計の 2 段階で解決しました。運用コストは月額 $30 ほどです。この設計のおかげで、VPC Flow Logs のトラフィック分析から始まった基盤は、作り直すことなくジョブのキュー待ち時間の可視化にも広がりました。

サイボウズ 生産性向上チーム 💪

Discussion