ネットワークサービス部の田上(id:rtagami)です。
最近クレジットカードの請求額が多すぎて計算しなくても赤字確定の状況が続いていましたが、 ありがたいことにボーナスが出たので心配しなくても大丈夫でした(?)。
今回は、わたしたちのプロダクトがプロダクション環境へのデプロイを毎週行うことになった経緯をご説明した上で、 その中で解決しなければならなかった課題と、それをどう解決したかをいくつかご紹介します。
前回は 【CI・CD戦略編 その4】検証環境への自動デプロイ でした。
リリース頻度が低いことの問題
【CI・CD戦略編 その2】自動化されたE2Eテストでプロダクトの品質を維持しようで、 リリース頻度の向上について触れました。
わたしたちがプロダクトを作り始めてから暫くの間は、定められたリリース頻度というものがなく、 基本的には何かしらの事情でリリースせざるを得ない状態になるまではリリースをしない、という状況でした。
リリースするという行為は必然的に一定レベルのリスクが伴います(一切リスクが無いリリースというものは存在し得ないと考えています)。
ですので、リリースをしない(表層的な)リスクが、リリースするリスクを上回らない限りリリースしない、というのは一見合理的な戦略に見えます。
しかし、この戦略はプロダクトの成長に大きな悪影響を与えていました。
ビッグバンリリースはドキドキするけどワクワクしない(ハラハラはする)
リリースせざるを得ない状態になるまでリリースをしない、ということは必然的にリリース間隔が長くなります。 わたしたちの場合は6ヶ月ほどリリース間隔が空いてしまったこともありました。
何より問題なのは、ここまで間を空けてしまうとリリースするという行為がとてつもないリスクを伴うことです。 全てのテストが通っているか、手動でどれだけ追加のテストをしたか、というような理由を元にした"論理展開"はまったく関係なくなります。
一般的なプログラミング言語を使っている限り、現実的な時間的制約の中では可能性のある状態全てをテストできません (どれだけテストをしても、必ずテストできていないケースが残るということです)。 よって、全てのリリースには必ず0より大きいリスクが伴います。
もし半年ぶりにリリースしたとすると、リリースするものは半年間積み重ねた成果になります。 期間中に加えた変更が少なければ良いものの、 一般的には膨大すぎて把握することが困難な量の変更が積み重なっている事でしょう。
結果として、リリース間隔が伸びれば伸びるほど、 問題があった際の切り戻しの判断が難しくなります(切り戻すことのリスクの評価そのものが困難でしょう)し、 何よりもバグが含まれていた際の問題箇所の特定は困難を極めることでしょう。
わたしたちの場合、当時のリリースは常に胃が痛くなるものでした。
フィードバックループのサイクルタイムは短くないと機能しない
プロダクトのフィードバックを得るために最も手っ取り早い手段はプロダクション環境にリリースすることです。 このフィードバックはお客様の声も含みますが、より広範に、コードに加えた変更がもたらした変化と捉えるのが適切です。
わかりやすい例示をすると、障害の発生を未然に防ぐための変更を加えたとして、 それはできるだけ早くプロダクション環境にリリースした上で、 それが期待通りの効果を発揮しているかどうかを評価しなければなりません。 リリースしてみたもののそれが期待通りに動かなかった場合には、 次なる手を考える必要があります。
リリース間隔が空けば空くほど、このフィードバックサイクルが機能しなくなり、 プロダクトの改善が滞ることになるのです。
高頻度でリリースするためにやったこと
このような課題を解決するためにはリリースの間隔をできるだけ短くする事が望まれますが、 現実問題として高頻度でリリースするためには乗り越えなければならないハードルがあります。 ここからは、わたしたちがリリース頻度を上げるために行った施策をご紹介します。 これらの施策によって、現在では無理なく週1回のペースでリリースを継続できています。
リリース手順の簡略化
基本的にリリース作業は定型的なものでしょう(定型的にリリースできていない場合は、まずはそこから手をつける必要があります)。 リリース作業が定型的であるということは、リリース頻度に対して、リリースにかかる手間の合計がリニアに増えていくという事でもあります。 毎週、何時間も掛けてリリース作業を行うことは非現実的であると言わざるを得ません。
よって、リリース作業の簡略化は高頻度のリリースにはマストの施策です。
メンテナンス手順書を簡単に作成できるようにする
何よりもリリース作業の簡略化を阻むのがメンテナンス手順書です。 メンテナンス手順書はリリース毎に作成と社内承認の手続きをすることが定められており、 毎回同じようなメンテナンス手順書を作るだけの割にはやたらと手間がかかっていました。
やたらと手間がかかる主な理由は、メンテナンス手順書を人間が作成していたからでした。 メンテナンス手順書のひな形をコピーし変更すべき内容だけを打ち替え、 ヌケモレが無いか確認してリポジトリにpushしてマージリクエストを作成する、 というような作業を毎週行っていた時期もありましたが、 実に精神的に疲弊するものでした。
いい加減どうにかしないとな、ということで、 人間がやっていた作業をSlackのBotができるようにしました。
具体的には、
- 弊社のGitLabに存在するメンテナンス手順書のリポジトリをBot実行環境内の作業用ディレクトリにクローンする (以下はこのクローンしてきたリポジトリに対する操作)
- リポジトリに置いてあるメンテナンス手順書のテンプレートを、 然るべき規則性をもったファイル名で同じリポジトリ内にコピーする
- テンプレート内のプレースホルダー文字列を、テンプレートエンジンを使って 実際に使用する情報に置換する
- 然るべき規則性をもった名前のブランチに追加したファイルをコミットする
- リポジトリのorigin(弊社のGitLab)にコミットをpushする
- クローンしてきたリポジトリを破棄する (以上がクローンしてきたリポジトリに対する操作)
- pushしたブランチをmainブランチにマージするマージリクエストをGitLabのAPIを使って作成する
というような一連の処理を、SlackのBotのダイアログ1つで行えるようにしました。
以下が実装したダイアログのスクリーンショットです(弊チームではメンテナンス手順書の事をメンチと呼んでいます):
メンテナンス手順書を機械的に生成できるようになったことで、メンテナンス手順書に関連する手続きが
- 手順書を作成するメンバー: ダイアログの質問に答える
- 手順書をレビューするメンバー: 作成されたマージリクエストのコミットが人間によって変更されていない(Botが作成したままの状態である)ことを確認する
だけ、という単純な作業に変わり、メンテナンス手順書作成の負荷が大幅に下がりました。
正常性確認くんを作る
メンテナンス手順書が効率を下げていたもう1つ理由は、メンテナンス手順書の長さです。 当時の手順書は300行を超える重厚長大なものであり、 その中でも特に手間がかかっていたのはデプロイ後の正常性確認でした。
Blue/Greenデプロイを採用しているわたしたちの場合、リリースは
- 1: 新しいアプリケーションを非アクティブな環境にデプロイする
- 2: 問題がなければ非アクティブな環境とアクティブな環境を入れ替える
という流れで行われますが、1と2の間が、デプロイしたものが本当に正しく動くかを確認する最後の機会なので、 各種の確認が集中的に行われることになります。これが、手動で20分程はかかる、なかなか大変な手順でした。
連載16回目の記事 でも軽く触れましたが、「正常性確認くん」はこの手順を簡略化する目的で産まれたものです。 デプロイしたアプリケーションをリリースする前の確認手順の大半をこの「正常性確認くん」に集約することで、 手動で20分程はかかる手順を数分で完了するコマンド1つにまとめ、リリース手順を大幅に簡略化したのです。
この仕組みはその後も発展を続け、リリース時の確認だけでなく、常に本番環境の状態を監視するデーモンに発展しました。 また、リリース時の確認も同じ「正常性確認くん」をCDパイプラインに組み込むことで、コマンドを実行する必要性自体を排除する形で生き続けています。
E2Eテストを充実させる
言うまでもないことですが、E2Eテストの充実もリリース作業の簡略化に役立っています。 E2Eテストのテストケースはむやみやたらに増やすものではないということは CI・CD戦略編 その1 や CI・CD戦略編 その2 でご紹介した通りですが、適切なレベルで必要なテストケースを追加し、内容を見直していく必要はあります。 わたしたちの場合は、E2Eテストとして必要十分と思われるテストケースの実装をしたことにより、 リリース前に手動で確認することは一切なくなりました。
後方互換性を意識した段階リリース
高頻度でプロダクトをリリースしていく為にはオンライン状態を維持したままでリリースできる事が前提条件になります。 リリースする度にシステムがオフラインになるようでは、 提供者としてもメンテナンスの調整に膨大な労力を割かなければなりませんし、 利用者の体験もかなり悪いものになることでしょう。
しかし、オンライン状態を維持したままアプリケーションのバージョンを上げる場合、 極めて短い期間でありますが、新しいバージョンのアプリケーションと古いバージョンのアプリケーションが 一時的に混在した状態でリクエストを処理することになります。
ここで問題になるのが、後方互換性がない変更をプロダクトに加えなければならない場合です。 わたしたちのプロダクトでは、Blue/Greenデプロイメントの採用などによって、 かなりのケースで新旧バージョンの混在を許容できるようになっています。 それでも、双方の環境で共有しているもの(典型的にはDBMS)に関わる変更などでは、 新旧バージョンの混在が処理の失敗を引き起こすことがあります。
プロダクトの性質によっては、単純に処理を失敗させることでもアクセプタブルになる可能性もありますが、 わたしたちのプロダクトでは、もう少し丁寧に変更を加えるようにしています。
具体的には、より新しい バージョンB
がその前のバージョンである バージョンA
に対して後方互換性を持てない(持たせたくない)場合、
バージョンA
-> バージョンA'
-> バージョンB
という形で変更を加え、
バージョンA'
に バージョンA
に対する後方互換性と バージョンB
に対する前方互換性の両方を持たせます。
こうすることで、バージョンA
と バージョンA'
、 バージョンA'
と バージョンB
が同居できるようになり、
バージョンA
から バージョンB
に互換性のない変更ができるのと共に、
1回ずつはオンラインで切り戻しができることになります
(オフラインでしか切り戻しが出来ないとこれもまたリリースの障壁になりますし、
切り戻し自体が出来ないようでれば、そのような変更はそもそも避けるべきでしょう)。
このように、段階的に変更を加えることで(手間は多少増えますが)安全にプロダクトを改善していけるのです。
フィーチャーフラグ
フィーチャーフラグ、またはフィーチャートグルもまた、高頻度にリリースをする一助になるものです (トランクベース開発と高頻度リリースを両立させるものとも言えます)。
わたしたちの場合は週に1回リリースをしていますが、 それは週に1回はmainブランチの内容がプロダクション環境に出ていくことと同義でもあります。 1コミットで機能が完成する場合には問題ないのですが、変更の内容によっては(変更の規模が大きい場合には)、 1コミットに全ての変更を収めるのが現実的でない場合もあります(そもそも1週間では機能を作り終えられない場合もあります)。
古典的なリポジトリ運用では変更を重ねるためのブランチを作ってそこに変更をマージしていき、 機能が完成したらそのブランチをmainブランチにマージするのが正しい訳ですが、 流れの早いリポジトリでこれをすると、苦労します(しました)。 スクラム開発では小さなインクリメントを検査してフィードバックすることが重要ですので、 変更を重ねるためのブランチに変更をマージする際にはコードレビューをするべきでしょう。 かといってコードレビューなしでmainブランチにコードをマージする訳にも行きませんので、 変更を重ねるためのブランチをmainブランチにマージする際も同じコードをレビューすることになります。 そして、すべての機能が出来上がった頃には変更を積み重ねるブランチは完全に時代遅れになっていますので、 mainブランチにマージする際の大量のコンフリクトに頭を悩ませることになります。 晴れてマージした変更は、まさにビッグバンリリースと言って差し支えのないものになっていることでしょう。
このような状況にならないよう、わたしたちはフィーチャーフラグを導入することにしました。 フィーチャーフラグの説明は、より良い情報がインターネット上に数多くあるので省略しますが、 mainブランチに入っているコードを非活性化する手段を用意することにより、 コードリリースのタイミングと機能の追加・変更のタイミングを分離できます。 フィーチャーフラグを導入することにより、トランクベース開発と高頻度のリリースを両立できるようになるのです。
実施しやすいスケジュールの設定
技術的な観点ではありませんが、実施しやすいリリーススケジュールを設定する事はとても重要です。
まず、(何があっても)事前に定めた条件に従ってリリースすることをチームで合意する必要があります。 リリースのタイミングを人間しか決められない(すなわち揺らぎのある)尺度ではなく、 前回のリリースから経過した時間などの議論の余地のない尺度で決定することで、 次のリリースをいつ行うのかという判断をする必要がなくなります。
また、人間にとって認知しやすい間隔でリリースをすることにより、スケジューリングが安定し、チームの行動計画が立てやすくなります。 一時期のわたしたちは隔週リリースに挑戦していたのですが、隔週リリースは毎週リリースと比べて格段に難しかったと感じます。 隔週リリースではリリースの予定をスプリントに入れ忘れたり、スプリントの計画をリリースの有無によって変更する必要があったりと、 運用上の問題を解決しづらい状態が続いてしまい、隔週リリース自体が自然消滅するという形で失敗してしまいました。 これは、1週間というスプリントの長さ対して、2週間ごとにリリースするというスケジュールのミスマッチが最大の問題だったと考えています。 わたしたちの場合は、スプリントの長さとリリース間隔を合わせた毎週リリースにした事により、リリース作業が定着しました。
そして、スケジュールに従って継続的にリリースを繰り返すことは組織面でもポジティブな影響があります。 リリースの体験を積み重ねることによって、その体験からフィードバックを得られるようになり、 リリースに絡む自動化実装や運用手順が成熟し、チームとしても自信を持ってリリースできる状況が継続するようになります。 同じように、リリースが成功する状態を継続させることによって、チームとしての信頼を プロダクトオーナーやステークホールダー、マネジメントから勝ち取ることも出来ます。
レトロスペクティブ
さまざまな施策を通して週1回のリリースはわたしたちのプロダクトに定着しましたが、 一方で現状に対する課題もいくつか浮かび上がってきています。
これ以上のリリース頻度の向上は難しい
先進的なプロダクトではmainブランチへのマージをきっかけに、 そのままプロダクション環境へのリリースをトリガーしている場合もあります。 On-demandリリースはThe Four Keysで 明確にEliteと書かれていることもあり、施策として説得力もあります。
しかし、今のところわたしたちのプロダクトでは週1回のリリースが適切なレベルだと感じています。
主な理由は以下の2つです:
- メンテナンス手順書の承認手続きを自動化するモチベーションが出せない
- プロダクトの性質的にカナリアリリース等の施策が効果を持ちづらい
将来的に何かが変わる可能性は十分ありますが、 現時点では週1回のリリースのワークフローをブラッシュアップしていく事を優先的に考えています。
フィーチャーフラグを自動でテストしづらい
フィーチャーフラグはプロダクトの挙動を動的に変えるものですので、 当然ながら一時的に(特にフィーチャーフラグの有効化直前に) 変更前と変更後の両方の状態を集中的にテストする必要があります。 しかし、特にE2Eテストのような高レイヤーのテストは組み合わせの増加に弱く、 変更前の挙動と変更後の挙動の双方を確認できるようなテストにするためには、 実質テストを2倍書かざるを得ないというような状況に陥ることがあります。 ただでさえ高コストなE2Eテストを2倍書くことになりますので、 フィーチャーフラグが必要となるような実装の変更は気軽には行えていないというのが現状です。
段階リリース・フィーチャーフラグの管理が煩雑
段階リリースの場合は、1段階目を実装して、しばらく間が空いてから (Blue/Green双方の環境にリリースできてから)2段階目を実装する必要があります。 フィーチャーフラグの場合は、今どのようなフィーチャーフラグがあって、 どの環境でどのフィーチャーフラグが有効になっているかを管理する必要があります。
これらの管理は煩雑というか性質上の認知負荷がかなり高く、 どうしても漏れたり忘れたりといった事が発生してしまいます。 漏れたり忘れたりしても直接的にトラブルになることはないのですが、 プロダクトのライフサイクル管理上好ましくないのは間違いありません。
一応の対応として、段階リリースが必要となる変更の場合は 1段階目のIssueの本文に2段階目のIssueを作る必要があることを明記しておく、 フィーチャーフラグは管理用のWikiを設けるなどの対策はしています。 しかし、結局のところは人間の注意力に依存していますので、 根本的な解決には至っていません。
まとめ
今回の記事の内容以外にもリリース頻度の向上の前提となる施策はいろいろとあります (例えば、CDパイプラインを整備することや、リリースしやすいシステム構造の実現など)。
リリース頻度は盲目的に高くすれば良いというものではありませんが、 適切なリリース頻度をチームで合意し、それを確実に成功させていくことが、 チームの生産性の向上に寄与することは間違いないと考えています。
また、リリース頻度の向上は、それ単体で成立する目的ではないことには注意する必要があります。 できるだけ簡単に、かつ安全にリリースできるワークフローを整えることが求められているという大前提があり、 The Four Keys も そのように読み解く必要があることは念頭に置いておく必要があるでしょう。
次回は、わたしたちのシステムに実装されている、 アプリケーションレベルのヘルスチェックについてご紹介します。 お楽しみに。
連載バックナンバー
CI戦術編
- 【CI戦術編 その1】alembic check コマンドを活用したマイグレーションスクリプト生成忘れ防止
- 【CI戦術編 その2】コードレビューを充実したものにする方法、あるいは一生残る恥ずかしい履歴を作らないように
- 【CI戦術編 その3】Pythonのimportのことならまかせろ isort
- 【CI戦術編 その4】Blackを利用したコーディングスタイルの統一
- 【CI戦術編 その5】Pythonで明示的に型を書く理由
- 【CI戦術編 その6】Python開発の強い味方 Pylint
- 【CI戦術編 その7】 pyupgradeを使って最新の記法に対応してみた
- 【CI戦術編 その8】OAS(OpenAPI Specification)で仕様書を自動生成しよう
- 【CI戦術編 その9】自動生成しか勝たん openapi-typescript
- 【CI戦術編 その10】 Renovateで依存ライブラリのアップデートに負けない方法
- 【CI戦術編 その11】Trivy: あなたの使ってるライブラリ、大丈夫ですか?
- 【CI戦術編 その12】戦術編総まとめ — いつ何を使うべきか
- CI・CD戦略編