【実践編】GitLab Self-hosted(AWS)のSLI/SLOを定義し、CloudWatchで表現する
【実践編】GitLab Self-hosted(AWS)のSLI/SLOを定義し、CloudWatchで表現する
この記事は、SLI/SLOを「定義し、実践し、運用で育てる」までを扱う3部作の2本目・実践編です。
- 【概念編】SLI/SLOとは何か——ユーザー体験から信頼性を定義する: SLI/SLOの基本的な考え方と作り方
- 【実践編】GitLab Self-hosted(AWS)のSLI/SLOを定義し、CloudWatchで表現する(この記事): 実際のインフラ構成に当てはめる
- 【運用編】SLI/SLOをチームに定着させ、運用で育てる: 定義したSLI/SLOをチームに定着させ、育てていく方法
免責事項: 本文中では、一般的なSLI/SLOの考え方に加えて、「コラム」としてPlatform SREがどう向き合うかという筆者個人の見解を時折挟んでいます。唯一の正解を示すものではなく、考え方の一例としてお読みください。
この記事について
この記事は、概念編で紹介した「User Journey → SLI Specification → SLI Implementation → SLO → 計測漏れ確認 → 運用判断」という型を、実際にAWS上で運用しているSelf-hosted GitLab環境に当てはめてみます。
概念編は画像投稿SNSという仮想の例でしたが、今回は具体的なインフラ構成を題材にすることで、「教科書どおりにはいかない部分」がどこに出てくるかを具体的に見ていきます。
想定読者
- 概念編を読んで、次は実際の構成でどう考えるかを見たい方
- Self-hosted GitLabをAWSで運用していて、CPU・メモリ・ディスクなどインフラ指標のアラームは設定済みだが、ユーザー体験を表すSLI/SLOがまだない方
対象環境
今回題材にする環境は、次のような構成のSelf-hosted GitLabです。
- インターネット向けALBがTLSを終端し、GitLab本体(Omnibusパッケージのオールインワン構成、組み込みPostgreSQL、EBS上にデータ)が動く単一のEC2インスタンスへHTTPで転送する
- Git over SSHはALBを経由せず、EC2のElastic IPへ直接22番ポートで接続する
- VPC内部向けの内部ALBもあり、Runner Managerなど内部通信に使われる
- CI/CDはRunner Manager(常設EC2)と、docker-autoscaler executor + fleeting-plugin-awsによる、ジョブに応じて0台からスケールするRunnerのASGで構成されている
- コスト最適化のため、GitLab本体とRunner Managerは毎晩22:30〜05:30(JST)の間、EventBridge Schedulerにより停止している。この時間帯はCloudWatchアラームも無効化されている
- 現状のCloudWatchには、CPU使用率・メモリ使用率・ディスク使用率・ALBのUnhealthyHostCount・バックアップ欠如の6つのアラームが設定されているが、ダッシュボードはない
- gitlab-rails・gitlab-shell・gitlab-runnerといったアプリケーションログはEC2ローカルにのみ出力されており、CloudWatch Logsへは送られていない
構成を図にすると、次のようになります。HTTPSはALBを経由しますが、SSHはALBを経由せずEC2へ直接届く点(赤い点線)が、後述する計測漏れの核心です。
なぜ今のアラームだけでは不十分か
現状の6つのアラームは、いずれも概念編で紹介したアンチパターン1「CPU使用率やメモリ使用率をSLIにする」に該当します。
CPU使用率が低くても、次のような状態はあり得ます。
- SSH鍵の認証は通るのに、git pushがタイムアウトする
- CI/CDのジョブがキューに溜まっていて、いつまでもRunnerが起動してこない
- ディスクには余裕があるのに、EBSのスループット上限に達していて書き込みが詰まっている
これらは「サーバーが動いているか」ではなく「開発者がGitLabを使って開発を進められているか」という観点でしか捉えられません。そこで、概念編の型に沿ってCritical User Journeyから考え直します。
Critical User Journeyを選ぶ
GitLabで開発者が日常的に行う操作のうち、まず次の2つを対象にします。
ユーザーがgit push/pullできる(SSH・HTTPS)
ユーザーがCI/CDパイプラインを実行できる
いずれもGitLabの提供価値の中核であり、これが壊れると開発チーム全体の作業が止まります。
ジャーニー1: git push/pull
Step 1〜2: ユーザー期待とSLI Type
ユーザーは、git pushやgit clone/git fetchを実行したら、それが妥当な時間内に成功することを期待しています。SLI TypeはAvailabilityとLatencyです。
Step 3〜4: SLI SpecificationとSLI Implementation
ここで重要なのが、HTTPS経由とSSH経由で通り道が違うという点です。
今回の構成では、HTTPS経由のgit操作はALBを経由しますが、SSH経由はALBを経由せずEC2へ直接届きます。つまり、片方だけを見ていては全体のAvailabilityを表せません。
HTTPS経由 SLI Specification
有効なgit push/pull(HTTPS)リクエストが成功した割合
HTTPS経由 SLI Implementation
gitlab-railsのアプリケーションログ(production_json.log相当)のうち、git-upload-pack / git-receive-pack へのリクエストで、HTTPステータス2xxを返したものを成功とする。
401/403(認証・権限エラー)、404(存在しないリポジトリ)はユーザー起因としてサービス失敗から除外する。
5xxおよびタイムアウトは失敗とする。
SSH経由 SLI Specification
有効なgit push/pull(SSH)リクエストが成功した割合
SSH経由 SLI Implementation
gitlab-shellのログのうち、git-upload-pack / git-receive-pack の 実行結果で、正常終了したものを成功とする。
鍵未登録・権限エラーはユーザー起因として除外する。
接続タイムアウト、内部API(GitLab本体への認可確認)呼び出し失敗はサービス失敗とする。
HTTPS側はALBのアクセスログではなく、あえてgitlab-rails側のアプリケーションログを情報源にしています。ALBのログはHTTPステータスとパスしか分からず、「どのユーザーの、どのリポジトリへの操作か」まで追えないためです。
Step 5: SLOを書く
過去30日間(夜間停止時間帯を除く)において、git push/pull(HTTPS・SSH合算)の99.5%以上が成功する。
過去30日間(夜間停止時間帯を除く)において、git push/pull(HTTPS・SSH合算)の95%以上が10秒以内に完了する。
数値は現時点での仮置きです。概念編のコラムでも触れたとおり、SLOに絶対的な正解はありません。実際にはまず計測を始め、直近数週間の実績値を確認したうえで、チームとして「これなら許容できる」と合意できる値に調整していくことになります。
ジャーニー2: CI/CDパイプライン実行
Step 1〜2: ユーザー期待とSLI Type
ユーザーはパイプラインをトリガーしたら、Runnerがジョブを実行し、妥当な時間内に結果(成功/失敗)が返ってくることを期待しています。SLI TypeはAvailability(ジョブが実行される)とLatency(ジョブが完了するまでの時間)です。
Step 3〜4: SLI SpecificationとSLI Implementation
この環境ではRunnerが0台からスケールするfleeting構成のため、「ジョブが成功したか」だけでなく「ジョブがそもそも実行され始めたか(キューで詰まっていないか)」も分けて考える必要があります。
Availability SLI Specification
有効なCI/CDジョブが正常に完了した割合
Availability SLI Implementation
gitlab-runnerのジョブログのうち、
ジョブが実行され、success または failed(ユーザーのコードに起因する失敗)
で終了したものを成功とする。
Runner側の要因(system_failure、Runnerインスタンスの起動失敗、
docker-autoscalerのプロビジョニングエラー)による失敗はサービス失敗とする。
Latency SLI Specification
有効なCI/CDジョブが、キュー投入から一定時間以内に実行開始した割合
Latency SLI Implementation
gitlab-runnerのジョブログから、ジョブがキューに入った時刻と
実行が開始された時刻の差分を算出する。
fleeting ASGのインスタンス起動待ちによる遅延も含めて計測する。
「成功したジョブの割合」だけを見ると、fleetingのインスタンス起動が遅れて実行開始できていないジョブが漏れてしまいます。ここは概念編のStep6「計測漏れの確認」がそのまま当てはまる部分です。
Step 5: SLOを書く
過去30日間(夜間停止時間帯を除く)において、有効なCI/CDジョブの99%以上が実行される(Runner起因の失敗が1%未満)。
過去30日間(夜間停止時間帯を除く)において、有効なCI/CDジョブの95%以上が、キュー投入から5分以内に実行開始する。
計測手段: 今の構成から何を追加する必要があるか
現状、CloudWatchに集約されているのはインフラメトリクス(CPU/メモリ/ディスク/ALB)のみで、上記のSLIを計算するために必要なアプリケーションログはEC2ローカルにしかありません。
そのため、SLI/SLOを実際に計測できるようにするには、まず次の準備が必要です。
- CloudWatch Agentの設定を追加し、GitLab本体EC2からgitlab-rails(production_json.log相当)とgitlab-shellのログをCloudWatch Logsへ送る
- 同様にRunner Manager/RunnerからgitLab-runnerのジョブログをCloudWatch Logsへ送る
- ALBのアクセスログは現状S3出力のみのため、Logs Insightsで横断的にクエリしたい場合はアプリケーションログ側を主な情報源とする(ALBログは「ALBまで到達したか」の裏付け程度に使う)
その上で、CloudWatch Logs Insightsで次のようなクエリを組み、成功率やレイテンシを集計します(フィールド名は実際のログ出力形式に合わせて読み替えてください)。
git push/pull成功率(HTTPS)のイメージ
fields @timestamp, method, path, status
| filter path like /git-upload-pack|git-receive-pack/
| stats count(*) as total,
sum(status < 300) as success
by bin(1h)
CI/CDジョブのRunner起因失敗率のイメージ
fields @timestamp, job_id, status, failure_reason
| filter status = "failed"
| stats count(*) as total_failed,
sum(failure_reason = "system_failure") as runner_caused
by bin(1h)
これらのクエリ結果をメトリクスフィルタでカスタムメトリクス化し、CloudWatchダッシュボードに次のようなパネルを並べると、日常的に状態を確認しやすくなります。
| パネル | ソース | 表示内容 |
|---|---|---|
| git push/pull成功率(HTTPS/SSH別) | gitlab-rails / gitlab-shellログ | 時間帯ごとの成功率 |
| git push/pullレイテンシ(p95) | gitlab-rails / gitlab-shellログ | レスポンスタイムの分布 |
| CI/CDジョブ成功率 | gitlab-runnerログ | Runner起因失敗を分けて表示 |
| CI/CDキュー待ち時間(p95) | gitlab-runnerログ | fleeting起動待ちを含む待ち時間 |
| 既存インフラアラーム | CloudWatch Alarms | CPU/メモリ/ディスク/ALB/バックアップ |
既存のインフラアラームと並べて表示することで、「インフラは正常だがユーザー体験は劣化している」という概念編冒頭で挙げたギャップに気づきやすくなります。
ログ収集からダッシュボード表示までの流れを図にすると、次のようになります。現状は矢印部分(CloudWatch Agentの設定)が未実装のため、まずここを整備する必要があります。
計測漏れの確認
概念編のStep6に沿って、実際の構成を踏まえた計測漏れをいくつか洗い出します。
- SSHがALBを経由しない: ALBのメトリクスやログだけを見ていると、SSH経由のgit操作の障害には一切気づけません。EC2側のgitlab-shellログを別系統として必ず含める必要があります。
- 夜間停止スケジュール: GitLab本体とRunner Managerは22:30〜05:30(JST)に計画停止しています。この時間帯をSLOの測定期間に含めてしまうと、意図した計画停止まで「失敗」としてカウントしてしまい、SLOの数値が実態と合わなくなります。測定クエリ側でこの時間帯を除外する必要があります。
- Runner Managerの単一障害点: fleetingのRunnerは0台から起動するため、Runner Manager自体が落ちると新規ジョブが一切実行されなくなります。「ジョブの成功率」だけでなく、「そもそもジョブが起動し始めたか」を分けて見ておかないと、この種の障害を見逃します。
-
アラーム通知が無効化されている: 現状
notifications_enabled = falseとなっており、既存のインフラアラームですら実際には通知が飛んでいません。SLI/SLOを計測できるようにしても、それをアラームや運用判断に使う仕組み(通知の有効化、週次レビューなど)がなければ、概念編のアンチパターン9「作っただけで運用しない」に陥ります。
エラーバジェットの消費速度(Burn Rate)を実装し、枯渇時に備える
概念編で触れたとおり、エラーバジェットは「残量」だけでなく「消費速度(Burn Rate)」で見る必要があります。
ここでは、その考え方を今回のGitLab環境の具体的な数値とCloudWatchの実装に落とし込みます。枯渇時にチームとしてどう対応していくかは、運用編でさらに扱います。
30日SLOをBurn Rateの閾値に変換する
今回定義したSLOから、許容エラー率(=エラーバジェット)は次のとおりです。
| ジャーニー | SLO | 許容エラー率(バジェット) |
|---|---|---|
| git push/pull(HTTPS・SSH合算) | 99.5% | 0.5% |
| CI/CDジョブ実行 | 99% | 1% |
Google SREのマルチウィンドウ・マルチバーンレートの考え方に沿って、長いウィンドウ(検知の確実性)と短いウィンドウ(異常解消時に素早くアラートを解除するため)を組み合わせ、3段階の重大度を設定します。
| 重大度 | 長いウィンドウ / 短いウィンドウ | Burn Rate | git push/pullの実エラー率閾値 | CI/CDの実エラー率閾値 | 対応 |
|---|---|---|---|---|---|
| Fast burn(Page) | 1時間 / 5分 | 14.4 | 7.2% | 14.4% | オンコールへ即時通知 |
| Mid burn(緊急チケット) | 6時間 / 30分 | 6 | 3% | 6% | 当日中に調査 |
| Slow burn(通常チケット) | 3日 / 6時間 | 1 | 0.5% | 1% | 定点観測会で改善タスク化 |
長いウィンドウだけを見ていると「短時間で急激に消費しているが、まだ月間の残量には大きく影響していない」状態を見逃します。短いウィンドウは、長いウィンドウの閾値超えを裏付けると同時に、異常が収まった後にアラートを速やかに解除する役割を持ちます。
Fast burnを例に、長いウィンドウ・短いウィンドウのAND条件を図にすると次のようになります。
両方のウィンドウが同時にしきい値を超えたときだけ通知することで、瞬間的なノイズでの誤検知を抑えつつ、異常が収まった直後には短いウィンドウの値が下がってアラームも速やかに解除されます。
CloudWatchでBurn Rateを計算する
「計測手段」で作成したメトリクスフィルタ(HTTPS/SSH/CI/CDそれぞれのtotal・failureカウント)を使い、メトリクス数式でウィンドウごとのエラー率を算出します。
git push/pull(HTTPS+SSH合算、1時間ウィンドウの例)
m1 = SUM(GitHttpsTotal) # 期間(Period) = 3600秒
m2 = SUM(GitHttpsFailure)
m3 = SUM(GitSshTotal)
m4 = SUM(GitSshFailure)
e1 = 100 * (m2 + m4) / (m1 + m3) # 直近1時間のエラー率(%)
このメトリクス数式に対して、しきい値7.2%・評価期間1データポイントのCloudWatchアラームを作成します。同じ数式を期間(Period)だけ300秒に変えたアラームをもう一つ作成し、両方が同時にALARM状態になったときだけ通知するCloudWatch複合アラーム(Composite Alarm)にまとめることで、長いウィンドウ・短いウィンドウのAND条件を表現します。6時間ウィンドウ・3日ウィンドウの中程度/慢性的なBurn Rateも同様に、期間とevaluation periodsを変えて同じ構成で作成します。CI/CDのRunner起因失敗率についても、同じ考え方でジョブのtotal・failureメトリクスから同様のアラームを組みます。
夜間停止スケジュールとの整合
既存の6アラームと同様に、22:30〜05:30(JST)はGitLab本体・Runner Managerが停止しているため、この時間帯を含むウィンドウでBurn Rateを評価すると「リクエストがそもそも来ていないだけ」なのに不安定な値が出たり、逆に停止直前・直後のわずかなリクエストで極端なエラー率が出たりします。既存アラームを無効化しているEventBridge Schedulerのターゲットに、Burn Rate用の複合アラームおよび子アラームのdisable-alarm-actions/enable-alarm-actionsも追加し、同じ運用時間帯だけを評価対象にします。
1日のスケジュールとSLO測定対象時間の関係を図にすると、次のようになります。
サンプル数が少ない時間帯への対処
夜間停止明けの早朝や深夜バッチのみの時間帯は、そもそもの母数(total)が数件しかないことがあります。この状態でエラー率(%)だけを閾値にすると、1〜2件の失敗で瞬間的に閾値を超え、実際のユーザー影響がほとんどないのにFast burnアラームが発火します。メトリクス数式にIF関数を使い、「母数が一定件数(例: 20件)未満のときはアラーム評価を行わない(0または欠測値を返す)」といったガードを入れておくと、この種の誤検知を抑えられます。
エラーバジェット枯渇時の対応(エラーバジェットポリシーの具体化)
運用編で挙げたエラーバジェットポリシーの項目を、このGitLab環境向けに具体化すると次のようになります。
なお、この環境は顧客向けプロダクトではなく社内向けの開発基盤であるため、運用編でいう「プロダクトチーム」と「SRE」は分離しておらず、Platform/Infraチームが両方の役割を兼ねる前提です。
| 項目 | 内容 |
|---|---|
| 対象 | git push/pull(HTTPS・SSH)、CI/CDジョブ実行 |
| 評価期間 | 過去30日間(夜間停止時間帯を除く) |
| Fast burn時のアクション | オンコール担当が即時対応。ALBのUnhealthyHostCount、EBSのスループット/IOPSクレジット、Runner ASGのインスタンス起動失敗を優先的に確認する |
| Mid/Slow burn時のアクション | 当日〜週次で原因調査。直近のGitLabバージョンアップやRunner ASG設定変更の有無を確認する |
| 責任者 | Platform/Infraチームのオンコール担当、週次定点観測会ではチームリードが判断 |
| 例外として認める変更 | セキュリティパッチ、Fast burnの原因そのものに対するホットフィックス |
| 通常運用へ戻す条件 | 直近24時間のBurn Rateが1を下回った状態が継続 |
| エスカレーション先 | Platform/Infraチームリード |
一次対応の内容は、これまでの計測漏れの確認で洗い出した「起こりがちな原因」に対応づけています。
1. ALBのUnhealthyHostCount、CPU/メモリ/ディスクの既存アラームと突合し、
インフラ起因かどうかを切り分ける
2. EBSのスループット・IOPSクレジット残量を確認する(バーストクレジット枯渇の疑い)
3. gitlab-rails/gitlab-shellのログで5xxやタイムアウトの偏りを確認する
4. gitlab-runnerのジョブログでsystem_failureやプロビジョニングエラーの
偏りを確認し、Runner ASG/fleeting-plugin-awsの起動失敗を切り分ける
5. 原因が特定でき次第、必要な緊急対応(インスタンスタイプ/EBSボリューム
タイプの見直し、Runner Managerの再起動など)を実施する
通知を有効化し、対応まで繋げる
計測漏れの確認で触れたとおり、現状はnotifications_enabled = falseで既存アラームの通知すら飛んでいません。
Burn Rateアラームを実装しても、Fast burnをオンコールのページング先(PagerDuty・Slack緊急チャンネルなど)に、Mid/Slow burnを通常のチケット管理ツールにそれぞれ届くようSNSトピックと通知先を分けて設定しない限り、アンチパターン9「作っただけで運用しない」を繰り返すだけになります。
実装の最後には、実際に発報させて通知が届くところまで確認します。
コラム:AIにSLI/SLOとエラーバジェットの叩き台作成を補助してもらう
ここまでGitLabという1サービスを題材に、CUJの洗い出しからSLI Implementation、Burn Rateの設計まで一つずつ追ってきました。
しかし、Platform SREが実際に見るサービスは、内製アプリケーションからGitLabのようなOSSまで多岐にわたります。
概念編で述べたとおりSLI/SLOはサービスの性質に応じた個別最適が必要であり、テンプレートを機械的に当てはめるだけでは済みません。
一方で、対象サービスが増えるほど、SREがすべてのサービスのコードとインフラ構成を読み込み、この記事でやったような作業に一つひとつ伴走するのは現実的ではありません。
運用編で述べた「SLOのオーナーをプロダクトチームに置く」を実現するには、プロダクトチーム自身が最初の叩き台を作れる「入口」が必要です。
そこで有効なのが、AIにアプリケーションコードとインフラコード(Terraform、Kubernetesマニフェストなど)を読み込ませ、CUJ・SLI Specification/Implementation・SLO・エラーバジェット/Burn Rateの候補と、既存インフラで実現できる具体的な計測方法まで提案させる方法です。
実際、この記事のCritical User Journeyの選定やSLI Implementationの候補も、terraform-aws・terraform-gitlab・ansibleのコードをAIに読み込ませて得た叩き台を、人間が検証・調整したものです。この用途向けに、次のような方針でAIスキル(Claude Codeのスキルとして定義)を用意しています。
- ルーティング定義や非同期処理の定義からCUJ候補を洗い出す
- 同じ機能に複数の経路がないか(今回のgit push/pullにおけるHTTPS/SSHのように)を必ず確認させる
- コード上のファイル・関数・エンドポイントを明記させ、成功/失敗条件とユーザー起因エラーの除外条件を実装から特定させる
- IaCからスケジュール処理(今回の夜間停止のような計画停止)を検出させ、SLOの測定期間から除外する条件として明記させる
- 目標値・エラーバジェット・Burn Rateの数値は断定ではなく「叩き台」として出力させ、最終的な合意はチームで行うことを明記させる
重要なのは、AIの出力を最終的なSLI/SLOとしてそのまま採用しないことです。あくまで叩き台であり、計測漏れの確認や、実際のトラフィック量に応じた目標値の調整は、引き続き人間(プロダクトチームとSRE)が行います。
この仕組みにより、SREが個別にすべてのサービスへ深く入り込まなくても、プロダクトチームが自分たちで最初の一歩を踏み出せる、品質が担保された入口を用意できます。SREの役割は、叩き台を代わりに作ることではなく、こうした入口を整備し、必要に応じてレビューする側に回ることです。
まとめ
概念編で紹介した型を、CPU・メモリ・ディスクといったインフラ指標のアラームしか無かった実際のGitLab環境に当てはめてみると、次のようなことが具体的に見えてきました。
- SSH経由のgit操作はALBを経由しないため、既存のALB中心の監視では捉えられない
- 夜間の計画停止スケジュールを考慮しないと、SLOの数値が実態とズレる
- CI/CDでは「ジョブが成功したか」だけでなく「そもそも実行され始めたか」を分けて見る必要がある
- SLI/SLOを計算できるようにするには、まずアプリケーションログをCloudWatch Logsへ送る準備自体が必要
- Burn Rateアラームも、夜間停止時間帯やサンプル数が少ない時間帯を考慮しないと誤検知・見逃しにつながる
- エラーバジェットポリシーは、社内向け基盤ではSREとプロダクトチームの役割が分離しないなど、環境に応じて運用編の型を調整する必要がある
- この一連の作業はAIによる叩き台作成で加速でき、SREが全サービスに個別に入り込まなくても、プロダクトチームが自分たちで最初の一歩を踏み出せる入口になる
「教科書どおりの型」をそのまま当てはめるだけでは終わらず、実際の構成を一つずつたどることで初めて、自分たちの環境特有の計測漏れが見つかります。
これは概念編で述べた「型は守るが、何を守るかは自分たちのサービスから考える」という考え方そのものです。
次のステップとしては、CloudWatch Agentによるログ収集の追加、Logs Insightsクエリとダッシュボードの整備、Burn Rateを検知する複合アラームの実装、そしてアラーム通知の有効化を行い、まずは数週間分の実績値を集めるところから始めることになります。
Discussion