0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ウォーターフォール開発でもDORAの4keysを計測したい — GitHub Actions × SFTP × 自社ポータルで自作した話

0
Posted at

はじめに:なぜウォーターフォールの現場で 4keys なのか

私が所属するチームの開発プロセスは ウォーターフォール です。

アジャイルでもスクラムでもない。設計書は Excel、リリースは計画ベース、自動テストの整備もまだ道半ば。そんな現場で「DORA の 4keys を計測しよう」と言い出したら、おそらく多くの人が「うちには関係ない」と思うでしょう。

でも、私がこの仕組みを作ろうと思ったのは、まさにその 「うちには関係ない」側の現場 にいたからです。

感じていた課題

  • リリース頻度が低い。大きなリリースに機能を詰め込み、一気に出すスタイル
  • 小バグが多い。リリース後に細かい不具合が見つかり、修正リリースが走る
  • 品質が良いとは言い切れない。しかし「なんとなく」の肌感覚でしかなく、数値で語れない
  • 自動テストの整備が追いついていない。手動テストへの依存が大きい

この「なんとなく品質が悪い気がする」「リリースのたびに不安がある」という状況を、どうにか 数字で見える化 できないか。そう考えていたときに出会ったのが VSM(Value Stream Mapping)DORA の Four Keys でした。

VSM と DORA に感銘を受けた

VSM は、開発プロセスの中で「価値が生まれている時間」と「待ち時間」を可視化する手法です。DORA の研究(『Accelerate』で有名)は、ソフトウェアデリバリーのパフォーマンスが組織パフォーマンスと強く相関することを統計的に示しています。

印象的だったのは、DORA の 4keys が 開発手法を問わない 指標だという点です。アジャイルだから高い、ウォーターフォールだから低い、ではなく、「今の自分たちがどこにいるのか」を客観的に測る物差しとして使える。ウォーターフォールであっても、デプロイ頻度やリードタイムを計測することで「改善の方向性」が見えてきます。

「まず測ろう。測れば語れる。語れれば変えられる」——そう思いました。

SaaS はあった。でも全員には届かなかった

実は、4keys を可視化できる SaaS ツールは会社として導入されていました。ただし問題が2つ。

  1. 利用には役職者の承認と説得が必要 で、気軽に「ちょっと試してみよう」ができない
  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 パイプラインが揃ってから計測するのではなく、今の状態を測ることが第一歩 です。

自分たちが今どこにいるのかを知ること。そこからしか、改善は始まらないと思います。

本記事のコード例は実装のエッセンスを示すために一般化したサンプルです。実際の環境に合わせて調整してください。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?