GitHub Actionsのワークフローを可視化するactions-timelineを作った
先日、GitHub Actionsに1行追加するだけでワークフローを時系列に可視化できるactions-timelineというactionをリリースしました。
使い方は以下のように Kesin11/actions-timeline
をジョブの最初に追加するだけです。
jobs:
build:
runs-on: ubuntu-latest
steps:
# actions-timelineはpost処理の最後に実行されないと全てのジョブの情報を収集できないため、
# 呼び出しは `actions/checkout` などよりも前の一番最初に行います。
- uses: Kesin11/actions-timeline@v1
with:
# PATを利用する場合は: ${{ secrets.MY_PAT }}
# デフォルトは ${{ github.token }} が使われるので通常は省略可能です
github-token: ''
# Your build steps...```
このようにワークフロー全体の中でどこどれだけ時間がかかっているかが一目で分かる図を生成してくれます。ボトルネックがどこなのかが一目で分かるため、CIの時間をチューニングする際に参考となるはずです。GitHub Actionsのサマリーページ中のMarkdownでMermaidによって描画されているため、GitHubだけで完結している手軽さが特徴です。
actions-timelineの実行結果サンプル
実はCIの実行時間を可視化・分析するツールは以前から何度か作成していて、actions-timeline
で3つ目になります。
今までのツールとの違いを軽く紹介しつつ、actions-timeline
の作成に至った経緯を紹介します。
CIAnalyzer
誰が設定できる | リポジトリのREAD権限があるユーザー |
分析機能 | ジョブの統計情報 |
環境構築 | GCS、BigQuery |
最初に作ったのがCIAnalyzerです。なるべくツール自体の運用の手間がかからないように常駐サーバー無し、データの保存先と可視化はマネージドサービスを使う前提で設計しました。具体的にはデータの保存先をBigQueryとすることによって自前でDBを管理する必要をなくし、webhookを受けるのではなくcronで定期的にAPIを叩くことで常駐サーバーを不要にし、データの可視化はBigQueryと簡単に連携できてマネージドサービスであるLooker Studioを使用する前提としました。
CIAnalyzerのアーキテクチャ
CIAnalyzerを作ったきっかけはAzure Pipelineの分析機能に感銘を受けたことで、それと同等の分析を当時自分が業務とプライベートで使用していたJenkins, CircleCI, Bitrise, GitHub Actionsでも可能にしたいと思って開発を始めました。
現在ではCircleCIにはInsightとTest Insightsでそれぞれパイプラインとテストの分析機能が無料で提供されており、Bitriseでも同様のInsights機能が一部無料で提供されていますが、CIAnalyzerを作成した当時はそれらの機能は存在してなかったと記憶しています。GitHub Actionsに関しては現在も、そして残念なことに近い未来にもGitHubからこのようなビルドやテストの分析機能をリリースする見込みはなさそうです[1]。
CIAnalylzerは前職で開発チームのJenkinsやGitHub Actionsのサポートをしていた際にCI/CDのジョブを改善するのにとても役立ちました。CIAnalyzerを利用してJenkinsのビルド情報をBigQuery + Looker Studioで分析して改善に繋げた実例を過去にCI/CD Conferenceで紹介しました。
CIAnalyzerについては過去に以下の記事で詳しく紹介したことがあるので、もし興味があればこちらも参照してみてください。
最近は、この後紹介する2つのツールを作るために新機能を追加するような開発はできていないのですが、Renovateで依存ライブラリのアップデートやドッグフーディングは続けているので一応メンテは続けています。最近転職した先ではGoogle CloudよりAWSの方が主流なこともあり、そろそろデータの保存先としてBigQuery以外にS3互換ストレージへ保存する機能ぐらいは追加してみようかなという気持ちも若干あります。
後に分かってきた問題点
CIAnalyzerは自分が自分のために開発したものなので最初は当然便利に利用していたのですが、自分以外の方に利用してもらったり、自分自身も業務内容が変わっていくにつれて使いにくい点も見えてきました。
まず、CIAnalyzerはジョブ自体やその内訳となる各ステップごとにかかっている時間の平均や中央値といった統計情報の分析は得意でしたが、1回のジョブがなぜか妙に遅い場合などの詳細な分析はLooker Studioでは行いにくく、結局は生のビルドログを見に行く必要がありました。BigQueryにはジョブ1回あたりの詳細なデータ自体は存在しているのですが、Looker Studioでは折れ線グラフや棒グラフといったグラフしか描画できないためです。
また別の問題として、CIAnalyzerはデータを収集する対象のリポジトリをyamlで指定する方式であったため、新しいリポジトリが作られるたびに毎回yamlを編集する必要がありました。これはこれでリポジトリごとに柔軟な設定が可能になるメリットも存在し、実際にそれを利用した機能の提供もしていた[2]のですがリポジトリの数が増えるにつれてyamlの編集が面倒というデメリットが目立ってきました。
指定したOrganizationのリポジトリを自動発見して一律デフォルト設定で収集するような機能の実装も考えたことがありますが、その場合は各種CIサービスのAPIレートリミットを上手く制御するための実装もおそらく同時に考える必要があるため二の足を踏んでいました。例外としてJenkinsはAPIのレートリミットを気にする必要がないためこの機能を実装済みです。
問題点をまとめると、1回のジョブの詳細をいい感じに可視化する図を描画できなかったことと、限られたリポジトリだけの面倒を見るユーザーの立場からは使いやすかったものの、Organization以上の規模で横断的にCI基盤を運用するような管理者の立場からは逆に使いにくい点が分かってきました。
github_actions_otel_trace
誰が設定できる | OrganizationまたはEnterpriseのAdminユーザー(webhookを設定する必要があるため) |
分析機能 | ジョブの統計情報、1回のジョブの詳細な時間内訳 |
環境構築 | OpenTelemetry Tracesに対応したサービス、OpenTelemetry Collectorのagentを動かす環境 |
CIAnalyzerの問題点を解決するために別のアプローチを採用したのが github_actions_otel_trace
です。
まずCIAnalyzerでは実現できなかった1回のジョブの詳細をいい感じに可視化する方法に関しては、Webサーバーの監視方面で盛り上がっている分散トレーシングのUIがジョブの分析にも向いていると思いました。有名なOSSとしてはJaeger、AWSではX-Ray、Google CloudではCloud Traceなどです。本来は時系列でどのサーバーのどの処理にどの程度時間がかかっているかを可視化するためのUIですが、CIにおけるワークフロー、ジョブ、ステップという階層構造と似ているので、これもしかしてGitHub Actionsのジョブの情報をTraceの形式に変換すればいい感じに描画してくれるのでは!?と思いつきました。
直接JaegerやGoogle CloudなどのTraceに対応したサービスのAPIでデータを送ることも可能なのですが、その代わりにOpenTelemetry Collector経由でデータを送るようにしました。OpenTelemetry Collectorはログ収集のagentのようなもので送られたデータを加工したり各種サービスに転送してくれるのですが、それらの機能は設定ファイルのyamlに書くだけです。OpenTelemetry Collectorを経由することでユーザーはTraceに対応したサービスを自由に選択できるようになりますし、こちらも送信先のサービスごとにコードを実装する手間が不要になりました。オープンな規格に乗っかるメリットですね。
またCIAnalyzerでのリポジトリが増えるごとにyamlを編集する手間の問題に対してはGitHubからのwebhookを受け取る方式に変更しました。webhookはOrganizationやEnterprise単位で設定できるため、一度設定してしまえばリポジトリが増えても何か作業をする必要はありません。webhookを受け付けるためのサーバーを立てておき、ジョブが完了した際のwebhookを受け取ったらGitHubのAPIからジョブの詳細データを取得し、その内容をOpenTelemetryのTrace形式に変換します。
github_actions_otel_traceのアーキテクチャ図
自分自身でのドッグフーディングはwebhookを受けるサーバーとしてCloud Runを利用しています。当時はECSやk8sで知られているサイドカーコンテナ構成がCloud Runでは不可能だったので github_actions_otel_trace
のwebhookを受けるサーバーとOpenTelemetry Collectorを1つのコンテナに同居させています[3]。現在はCloud Runでもサイドカーコンテナ構成が可能になったため、OpenTelemetry Collector公式のイメージをサイドカーとして立ち上げる構成もそのうち試してみたいと思っています。
自分用のGoogle Cloudによる構成
後に分かってきた問題点
自分のドッグフーディングではデータの送信先としてGoogle CloudのCloud Traceを利用しているのですが、少なくともジョブのデータを可視化・分析する用途としてはCloud Traceはイマイチです。まず致命的な問題点として、データの保持期間が最大30日[4]のためBigQueryの力によってデータの保持期間を自由にできたCIAnalyzerの代替にはなり得なかったです。UI的な観点においても、Cloud TraceはWebサーバーのTraceデータを扱う前提でUIが構成されているため検索やフィルターが使いにくい印象でした。
Cloud TraceのUI。レイテンシとかHTTPメソッドとか出されても・・・本来的にはその方が正しい使い方ではあるけども
一方で、1回のジョブの詳細を可視化する観点に関しては狙い通りの結果が得られました。ワークフロー全体を見たときに、どのジョブが並列に実行され、どのジョブが直列に実行されているかが一目で分かることによってどこが実行時間のボトルネックとなっているのかが一目で分かるようになりました。
ジョブの情報をTraceとしてCloud Traceで表示させてみた例
問題点は全てCloud Traceに由来するものなので、もしかするとDatadogやGrafanaなどの別のサービスであればこのあたりの問題点は感じなかったかもしれません。OpenTelemetry Collectorによってサービスを切り替えることは容易なので、いずれCloud Trace以外のサービスでも試してみたいと思っています。
actions-timeline
誰が設定できる | リポジトリのWRITE権限を持つユーザー |
分析機能 | 1回のジョブの詳細な時間内訳 |
環境構築 | 無し(GitHub内だけで完結) |
github_actions_otel_trace
を改良する余地はあるものの時間がかかりそうなのと、ちょうど最近転職した先でGitHub ActionsのCI時間を改善したいという話をしていたので、管理者寄りのアプローチから一転してユーザー側がもっと気軽に使えて即効性があるツールを作ってみようと思いました。CIAnalyzerもユーザー側の方が利用しやすいツールなのですが、転職先はGCPよりもAWSがメインであったため現状ではGCS + BigQueryを想定しているCIAnalyzerをすぐにセットアップしてもらうのが難しそうだったという事情もありました。
GitHub Actionsの分析に特化させるならば使い方はワークフローの中でactionを呼び出してもらうだけで済むぐらいに簡単にした上で、可視化の図なども含めて出力は全てGitHub上で完結させることによって導入コストを徹底的に下げることを目指しました。
GitHub内で完結させるアイディアはK1Low/octocovのように従来は外部サービスを見に行くのが当たり前であった機能をMarkdownの描画力を活用したり、どうしても必要になりがちなDBの役割すら工夫次第ででGitHub内で完結可能であることに感銘を受けたところから来ています。
最初に考えたのは github_actions_otel_trace
で実現できたTraceを可視化するための図[5]をなんとかしてGitHub上で描画する方法でした。最悪、svgやpngとして生成すればいけるとは思っていましたが、できればMarkdownとして可能な図の範囲内で表現したかったので最近GitHubでサポートされたMermaidを調べてみました。
Mermaidが描画できる様々な図を眺めていたところガントチャートがTraceの図とかなり近いことに気がつきました。ガントチャートは本来スケジュール管理などに使われるものなので普通は日付単位の描画を想定しており、さらにリンク先のサンプル図から分かるように横軸は絶対時間なのですが、ワークフローの開始時間を00:00:00時として相対時間で表現することによってそれっぽい見た目になることをダミーデータのPOCで確認しました。
見た目の問題をクリアできたので、残りの実装はCIAnalyzer, github_actions_otel_trace
で実装してきたようにワークフローやジョブのAPIのデータから各ステップにかかった時間を計算してMermaidのガントチャートのフォーマットに変換しただけです。ついでにジョブがトリガーされてから実際に動き出すまでのキュー待ち時間も表示させてみました。
実装完了後にダミーデータではなくて実際にactionとして実行した本物の結果はこんな感じです。
actions-timelineのワークフローを可視化した図
なかなかいい感じに時系列が把握できる図になっていると思いませんか?ワークフロー全体でどのジョブにどれだけ時間がかかっていて、さらにどのステップにどれだけ時間がかかっているのかが分かるかと思います。今までもビルドログを見れば分かることではありましたが、一目で直感的にボトルネックを把握できるのでCI/CDのチューニングを考える際にどこから手を付けるべきかがとても分かりやすくなるはずです。
自分の経験上、CI/CDにかかっている時間をチューニングする場合には闇雲に手を付けるのではなく、以下のような観点から優先度を付けて取り組むべきです。
- 他と比較して圧倒的に時間がかかるジョブ・ステップ
- 比較的短時間だが実行される回数が多いためにトータルとして時間がかかるステップ
- ツールやパッケージのインストールなど、1回あたりは短時間でもmatrixなどで大量に並列で実行される場合はトータルとして時間を消費するポイントになる
- ワークフローの中で直列にしか実行できないジョブ
-
needs
によってジョブ間に依存関係が必要な場合、並列化によって時間を短縮できないためワークフロー全体から見たときのボトルネックになりやすい
-
CIAnalyzerではジョブやステップ単位での実行時間の中央値やパーセンタイル(90%タイルなど)といった統計情報を見ることでボトルネックを把握できました。actions-timelineはDBにデータを蓄積しないために残念ながらこのような統計情報を調べることはできませんが、その代わりに別角度からボトルネックを把握できます。
実際のところ、CI/CDの専任者でもない限りCIAnalyzerによるビルドの統計情報を必要とすることは滅多にないと思いますので、大半の人にとってはactions-timelineが提供する図で十分だと思います。
Deno
最後に今回の話題からは脱線しますが、actions-timelineはDenoで実装しています。通常はjavascriptタイプのactionをDenoで実装はできないのですが、dntでNode.js用のjsコードに変換してからesbuildでaction用の単一のjsファイルにバンドルするという手法でjavascriptタイプのactionとしてリリースしています。この方法はおそらくまだ前例が多くなく、これ単体でも面白い話題だと思いますので後日別記事として紹介する予定です。
追記: 12/13
DenoでActionsを作成するテンプレートリポジトリを作成したので解説記事を書きました。
まとめ
actions-timelineの紹介と、これを作るまでの経緯と過去のツールの紹介をしました。
現在のイチオシは当然最新作のactions-timelineですが、CIAnalyzerやgithub_actions_otel_trace
はそれぞれにまた違った特徴があるので、自分の用途に合いそうだと感じたらぜひ使ってみてください。issueやpull-requestもお待ちしています。
それではまたGitHub Actionsかその他のCIサービスの世界のどこかでお会いしましょう。では。
-
少なくともパブリックに公開されているロードマップ上では分析機能のissueは見当たらないように思います ↩︎
-
GitHub ActionsやJenkinsのアーティファクトに任意のパスで保存したJUnit形式のXMLからテスト結果を抽出する機能や、自由なスキーマのJSONの中身そのままBigQueryに送る機能があります ↩︎
-
リポジトリに含めているDockerfileが同居させる構成のイメージです ↩︎
-
https://cloud.google.com/trace/docs/trace-export-overview の"To store trace data for a period longer than the default retention period of 30 days." より ↩︎
-
ちなみにこういう図を一般的になんと呼ぶのか未だによく分かっていません。CI/CD界隈ではTraceという概念が一般的ではないので見た目から
timeline
という名前を付けてaction名にしました ↩︎
Discussion