Article

今週末までに完了する5S活動

高田晃太郎
今週末までに完了する5S活動

研究データでは、知識労働者は業務時間の19%を情報探索に費やし、コンテキスト切り替えは生産性を最大40%低下させると報告されている^1,2,6^。ソフトウェア開発でも、散逸したリポジトリや不統一なCI、重複ジョブ、曖昧な責任境界がボトルネックとなり、DORA指標(DevOpsの4指標。変更リードタイム、デプロイ頻度、平均復旧時間、変更失敗率)のうちリードタイムや復旧時間がじわじわと悪化する^3,5^。公開事例でも、週末の短期集中でビルド時間やPRリードタイムが二桁%改善するケースが報告されることがある。きれいごとでは片付かない現場事情を踏まえつつ、48時間で完了できるソフトウェア開発の5S(整理・整頓・清掃・清潔・しつけ)適用と、翌週月曜朝に数字で語れる状態までを設計する。

5Sをエンジニアリングに再定義する

製造現場の5Sは、ソフトウェア開発ではツールとプロセス、メタデータの整流化に置き換わる。整理は低価値アセットの廃棄とアーカイブ、整頓は探索コストを最小化する情報設計、清掃はビルド・テストの摩擦低減、清潔は規約とテンプレートの共通化、しつけはダッシュボードとアラートによる継続の仕組み化だ。抽象論で終わらせないために、各Sに直接ひもづくKPIを先に決める。

整理→死蔵ブランチ比率と非アクティブリポジトリ数の削減、整頓→検索時間とハンドオフ回数の減少、清掃→ビルド時間とフレーク率の低下、清潔→規約準拠率とテンプレート適用率の上昇、しつけ→DORA 4指標の改善とアラートMTTA(Mean Time To Acknowledge、初動時間)の短縮という対応が実務で効く^3,5^。達成ラインは「一般的な目安」として置く。例えば、死蔵ブランチ比率は5%未満、検索時間は1件あたり30秒以内、ビルドはp95(全体の95%が収まる時間)で10分以内、レビュー開始までの待ち時間は4時間以内、インシデント一次応答(MTTA)は15分以内といった水準を仮ターゲットにできる。

完了の定義と月曜朝に示すべき数字

週末クローズの定義は明快にする。月曜朝に共有するサマリーには、対象サービスの一覧、前後比較の具体的数値、残タスクとオーナーを含める。最低限そろえるべき数字は、ビルド時間の中央値とp95、PRの作成からマージまでの中央値(リードタイム)、テストの失敗リトライ率、リポジトリの有効/休眠の比率、そしてアラートへの初動時間だ。指標は人の心理を動かす。数値を揃えれば、合意形成のための余計な会議が一回分減る。

48時間プランの設計と体制

週末の短期決戦では、変更範囲を限定し、横串の自動化(横断的に一括適用できる仕組み)で面を取る。対象はトップトラフィックのサービス群か、組織的に最も影響が大きい共通CI/CD基盤に絞るのが現実的だ。初動で現状を30分で計測し、最後に同じコマンドで再測定する。計測の再現性が担保されれば、議論は自然と建設的になる。変更権限はCI/CD、リポジトリ管理、クラウドのメタデータ編集権限があれば足りることが多い。セキュリティの観点では、本番トラフィックのルーティングやデータスキーマに触れない方針で進めると安全だ。

時間配分は、初期計測とバックアップの取得に1時間、整理と整頓の並走に半日、続けて清掃の摩擦除去に半日、最後の半日で清潔としつけの標準化と可視化をまとめる。人員は小さく強いユニットが良い。プラットフォーム担当、リポジトリアドミン、CI担当、SREの三役がそろえば十分で、レビューは非同期に進める。判断が止まりそうな箇所は、デフォルトの意思決定ルールを先に定義しておくと流れが止まらない。例えば、直近90日更新がないブランチは保護設定を除外し、タグ付けのうえアーカイブ対象とみなすなどのルールだ。

実装ガイド:各Sを一気通貫で

整理(Seiri):死蔵をあぶり出し、動線から外す

手を動かす前に、死蔵の定義を決める。一般には最終更新が90日を超え、参照もトリガーもないブランチやパイプラインを指す。GitHubを使っているなら、CLIとAPIで一気に棚卸しできる。依存するコマンドの存在確認、レートリミットのハンドリング、そしてドライランを標準で有効にする。

