Upgrade to Pro — share decks privately, control downloads, hide ads and more …

トランクベース開発の実現に向けた開発プロセスとCIパイプラインの継続的改善

aanrii
March 20, 2023

 トランクベース開発の実現に向けた開発プロセスとCIパイプラインの継続的改善

aanrii

March 20, 2023
Tweet

Transcript

  1. Four Keys GoogleのDevOps Research and Assesment (DORA) チームが提唱する ソフトウェア開発チームのパフォーマンスを示す 4

    つの指標 • デプロイの頻度 - 組織による正常な本番環境へのリリースの頻度 • 変更のリードタイム - commit から本番環境稼働までの所要時間 • 変更障害率 - デプロイが原因で本番環境で障害が発生する割合(%) • サービス復元時間 - 組織が本番環境での障害から回復するのにかかる時間
  2. Four Keys GoogleのDevOps Research and Assesment (DORA) チームが提唱する ソフトウェア開発チームのパフォーマンスを示す 4

    つの指標 • デプロイの頻度 - 組織による正常な本番環境へのリリースの頻度 • 変更のリードタイム - commit から本番環境稼働までの所要時間 ◦ 短ければ短いほど、開発チームのアウトプットの最大化につながる • 変更障害率 - デプロイが原因で本番環境で障害が発生する割合(%) • サービス復元時間 - 組織が本番環境での障害から回復するのにかかる時間
  3. 変更のリードタイム コーディング Pull Request作成 Pull Request レビュー / CIチェック Pull

    Request merge リリース作業 要件定義/設計 変更のリードタイム 課題定義
  4. 変更のリードタイム コーディング Pull Request作成 Pull Request レビュー / CIチェック Pull

    Request merge リリース作業 要件定義/設計 変更のリードタイム 課題定義 開発のサイクルタイム 開発のサイクルタイムの削減が 変更のリードタイムの削減につながる ※どこまでのフェーズをサイクルタイムに含めるかは定義 により異なる
  5. 一般的なブランチ戦略 (ex. GitHub Flow) main branch feature branch A merge

    merge merge feature branch D feature branch B feature branch C merge
  6. feature branchの課題: merge conflict main branch feature branch A file

    A file A’’ file A’ file A → file A’ の変更と file A → file A’’ の変更との間で conflictが発生する file A file A’’ file A’
  7. merge conflictの解消コストが大きくなる要因 • feature branchとmain branchとの差分 (≒ Pull Requestのサイズ) が大きいほどconflict箇所が増える

    • feature branchの生存期間が長くなるほど差分が大きくなる • conflict箇所が多ければ多いほど、解消にコストがかかる
  8. トランクベース開発の種類 • feature branchを使わない方法 (committing straight to the trunk) •

    feature branchを使う方法 (short-lived feature branches) ◦ 認証認可チームで採用している方法はこれ
  9. トランクベース開発 (feature branchを使わない方法) 󰢿 Developer A Developer Aがコードに変更を加えた時 feature branchは切らずに

    main branchに直接commit & pushする 各開発者はmain branchに対し 1日に数回のcommit & pushを行う main branch (trunk)
  10. トランクベース開発 (feature branchを使わない方法) 󰢿 Developer A 󰢴 Developer B 🧓

    Developer D 󰬋 Developer C ひとつの機能は複数回のcommitにより実現される つまり、main branchには “未完成なコード” が (一時的に) 混入することがある main branch (trunk)
  11. トランクベース開発 (feature branchを使わない方法) release 1.0 release 1.1 release 1.2 特定の機能が

    “未完成な状態” であっても 関係なくリリースされることがある main branch (trunk)
  12. バグの混入を未然に防ぐ • feature branchを使った開発なら… ◦ Pull Request を出し、コードレビューを行う ◦ Pull

    Requestに対してCIによるチェック (自動テスト) を行う しかし、feature branchがそもそも存在しないので Pull Request が出せない
  13. 同期的にコードレビューする • 変更をいち早くmain branchに取り込むために必要 ◦ レビュー着手が遅れれば遅れるほど main branchの変更に追従しにくくなり、 conflictが発生しやすくなる •

    具体的な方法 ◦ commitの準備ができた段階でレビューを依頼する ▪ レビュイーとレビュアーが同席のもとでレビューを行う ◦ ペアプログラミングする ▪ 複数人でコードを書きながら、内容について相互に合意を取っていく
  14. ローカルでテストする • 変更をcommitする前に、開発者自身で品質を保証する • 開発者自身が速やかに品質保証できる手段を用意する必要がある ◦ 自動テスト, Lint, etc ◦

    速やかに結果が得られるようにしておく ▪ ビルドやテストはなるべく早く終わらせる ◦ 全ての開発者が等しい結果を得られるようにしておく ▪ 開発環境によってはテストが失敗する、などという状況は好ましくない
  15. feature flags func oldFunc() { // すでに開発されたコード } func newFunc()

    { // 開発中のコード } func oldFunc() { // すでに開発されたコード } func main() { oldFunc() }
  16. feature flags func oldFunc() { // すでに開発されたコード } func newFunc()

    { // 開発中のコード } func main() { newFunc() } func oldFunc() { // すでに開発されたコード } func main() { oldFunc() } 開発中の newFunc() が 本番環境で実行されてしまう
  17. feature flags func oldFunc() { // すでに開発されたコード } func newFunc()

    { // 開発中のコード } func main() { if os.GetEnv(“ENABLE_NEW_FUNC”) == “true” { newFunc() } else { oldFunc() } } func oldFunc() { // すでに開発されたコード } func main() { oldFunc() } env: - name: ENABLE_NEW_FUNC value: false
  18. feature flags func oldFunc() { // すでに開発されたコード } func newFunc()

    { // 開発中のコード } func main() { if os.GetEnv(“ENABLE_NEW_FUNC”) == “true” { newFunc() } else { oldFunc() } } func oldFunc() { // すでに開発されたコード } func main() { oldFunc() } ENABLE_NEW_FUNC環境変数 (= feature flag) が “true” の場合のみ newFunc() を実行するようにする env: - name: ENABLE_NEW_FUNC value: false
  19. feature flags func oldFunc() { // すでに開発されたコード } func newFunc()

    { // 開発中のコード } func main() { if os.GetEnv(“ENABLE_NEW_FUNC”) == “true” { newFunc() } else { oldFunc() } } func oldFunc() { // すでに開発されたコード } func main() { oldFunc() } env: - name: ENABLE_NEW_FUNC value: false newFunc() が開発完了するまで ENABLE_NEW_FUNC環境変数は falseにしておく
  20. feature flags func oldFunc() { // すでに開発されたコード } func newFunc()

    { // 開発完了したコード } func main() { if os.GetEnv(“ENABLE_NEW_FUNC”) == “true” { newFunc() } else { oldFunc() } } func oldFunc() { // すでに開発されたコード } func main() { oldFunc() } env: - name: ENABLE_NEW_FUNC value: true newFunc() が開発完了したら ENABLE_NEW_FUNC環境変数を trueにすることで デプロイ不要でリリース できる
  21. feature flags func oldFunc() { // すでに開発されたコード } func main()

    { oldFunc() } func newFunc() { // 開発完了したコード } func main() { newFunc() } func oldFunc() { // すでに開発されたコード } func newFunc() { // 開発完了したコード } func main() { if os.GetEnv(“ENABLE_NEW_FUNC”) == “true” { newFunc() } else { oldFunc() } } newFunc() に問題ないことが 十分確認できたら feature flagを削除する
  22. feature flagsのデメリット • コードが複雑になる ◦ if文、デッドコード、etc • feature flags自体の管理コストがかかる ◦

    flagが増えれば増えるほど管理が煩雑になる ◦ 専用の管理ツールもある (ex. LaunchDarkly)
  23. feature flagsのデメリット • コードが複雑になる ◦ if文、デッドコード、etc • feature flags自体の管理コストがかかる ◦

    flagが増えれば増えるほど管理が煩雑になる ◦ 専用の管理ツールもある (ex. LaunchDarkly) → 不要になったflagやif文、デッドコードはなるべく速やかに消す
  24. トランクベース開発まとめ (feature branchを使わない方法) • メンバー全員がfeature branchを切らず main branchに直接commit & pushをすることで

    merge conflictのリスクを回避する • 同期的なコードレビュー、ローカルでの自動テストにより 開発のアジリティを維持しつつコードの品質を保証する • feature flagsを用いることでmain branchを常にproduction readyにする
  25. short-lived feature branch short-lived feature branch feature branch 同じ機能を複数の小規模な short-lived

    branchによって開発する 個々のbranchを数日以内にmergeすることで mergeのconflictリスクと解消コストを抑える main branch (trunk) main branch (trunk)
  26. short-lived feature branchのルール • 作成後数日以内にmergeし、削除しなければならない ◦ trunkbaseddevelopment.com では2日以内を推奨している ◦ merge時点で必ずしも機能の開発が完了している必要はない

    ▪ feature branchを使わないトランクベース開発 (committing straight to the trunk) と同じ • main branch (trunk) 以外にmergeしてはいけない • branchを切った開発者、およびペアプログラミングでの共同作業者以外がcommit してはいけない ◦ あくまでmain branchを開発の中心とするべきである
  27. feature branchを使わない方法 (committing straight to the trunk) との比較 • short-lived

    feature branchによりPull Requestが使える ◦ Pull Requestによるレビューが可能 ▪ レビューの非同期化による mergeの遅延リスクがある ◦ CIパイプラインによるmerge前のコードチェックが可能 ▪ レビュアーが結果を参照できる
  28. どちらを選択するか? • feature branchを使わないほうが高い開発速度を得られる ◦ Pull Requestを作成する手間が省ける ◦ feature branchが肥大化するリスクをより減らすことができる

    ◦ 同期レビューを強制しやすい • (short-lived) feature branchを使った方が trunk (main branch) をバグから保護しやすい ◦ Pull Request に対してCI checkをかけられる
  29. トランクベース開発の導入背景 • PFや全社のマイクロサービス化推進に伴い トランクベース開発の導入が検討された ◦ Two pizza rule ▪ マイクロサービスを運用するチームの人数は

    2つのピザを分け合える程度が妥当 • 6-10人ぐらい、という説も : https://jasoncrawford.org/two-pizza-teams ▪ これだけの開発者がひとつのコードベースを積極的に保守する場合 conflictのリスクが現実的に避けられなさそう • マイクロサービスアーキテクトグループで 人柱 PoCを実施することとなった ◦ チーム発足後初めてのサービス開発において適用されることとなった
  30. (同期 or 非同期) コードレビュー • 基本的には非同期なコードレビューを採用 ◦ Goのコードレビューを務められるレビュアーが 1人しかおらず 同期レビューのタイミングを確保することが困難

    • PR作成後なるべく24時間以内にレビュー対応するようにした ◦ GitHubのSlack連携機能でアサインされたレビュワーに通知を送るようにした • 一部のレビュー観点についてはCIパイプラインで自動化した ◦ 単体テストの成否、Lintによるコーディングルール違反の検知、 etc
  31. 測定指標: サイクルタイム • ここでは「feature branchにfirst commitされてからmain branchにmerge されるまでの期間」と定義 ◦ 変更をどれだけ早くmain

    branchに取り込めたかを示す指標として採用 ◦ gitにおいて、branchの作成日時はそもそも記録されていない ため branchの生存期間を直接計測することはできない
  32. Code Climate Velocity • エンジニア組織のパフォーマンス可視化ツール ◦ サイクルタイムの計測や、 PRレビューまでの待機時間を可視化 ◦ 現在はFindy

    Teamsも並行運用中 • 週次の保守運用定例でチェック ◦ 目標を達成できなかった場合、その原因を考える
  33. 成果: サイクルタイム比較 (2022/10〜2023/03) 期間中のPR総数 (件) 平均サイクルタイム (時間) 全社平均 - 68.8

    チームA 1763 27.7 チームB 2086 32.1 チームC 489 108.5 認証認可チーム 1787 11.5 全社およびPF事業本部で、規模感が似ている他チームと比較 ※チームによってサービスの性質や開発体制が異なるため、あくまで参考値
  34. feature branchの肥大化 • feature branchのサイズ (feature branchに含まれるコードの変更行数) が 多ければ多いほど、mergeまでにかかる時間が伸びる •

    認証認可チームでは 変更行が600行以上あるPull Requestは原則rejectしている ◦ 問答無用でCI checkを落とす ▪ https://github.com/CodelyTV/pr-size-labeler ◦ 閾値は過去の事例をみて判断した
  35. feature branchのサイズが大きくなる要因 • ひとつのfeature branchで全てを実装しようとしてしまう • 例: サービスにWebAPIを新規追加する ◦ ハンドラーの追加、ドメインモデルの追加、

    DBや外部APIとの通信などの実装を すべての一つのfeature branchの中で完結させる必要はない ◦ ハンドラーを後で実装するなり、 feature flagsを使うなりすれば 開発中の機能をクライアントから隠蔽することができる
  36. feature branchのサイズを小さく保つために • タスク整理 (Planning) の段階で個々のタスクの粒度を確認する ◦ 目標を具体的にイメージできる粒度にまでタスクを分解する ▪ ex.

    WebAPIを新規作成するために必要なサブタスクを事前にリストアップする • ハンドラーの追加, ドメインモデルの追加 , DBや外部APIとの通信, etc ▪ 分解できないようなら、まず調査タスクを切る ◦ 細分化されたひとつのタスクにつき、一個以上の feature branchを切る • 途中で必要なタスクに気づいても、そのbranchの中ではやらない ◦ TODOコメントをつけておいて、一旦その branchはmergeしてしまう ◦ あとで新たにfeature branchを切って対応する
  37. なるべくレビューのコストを下げる • Pull Request (feature branch) のサイズを小さくする ◦ ひとつのPull Requestに対するレビュー所要時間を短縮できる

    →スキマ時間でレビューができる →スケジュールの融通が効く • 時間を確保して同期レビューする ◦ コードを書いた人に直接説明してもらうのが一番早い ◦ 僕自身、ぱっと見で指摘事項が多くなりそうな時や レビュー→修正が一巡してもなお修正点が残っている場合は同期レビューに切り替える
  38. それでもレビューに時間がかかる場合 • 時間をかけてPull Requestの完成度を追求している間に main branchがどんどん更新されていく ◦ conflictのリスクが高まる • 途中で区切りをつけてmergeしてしまうのも手

    ◦ ビルドやテストが通れば一旦 OK、でもよい • TODOコメントをつけておいて、後で直す ◦ TODOコメントからGitHub Issuesを作成 ▪ https://github.com/alstr/todo-to-issue-action ▪ main branchへのpushにフックさせて実行している ◦ 切ったIssueはDaily standupで拾って修正スケジュールを組む
  39. 仕様の複雑度 • 複雑な仕様を実装したコードは、どうしても複雑になってしまう ◦ 実装にもレビューにも時間がかかる • レビュアーとの認識違いにより、手戻りが発生するリスクがある • 仕様を単純にできるなら、それに越したことはない ◦

    要件定義に介入する ◦ 込み入った仕様も、課題を分割して小さい単純な仕様に落とし込めるかもしれない • それがダメなら、仕様や実装方針について レビュアーと事前にすり合わせておく
  40. 課題 • 現状計測しているサイクルタイムは 個々のfeature branchの生存期間を示しているに過ぎない ◦ 本質的には、個々の機能やそれによる価値を どれだけ早くユーザーにデリバリーできたか を測る必要がある ▪

    変更のリードタイムそのもの をみなければいけない • 計測を試みているが、まだできていないポイント ◦ (トランクベース開発自体の課題ではないが …)