はじめに:なぜウォーターフォールの現場で 4keys なのか
私が所属するチームの開発プロセスは ウォーターフォール です。
アジャイルでもスクラムでもない。設計書は Excel、リリースは計画ベース、自動テストの整備もまだ道半ば。そんな現場で「DORA の 4keys を計測しよう」と言い出したら、おそらく多くの人が「うちには関係ない」と思うでしょう。
でも、私がこの仕組みを作ろうと思ったのは、まさにその 「うちには関係ない」側の現場 にいたからです。
感じていた課題
- リリース頻度が低い。大きなリリースに機能を詰め込み、一気に出すスタイル
- 小バグが多い。リリース後に細かい不具合が見つかり、修正リリースが走る
- 品質が良いとは言い切れない。しかし「なんとなく」の肌感覚でしかなく、数値で語れない
- 自動テストの整備が追いついていない。手動テストへの依存が大きい
この「なんとなく品質が悪い気がする」「リリースのたびに不安がある」という状況を、どうにか 数字で見える化 できないか。そう考えていたときに出会ったのが VSM(Value Stream Mapping) と DORA の Four Keys でした。
VSM と DORA に感銘を受けた
VSM は、開発プロセスの中で「価値が生まれている時間」と「待ち時間」を可視化する手法です。DORA の研究(『Accelerate』で有名)は、ソフトウェアデリバリーのパフォーマンスが組織パフォーマンスと強く相関することを統計的に示しています。
印象的だったのは、DORA の 4keys が 開発手法を問わない 指標だという点です。アジャイルだから高い、ウォーターフォールだから低い、ではなく、「今の自分たちがどこにいるのか」を客観的に測る物差しとして使える。ウォーターフォールであっても、デプロイ頻度やリードタイムを計測することで「改善の方向性」が見えてきます。
「まず測ろう。測れば語れる。語れれば変えられる」——そう思いました。
SaaS はあった。でも全員には届かなかった
実は、4keys を可視化できる SaaS ツールは会社として導入されていました。ただし問題が2つ。
- 利用には役職者の承認と説得が必要 で、気軽に「ちょっと試してみよう」ができない
- チーム全員がアクセスできるわけではない。見られる人が限られている
メトリクスの本当の価値は、チーム全員が日常的に目にする ことで生まれます。一部の人だけが見ているダッシュボードでは「あの人が見てるアレ」で終わってしまう。
だったら、チーム全員がアクセスできる自社ポータルに組み込んで、「こういうものだよ」とまず触れてもらおう。既存の SaaS を否定したいのではなく、チームへの浸透のための最初の一歩として自作を選びました。
全体アーキテクチャ
┌──────────────────┐
│ GitHub Actions │
│ │
│ on: push (main) │
│ + 手動実行対応 │
│ │
│ ① メトリクス収集 │
│ ② JSON生成 │
│ ③ SFTP転送 │
└────────┬─────────┘
│ SFTP
▼
┌──────────────────┐
│ 所定サーバ │
│ /data/4keys/ │
└────────┬─────────┘
│ ファイル監視
▼
┌──────────────────────────────┐
│ 開発ポータルサイト │
│ ┌──────────┐ ┌────────────┐ │
│ │ Backend │ │ Frontend │ │
│ │ (Java) │ │ (Vue.js) │ │
│ │ │ │ │ │
│ │ JSON取込 │ │ ダッシュ │ │
│ │ DB更新 │ │ ボード表示 │ │
│ └────┬─────┘ └────────────┘ │
│ │ │
│ ┌────▼──────┐ │
│ │PostgreSQL │ │
│ └───────────┘ │
└──────────────────────────────┘
設計上のポイントは、Actions とポータルを SFTP + ファイル監視で疎結合 にしていることです。Actions 側は「JSON を所定のディレクトリに置くだけ」、ポータル側は「ファイルが来たら取り込むだけ」。お互いの内部実装を知らなくてよいので、片方の変更が他方に影響しません。
ポータルサイト自体は元々チーム内で使っていた開発支援ツールで、ステージング用パイプラインの実行やテストツールなどが集約されています。ここに 4keys ダッシュボードを追加する形で実装しました。
計測対象の 3 指標
DORA の 4keys のうち、今回は GitHub のイベントから取得可能な 3 指標 を計測しています。
1. デプロイ頻度(Deployment Frequency)
main ブランチへのマージ回数を期間ごとに集計します。
main への push をトリガーに GitHub Actions が走り、1 マージ = 1 レコードが生成されるため、DB に蓄積されたレコード数を日次・週次・月次で集計するだけで算出できます。
ウォーターフォールの場合、この数字は「低い」ことが多いです。でもそれが可視化されること自体に意味があります。「月に 2 回しかマージしていない」という事実が数字で見えると、「もう少し細かく分割してマージできないか」という議論のきっかけになります。
2. 変更リードタイム(Change Lead Time)
PR の最初のコミットから main マージまでの経過時間を計測します。
GitHub API で PR に紐づくコミット一覧を取得し、最古コミットの authored_date とマージ時刻の差分を秒単位で算出します。
ウォーターフォールでは設計・実装・テストが直列に進むため、リードタイムは長くなりがちです。ただ、「PR が何日間レビュー待ちで滞留していたか」が見えるだけでも改善余地に気づけます。
3. 変更障害率(Change Failure Rate)
一定期間のデプロイのうち、障害を引き起こしたデプロイの割合です。
自動判定は hotfix/ ブランチからのマージや incident ラベル付き PR で行い、それで拾いきれないケースはポータル上の手動フラグで補完します。完全自動化より「まず計測を始める」ことを優先しました。
MTTR(平均復旧時間) は障害検知〜復旧の時間であり、GitHub のイベントだけでは完結しないため今回のスコープ外です。アラート管理ツールとの連携による将来対応を想定しています。
GitHub Actions ワークフロー
ワークフロー定義
name: 4keys Metrics Collection
on:
push:
branches: [main]
workflow_dispatch: # 手動実行にも対応
jobs:
collect-metrics:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 全履歴取得(リードタイム計算に必要)
- name: Collect merge metrics
id: metrics
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# 直近マージされたPR情報を取得
PR_NUMBER=$(gh pr list --state merged --base main --limit 1 \
--json number --jq '.[0].number')
if [ -z "$PR_NUMBER" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# PR詳細
PR_DATA=$(gh pr view "$PR_NUMBER" --json \
mergedAt,createdAt,commits,labels,headRefName)
MERGED_AT=$(echo "$PR_DATA" | jq -r '.mergedAt')
HEAD_BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName')
# 最初のコミット日時(リードタイム計算用)
FIRST_COMMIT=$(echo "$PR_DATA" | jq -r \
'[.commits[].authoredDate] | sort | first')
# リードタイム算出(秒)
MERGED_EPOCH=$(date -d "$MERGED_AT" +%s)
FIRST_COMMIT_EPOCH=$(date -d "$FIRST_COMMIT" +%s)
LEAD_TIME_SEC=$((MERGED_EPOCH - FIRST_COMMIT_EPOCH))
# 障害デプロイの判定
IS_FAILURE="false"
if echo "$HEAD_BRANCH" | grep -qE "^hotfix/"; then
IS_FAILURE="true"
fi
if echo "$PR_DATA" | jq -e '.labels[] | select(.name == "incident")' > /dev/null 2>&1; then
IS_FAILURE="true"
fi
# JSON 生成
TIMESTAMP=$(date -u +%Y%m%d%H%M%S)
FILENAME="4keys_${TIMESTAMP}.json"
jq -n \
--arg repo "${{ github.repository }}" \
--argjson pr "$PR_NUMBER" \
--arg merged "$MERGED_AT" \
--arg first_commit "$FIRST_COMMIT" \
--argjson lead_time "$LEAD_TIME_SEC" \
--argjson is_failure "$IS_FAILURE" \
--arg branch "$HEAD_BRANCH" \
--arg collected "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{
repository: $repo,
pr_number: $pr,
merged_at: $merged,
first_commit_at: $first_commit,
lead_time_seconds: $lead_time,
is_failure: $is_failure,
branch: $branch,
collected_at: $collected
}' > "$FILENAME"
echo "filename=$FILENAME" >> "$GITHUB_OUTPUT"
- name: Transfer via SFTP
if: steps.metrics.outputs.skip != 'true'
env:
SFTP_HOST: ${{ secrets.SFTP_HOST }}
SFTP_USER: ${{ secrets.SFTP_USER }}
SFTP_KEY: ${{ secrets.SFTP_KEY }}
SFTP_DIR: ${{ secrets.SFTP_DIR }}
run: |
echo "$SFTP_KEY" > /tmp/sftp_key
chmod 600 /tmp/sftp_key
sftp -i /tmp/sftp_key \
-o StrictHostKeyChecking=no \
"${SFTP_USER}@${SFTP_HOST}" <<EOF
cd ${SFTP_DIR}
put ${{ steps.metrics.outputs.filename }}
bye
EOF
rm -f /tmp/sftp_key
Secrets 管理
| Secret 名 | 内容 |
|---|---|
SFTP_HOST |
転送先サーバのホスト名 |
SFTP_USER |
SFTP 接続ユーザー |
SFTP_KEY |
SSH 秘密鍵(PEM 形式) |
SFTP_DIR |
JSON 配置先ディレクトリパス |
秘密鍵を GitHub Secrets に登録する際は、改行を含む PEM 形式をそのまま貼り付けてください。Base64 エンコードは不要です。また、GHAS(GitHub Advanced Security)の Push Protection を有効にしておくと、うっかりシークレットをコードに書いてしまった際にプッシュ時点でブロックしてくれます。
ポータルサイド:ファイル監視 → DB 取り込み → 表示
ファイル監視と DB 取り込み(バックエンド)
ポータルの Java バックエンドで所定ディレクトリをポーリングし、新しい JSON を検知したら PostgreSQL に取り込みます。
@ApplicationScoped
public class FourKeysFileWatcher {
private static final Logger LOG = Logger.getLogger(FourKeysFileWatcher.class.getName());
@Inject
private FourKeysRepository repository;
private final Path watchDir;
private final Path processedDir;
@Inject
public FourKeysFileWatcher(
@ConfigProperty(name = "fourkeys.watch.dir") String dir,
@ConfigProperty(name = "fourkeys.processed.dir") String processed) {
this.watchDir = Path.of(dir);
this.processedDir = Path.of(processed);
}
// 定期ポーリング(例: 30秒間隔)
@Schedule(second = "*/30", minute = "*", hour = "*", persistent = false)
public void poll() {
try (var files = Files.list(watchDir)) {
files.filter(p -> p.toString().endsWith(".json"))
.sorted()
.forEach(this::processFile);
} catch (IOException e) {
LOG.log(Level.WARNING, "ファイル監視中にエラー", e);
}
}
private void processFile(Path file) {
try {
String content = Files.readString(file);
FourKeysRecord record = parseJson(content);
// 重複チェック(同じPR番号+リポジトリ)
if (!repository.exists(record.repository(), record.prNumber())) {
repository.insert(record);
LOG.info("取り込み完了: " + file.getFileName());
}
// 処理済みディレクトリへ移動
Files.move(file, processedDir.resolve(file.getFileName()),
StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
LOG.log(Level.SEVERE, "ファイル処理失敗: " + file, e);
}
}
}
処理済みファイルは別ディレクトリへ移動する方式にしています。削除ではなく移動にすることで、問題が起きたときに元データを確認できます。
テーブル設計
CREATE TABLE four_keys_metrics (
id BIGSERIAL PRIMARY KEY,
repository VARCHAR(255) NOT NULL,
pr_number INTEGER NOT NULL,
merged_at TIMESTAMPTZ NOT NULL,
first_commit TIMESTAMPTZ NOT NULL,
lead_time_sec INTEGER NOT NULL,
is_failure BOOLEAN NOT NULL DEFAULT FALSE,
branch VARCHAR(255),
collected_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (repository, pr_number)
);
-- 集計用インデックス
CREATE INDEX idx_metrics_merged_at ON four_keys_metrics (merged_at);
CREATE INDEX idx_metrics_repo ON four_keys_metrics (repository);
ダッシュボード(フロントエンド)
Vue.js のフロントエンドで API から集計データを取得し、ダッシュボードとして表示します。
┌─────────────────────────────────────────────────┐
│ 📊 4keys ダッシュボード │
├─────────────┬───────────────┬───────────────────┤
│ デプロイ頻度 │ 変更リードタイム │ 変更障害率 │
│ 3回/週 │ 4.2日 │ 8.3% │
├─────────────┴───────────────┴───────────────────┤
│ │
│ 📈 デプロイ頻度の推移(週次) │
│ ┃ │
│ ┃ ■ │
│ ┃ ■ ■ ■ ■ │
│ ┃ ■ ■ ■ ■ ■ ■ ■ ■ │
│ ┗━━━━━━━━━━━━━━━━━━━━━━ │
│ W1 W2 W3 W4 W5 W6 W7 W8 │
│ │
│ 📈 リードタイム分布 │
│ │
│ 📈 障害率の推移 │
│ │
└─────────────────────────────────────────────────┘
表示する内容はシンプルに徹しました。最初から凝ったグラフを作り込むよりも、「数字が見える」という体験をまずチームに届ける ことを優先しています。
ウォーターフォールの現場で可視化して見えてきたこと
まだ運用を始めたばかりですが、数字が見えること自体が変化を生み始めています。
「遅い」が具体的になる
「リリースが遅い気がする」から「PR のリードタイムが平均 X 日」に変わると、「じゃあ何が X 日を食っているのか」という次の問いが自然に生まれます。レビュー待ちなのか、テスト待ちなのか、承認プロセスなのか。感覚を数字に変換するだけで、議論の質が変わります。
「品質が悪い」を分解できる
「小バグが多い」という肌感覚も、変更障害率として数字になると「直近 3 ヶ月で X% のマージが hotfix を伴っている」と言い換えられます。それが許容範囲なのかどうかの判断基準を持てるようになります。
SaaS 導入への橋渡し
自作ダッシュボードで「こういうメトリクスが見えると便利ですよね」をチームが体験すると、本格的な SaaS ツールの導入提案に対する心理的ハードルが下がります。「よくわからないツールを入れる」ではなく「あのダッシュボードのもっとすごい版」として説明できるようになります。
まとめ
| やったこと | 内容 |
|---|---|
| データ収集 | GitHub Actions で main マージ時に 4keys メトリクスを JSON 出力 |
| データ転送 | SFTP で所定サーバへ転送(疎結合) |
| データ取り込み | Java バックエンドでファイル監視 → PostgreSQL へ INSERT |
| 可視化 | Vue.js ダッシュボードで週次・月次の推移を表示 |
技術的には特に難しいことはしていません。GitHub CLI でデータを集めて、JSON にして、SFTP で送って、DB に入れて、画面に出す。それだけです。
大事だったのは技術ではなく 「まず計測を始める」という判断 でした。
ウォーターフォールだから 4keys は関係ない、ということはありません。むしろ、改善の余地が大きい現場ほど、可視化のインパクトは大きいと感じています。完璧な自動テストや CI/CD パイプラインが揃ってから計測するのではなく、今の状態を測ることが第一歩 です。
自分たちが今どこにいるのかを知ること。そこからしか、改善は始まらないと思います。
本記事のコード例は実装のエッセンスを示すために一般化したサンプルです。実際の環境に合わせて調整してください。