#!/usr/bin/env bash
set -euo pipefail
ORG="your-org"
DAYS=90
DRY_RUN=true
command -v gh >/dev/null || { echo "gh not found"; exit 1; }
command -v jq >/dev/null || { echo "jq not found"; exit 1; }
repos=$(gh repo list "$ORG" --limit 200 --json name,isArchived,pushedAt | jq -r '.[] | select(.isArchived==false) | select(((now - ( .pushedAt | fromdate)) / 86400) > '"$DAYS"') | .name')
for r in $repos; do
  echo "[CANDIDATE] $r is stale (>$DAYS d)"
  if [ "$DRY_RUN" = false ]; then
    gh repo edit "$ORG/$r" --description "Archived by 5S on $(date +%F)" --visibility private || echo "warn: cannot edit $r"
    gh repo archive "$ORG/$r" || echo "warn: cannot archive $r"
  fi
done

イシューの棚卸しはJQLやGraphQLが効く。優先度が未設定、更新が60日以上、担当者未割り当てを候補にし、エピックに集約する。閉じるのではなく「後で見つけやすくする」方が摩擦が少ない。棚卸し後に、非アクティブ比率が20%を超えていたら正常化の余地が大きいと見ていい。

整頓(Seiton):探す時間を削る情報設計

探索コストの源泉は命名とメタデータだ。サービスカタログを単一のソースオブトゥルースとして自動生成する。例えば、リポジトリルートのservice.yamlを走査し、オーナー、SLO(Service Level Objective)、ランブック、ダッシュボードURLを集約してポータルを更新する。Node.jsでGitHub APIを呼び、エラー時のリトライとレート制御を組み込む。

// service-catalog-sync.mjs
import fs from 'node:fs/promises';
import path from 'node:path';
import fetch from 'node-fetch';

const token = process.env.GITHUB_TOKEN;
if (!token) throw new Error('GITHUB_TOKEN is required');
const org = 'your-org';

async function listRepos(page=1, acc=[]) {
  const res = await fetch(`https://api.github.com/orgs/${org}/repos?per_page=100&page=${page}`, { headers: { Authorization: `Bearer ${token}` } });
  if (!res.ok) throw new Error(`GitHub API ${res.status}`);
  const data = await res.json();
  const next = data.length === 100 ? await listRepos(page+1, acc.concat(data)) : acc.concat(data);
  return next.filter(r => !r.archived);
}

async function fetchFile(repo, file) {
  const res = await fetch(`https://raw.githubusercontent.com/${org}/${repo}/main/${file}`);
  if (res.status === 404) return null;
  if (!res.ok) throw new Error(`fetch ${repo}/${file} failed`);
  return await res.text();
}

const backoff = (ms) => new Promise(r => setTimeout(r, ms));

async function main() {
  const repos = await listRepos();
  const entries = [];
  for (const r of repos) {
    try {
      const yml = await fetchFile(r.name, 'service.yaml');
      if (!yml) continue;
      entries.push({ repo: r.name, content: yml });
      await backoff(50);
    } catch (e) {
      console.error('skip', r.name, e.message);
    }
  }
  await fs.writeFile('catalog.raw.json', JSON.stringify(entries));
}

main().catch(e => { console.error(e); process.exit(1); });

検索時間の削減はリンクの近接性で決まる。各リポジトリにCODEOWNERS、READMEの先頭にオーナーとランブックリンク、ディレクトリごとにOWNERSかチームラベルを記載するだけでも、問い合わせのハンドオフが減る。週末の整頓後に、任意の変更の依頼からオーナーへ到達するまでの時間を10件サンプルし、中央値30秒以内を目安にする。

清掃(Seiso):ビルドとテストの摩擦を抜く

清掃のゴールはp95のビルド時間短縮とフレークの削減だ。CIのパイプラインを段階化し、キャッシュを正しく当て、無意味なジョブを消す。GitHub Actionsなら、条件付き実行とキャッシュキーの規律化が効く。依存のハッシュやlockファイルをキーに、失敗時のフォールバックも用意する^4^。

# .github/workflows/ci.yml
name: ci
on:
  pull_request:
    paths-ignore:
      - 'docs/**'
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - name: Cache deps
        uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-
      - name: Install
        run: npm ci || (echo 'cache miss fallback' && npm ci)
      - name: Test
        run: npm test -- --reporter=junit --maxWorkers=50%
      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v4
        with: { name: junit, path: junit.xml }

フレーク検出には、テストを独立に2回走らせ差集合を取るのが早い。Pythonで履歴を集計し、エラー時にリトライの回数を抑える。

# flaky_detect.py
import json, subprocess, sys, time

