STORES 予約 でwebアプリケーションエンジニアをやっております。ykpythemindです。 本記事は hey Advent Calendar 2021 の15日目です。
STORES 予約 は2013年のサービス開始から8年ほど経ち、ある程度成熟した Webアプリケーションになってきました。 新型コロナウイルスワクチンの職域接種に関する予約数は累計195万件超となりました。
しかしながらまだまだ足りない機能や改善すべき点が多くあり、継続的に安全なデプロイ/リリースを行っていくことが非常に重要なフェーズになっています。
今回は私達の開発フロー、とくにGitHubのPull Request機能を使った細かいリリース戦略について書きます。
- GitHubフロー
- Pull Requestを小さく作る
- フィーチャートグルを使う
- プロダクトマネージャーと相談しつつ少しずつリリース
GitHubフロー
私たちのブランチ戦略はGitHubフローを採用しています。 GitHubフローはmainブランチを常にデプロイ可能な状態に保っておくシンプルなルールを元にしたフローです。
Pull Requestのauthorがデプロイ+デプロイ後のエラー監視まで実施する運用をしています。デプロイ後にエラーレートが上がっているなどがあれば本人がすぐにロールバックを行います。 ちなみにこれはSTORES 予約 の前身のクービック株式会社からのフローなのですが、とくにデプロイ担当やデプロイタイミングを決めないことで各人のコードへのオーナーシップや開発文化の形成に寄与しているような感覚があります。
Pull Requestを小さく作る
私達の開発フローではOpenされたPull Requestは常にマージ+デプロイ可能な状態であるべきです。Pull Requestは常に意味のある単一の変更が含まれることになります。
タスクをやる前に先に交通整理が必要そうであれば実施してPull Requestにする、データベースの変更が必要な場合は先にmigrationファイルのみを含めたPull Requestを出して db:migrate
を実施するなど、なるべくdiffを小さくするようにしています。目安としては +-100 くらいの変更であれば問題なく、+-400あたりから認知負荷が高くなってくるので分割できる方法がないか探っています。
例外としては、機械的なlintの修正などは大きくなってしまうことを許容しています。また、一度大きいPull Requestを作って作業方針のイメージを共有してから、それ自体はマージせず、安全に出せるようPull Requestを複数に分割してデプロイしていくアプローチをすることがあります。
私たちはチームのPull RequestのデータをBigqueryに集計している *2 のですが、2021年は1人の開発者が1日に平均2つのPull Requestを作成してマージしていることがわかりました。チームの1日あたりの最大Merged Pull Request数は7月19日の38だったので, 38回近くデプロイした日があるようです。
フィーチャートグルを使う
大きめの機能のリリースや、リリース日が決まっているタスクの場合、フィーチャートグルを入れつつ mainブランチにマージして本番にデプロイしていくスタイルを取っています。
フィーチャートグルは環境変数などで処理の分岐をすることで、特定のコードを本番環境では実行されない処理/表示されないリンク にするテクニックです。 *3
Rubyにはフィーチャートグルを実現するgemがいくつかありますが、私達は特にgemを使用しておらず、Rails.configuration.x での切り替えを行っています。
# config/application.rb module Server class Application < Rails::Application # 省略 ... config.x.use_new_expiration_for_ticket_book = ENV['USE_NEW_EXPIRATION_FOR_TICKET_BOOK'].present? end end
class TicketBook < ApplicationRecord # 省略 ... def will_expire_at(start_at: Time.zone.now) if Rails.configuration.x.use_new_expiration_for_ticket_book # 新しい仕様の場合こちらのコードが使われる. 機能リリース後, 問題なければこちらの分岐のみ残す。 start_at + (expiration * 30.days) else start_at + expiration.month end end end
検証環境では環境変数USE_NEW_EXPIRATION_FOR_TICKET_BOOKを設定することでこの新機能を使用できます。
不安な場合、テストも追加します。RSpecの場合、mockしてしまってもいいでしょう。
allow(Rails.configuration.x).to receive(:use_new_expiration_for_ticket_book) .and_return(true) # 新しい仕様の場合のテスト...
機能を正式にリリースする場合は本番環境でも環境変数USE_NEW_EXPIRATION_FOR_TICKET_BOOKをオンにし、リリース後にも問題がない場合最終的にフィーチャートグルの分岐を削除します。
プロダクトマネージャーと相談しつつ機能を少しずつリリース
最終的にAという機能をリリースしたいときに、いくつかの機能的な改善が含まれることがあります。
たとえば「予約カレンダーのモーダル内に担当スタッフのリストを表示する」のような機能Bがあるとします。 これは機能Aを実現するために必須のタスクなのですが、実は機能A自体の完成を待たなくてもリリースできるのです。
この場合チームのプロダクトマネージャーと相談し、単純にmainにマージして機能Bのみをリリースします。(私達の場合はリリース ≒ デプロイの為、デプロイを実施する。)
機能リリース自体、いくらテストをしていても不安定で何が起こるかわからないものです。不確実な大きいリリースを避けるように日々運用しています。
良い点
以上のように細かいPull Requestベースでのデプロイを意識して開発しています。日々の業務の起点がPull Requestになるので分かりやすいです。
2年ほど前はもう少し大きい粒度のPull Requestで大雑把なデプロイを行っていたのですが、チームが拡大する中で、粒度を小さくする方針が根付いてきました。これによってデプロイ後の不具合が発生しにくくなったのと、Pull Requestやタスク自体の手戻りがなくなりました。少ない変更を重ねていくのでPull Requestのレビュワーの負担が減り、方向修正や議論がしやすいです。
今後の課題
STORES 予約 の開発チームは2021年12月現在、フルタイムの開発者が7名、業務委託の方も7名程度のまだまだ小さいチームです。 しかしながらデプロイを頻繁に行う都合でデプロイ待ちやデプロイタイミングが被ってしまうことが少しずつ問題になりつつあります。 デプロイの競合やしばらくmainブランチをデプロイしてほしくない場合などに備えて、デプロイロックの仕組み *4 を実装したいと思っています。
今後もシンプルなデプロイフローを使いつつ、よりチームに最適化するように改善を続けていきます。
*1:
「デプロイプロセスに正解はない」――MicrosoftとGitHubが明かす機能リリースの裏側【デブサミ2020】 (2/2):CodeZine(コードジン)
Improving how we deploy GitHub | The GitHub Blog
などで実例が紹介されています。なお、2021年でも基本的にはmainブランチが常に本番デプロイ可能なのは同じそうですが、GitHubではmainブランチへのマージ前にブランチを本番環境にデプロイし、問題がなければmainに取り込む運用をしています。私達のフローとは厳密には異なりますが、本記事ではGitHubフローという名称とさせていただきます。
*2: Pull Requestから社内全チームの開発パフォーマンス指標を可視化し、開発チーム改善に活かそう - Hatena Developer Blog こちらの記事を参考に取り込んでいます。
*3:
One of the most common arguments in favor of FeatureBranch is that it provides a mechanism for pending features that take longer than a single release cycle.
https://martinfowler.com/bliki/FeatureToggle.html
*4:Deploying branches to GitHub.com | The GitHub Blog にGitHubでの実例が紹介されています。