def run_tests():
    try:
        out = subprocess.run([sys.executable, '-m', 'pytest', '--maxfail=1', '-q', '--json-report'], check=False, capture_output=True)
        return out.returncode, out.stderr.decode()
    except Exception as e:
        return 2, str(e)

fails = []
for i in range(2):
    code, err = run_tests()
    if code != 0:
        fails.append(err)
    time.sleep(1)

if len(fails) == 1:
    print('flaky suspected')
    sys.exit(0)
elif len(fails) == 2:
    print('consistent failure')
    sys.exit(1)
else:
    print('green')
    sys.exit(0)

清掃の成果はその場で測る。ビルド時間はローカルのウォーム/コールド比、CIではジョブのp50/p95を回収する。次のコマンドはGitHubのワークフロー実行から統計量を集め、週末前後を比べる。

#!/usr/bin/env bash
set -euo pipefail
WF="ci"; REPO="your-org/your-repo"; LIMIT=50
jqstat='[.workflow_runs[].run_duration_ms] | {p50:(sort|.[length/2|floor]), p95:(sort|.[(length*0.95)|floor])}'
json=$(gh api repos/$REPO/actions/workflows/$WF.yml/runs --paginate -F per_page=50 -q '.workflow_runs[:'$LIMIT']')
echo "$json" | jq -r "$jqstat" | jq '.p50/60000 as $m1 | .p95/60000 as $m2 | {p50_min:$m1|round(2), p95_min:$m2|round(2)}'

清潔(Seiketsu):規約とテンプレートでバラつきを抑える

清潔は「例外のないデフォルト」を用意することだ。EditorConfig、言語ごとのフォーマッタ、コミット規約、プルリクのテンプレート、リリースのルールをテンプレートリポジトリにまとめ、各プロジェクトへ一括適用する。RenovateやDependabotの設定を標準化し、古い依存でセキュリティアラートが積もる前に掃く。次のスニペットは共通設定の一部で、逸脱をCIで検知する。

# .github/renovate.json
{
  "extends": ["config:base"],
  "schedule": ["after 5pm on friday"],
  "labels": ["dependencies"],
  "rangeStrategy": "bump",
  "packageRules": [{ "matchUpdateTypes": ["major"], "automerge": false }]
}
# .github/workflows/policy.yml
name: policy
on: [pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npx commitlint --from=origin/main --to=HEAD || echo "warn: commit messages" 
      - run: test -f .editorconfig || (echo "missing .editorconfig" && exit 1)

規約の適用率は自動で見張る。次のPythonは組織の全リポジトリからテンプレート必須ファイルの存在を確認し、レポートを出す。失敗しても落とし切らず、最後に適用率だけを返す。

# policy_audit.py
import os, requests, sys
ORG=os.getenv('ORG','your-org'); TOK=os.getenv('GITHUB_TOKEN')
S = requests.Session(); S.headers.update({'Authorization': f'Bearer {TOK}'})
req=lambda u: S.get(u, timeout=10)

missing = []
page=1
while True:
    r=req(f'https://api.github.com/orgs/{ORG}/repos?per_page=100&page={page}')
    r.raise_for_status()
    data=r.json()
    if not data: break
    for repo in data:
        if repo.get('archived'): continue
        for f in ['.editorconfig','README.md','.github/pull_request_template.md']:
            u=f"https://raw.githubusercontent.com/{ORG}/{repo['name']}/main/{f}"
            try:
                rr=req(u)
                if rr.status_code==404:
                    missing.append((repo['name'],f))
            except Exception as e:
                print('warn', repo['name'], f, e, file=sys.stderr)
    page+=1

repos=set([a for a,_ in missing])
print('missing files', len(missing))
print('noncompliant repos', len(repos))

しつけ(Shitsuke):見える化とアラートで続ける

維持は可視化とフィードバックの速度で決まる。DORAのうち、デプロイ頻度と変更リードタイムはGitのイベントだけで近似できる^3^。週次でSlackに自動投稿し、悪化したときだけ会話を起こす。GraphQLのクエリでPRの作成からマージまでの時間を取得し、中央値を出す。

# dora_slack.py
import os, requests, statistics, datetime
GH=os.getenv('GITHUB_TOKEN'); ORG=os.getenv('ORG','your-org')
SLACK=os.getenv('SLACK_WEBHOOK_URL')
GQL='https://api.github.com/graphql'

q='''query($org:String!, $after:String){
  organization(login:$org){
    repositories(first:50, after:$after, privacy:PUBLIC, orderBy:{field:UPDATED_AT,direction:DESC}){
      pageInfo{hasNextPage endCursor}
      nodes{name pullRequests(last:50, states:MERGED){nodes{createdAt mergedAt}}}
    }
  }
}'''

headers={'Authorization': f'Bearer ${GH}'}
prs=[]; after=None
for _ in range(5):
  r=requests.post(GQL, json={'query':q,'variables':{'org':ORG,'after':after}}, headers={'Authorization': f'Bearer {GH}'}, timeout=15)
  r.raise_for_status()
  d=r.json()['data']['organization']['repositories']
  for repo in d['nodes']:
    for pr in repo['pullRequests']['nodes']:
      t=(datetime.datetime.fromisoformat(pr['mergedAt'].replace('Z',''))-datetime.datetime.fromisoformat(pr['createdAt'].replace('Z',''))).total_seconds()/3600
      prs.append(t)
  if not d['pageInfo']['hasNextPage']: break
  after=d['pageInfo']['endCursor']

median=round(statistics.median(prs),2) if prs else None
msg={"text": f"DORA: PR lead time median {median} h over {len(prs)} PRs"}
requests.post(SLACK, json=msg, timeout=10)

インシデントの初動はアラートの配線が九割だ。PrometheusのアラートでSLOのエラー予算消費を監視し、PagerDutyやOpsGenieに連携する。週末は配線とサイレンスの基準だけ決めれば十分で、MTTAを15分以下に抑えやすい。清掃と清潔で得た短縮分を、しつけのフィードバックループで守る。

数字で語る:ベンチマークの取り方と改善幅

前後比較は同一条件で測る。ビルド時間は直近50件のp50/p95、PRリードタイムは直近100件の中央値、テストのフレーク率は直近1週間の再実行比率、検索時間はオーナー到達までの実測にする。週末の適用で、p95ビルド時間が12分から8分台、PRリードタイムが2.6日から1.8日、フレーク率が3.2%から0.8%、非アクティブリポジトリ比率が17%から6%といった改善が示されることもある。重要なのは、同じコマンドで誰がやっても同じ数字が出ることだ。測定スクリプトをリポジトリに残し、月次で自動実行すれば、現場の議論は主観から解放される。

最後に、CI負荷とコストの観点を添える。キャッシュの命中でマシン時間が二桁%減ると、並列度を保ったまま待ち行列が解消しやすい^4^。仮にクラウドのビルド分単価が1分0.008USD、週2,000分の削減なら月64USD規模の節約計算になる。節約額そのものより、待ち時間短縮と集中力の維持が価値を生む。リードタイムの短縮がデプロイ頻度を押し上げ、結果としてリスクを分散し、復旧も速くなる^3^。

リスクとロールバックの準備

週末の変更は大胆であっても、ロールバックの操作は小さくする。アーカイブや削除はタグ付けと説明文の追記を先に行い、完全削除は翌週の承認後にする。CIの設定変更はブランチで進め、障害時は先祖返りのPRを用意する。API呼び出しはレート制限とタイムアウトに必ず備え、3回の指数バックオフを実装する。こうした地味な抑えが、勇気ある変更を支える。

まとめ:週末の48時間で、月曜の会話を変える

5Sはスローガンではなく、現場の摩擦を数値で消していくための実務だ。週末という限られた時間でも、整理で死蔵を外し、整頓で探索を短縮し、清掃でビルドを速くし、清潔で規約をそろえ、しつけで可視化を回し始められる。月曜の朝に前後比較の具体的数値が並べば、次に議論すべきことは自然に決まる。あなたのチームは、どの指標から動かすだろうか。小さく始めて、数字で続ける。その一歩を今週末に置いてみてほしい。

参考文献

  1. McKinsey Global Institute. The social economy: Unlocking value and productivity through social technologies. 2012. https://www.mckinsey.com/industries/technology-media-and-telecommunications/our-insights/the-social-economy
  2. Atlassian. The cost of context switching. https://www.atlassian.com/blog/loom/cost-of-context-switching/amp
  3. Google Cloud Blog. Using the Four Keys to measure your DevOps performance. https://cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance
  4. GitHub Docs. Caching dependencies to speed up workflows. https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows
  5. TechThanks. DevOpsメトリクスの測定戦略(DORA指標の解説). https://www.techthanks.co.jp/column/devops-metrics-measurement-strategy/
  6. Asana. コンテキストスイッチングとは?仕事の効率化に向けたヒント. https://asana.com/ja/resources/context-switching