- バックエンド / リーダー候補
- PdM
- Webエンジニア(シニア)
- Other occupations (18)
- Development
- Business
高頻度で安全なリリースを支える「クラスタ」という抽象
Photo by Arfan Abdulazeez on Unsplash
こんにちは、ウォンテッドリーで技術基盤担当のエンジニアリングマネージャーをしている大坪です。このストーリーは、Wantedly Advent Calendar 2023 の24日目の記事です。前日23日目は機械学習領域のテックリードになって1年間取り組んできたことです。
この記事は技術書典15で頒布した WANTEDLY TECH BOOK 13 の第2章 高頻度で安全なリリースを支える「クラスタ」という抽象 に加筆修正を加えたものです。
リリースとデプロイの分離
DevOps の重要性と開発組織のパフォーマンスを示す Four Keys が知られるようになり、高頻度にデプロイするケイパビリティの重要度は知られるようになりました。一方で本番環境への変更の敷居を上げる項目は非常に多く、デプロイ頻度を高めることが簡単でないと感じる状況もよくあるでしょう。そのような場合に見受けられるのがリリースとデプロイの混同です。この2つを別物であると認識し、リリースを行わないデプロイを繰り返すことで本番環境から高頻度にフィードバックを得ることのできる環境を手にすることができます。本記事ではリリースとデプロイを直交するものとしてそれぞれ定義し、さらにリリースの 4 つの手法をまとめるためのマイクロサービスのクラスタを再定義します。これによりリリースサイクルを高速化し、開発組織のパフォーマンスを向上させる事ができるようになります。
本章の前提として、複数のマイクロサービスで構成されるシステムの更新方法について述べます。主にウェブ上で HTTP や gRPC などで提供されるサービスを念頭に置きますが、より一般にユーザー起因のリクエストからレスポンスとデータベースへの変更が伴うシステムに適応できる概念について考えます。
リリースはリスクのある本番環境の変更
まず、本章におけるリリースの意味を定義します。本章では「リスクのある本番環境の変更」とします。
ここで言うリスクとは様々なものがあります。まず自明なものとして機能やロジックの変更などユーザーが目にする変更は常にリグレッションのリスクを持ちます。デプロイ頻度を下げたくなる要因として最もポピュラーなものでしょう。これに加えて影響範囲の大きい基盤部分のリファクタリングや Rails の upgrade など根幹となる依存関係の更新もリスクのある本番環境への変更と考えることができます。
当然ながらこの定義ではほとんどすべての変更がリスクのあるものという扱いになってしまいます。そのためどこからをリスクと捉えるかを区別するための一定のしきい値が必要になりますが、この具体的なしきい値は本章の本題から外れるため議論から外します。重要なのは「どのようにして一定のしきい値を超えたリスクと向き合うか」というプロセスの部分です。
高頻度デプロイは高頻度フィードバックを生む
リリースを避けてでもデプロイを増やしていくべきであるという考えがこの章の前提となっていますが、この理由を考えます。DevOps 研究に於いてソフトウェアデリバリーと組織のパフォーマンスを改善する能力の一つとしてトランクベースの開発が挙げられています。これはメインブランチに高頻度にコード変更をマージしていく手法です。
トランクベースの開発はテキストとセマンティック両方でコンフリクトを生じにくくできることがその価値の一つですが、もう一点重要なことに、トランクの更新を高頻度でデプロイできれば本番環境からのフィードバックが得られるという点があります。フィードバックの利用方法は大きく 2 種類あり、Testing in Production と Observability Driven Development です。前者は本番で動作していることを持ってソフトウェアの品質の高さの確信を得る手法です。本番環境で動作していることが最も信頼できるソフトウェアの正しさであるという考えに基づきます。リグレッションのリスクと影響度を十分小さくできれば「今日書いたコードには問題がなかった」と高い精度で言えるようになるということです。後者は本番環境である機能やロジックがどのように使われているかを知ることが次の意思決定につながるという考えです。これもリスクが十分に小さければ「今日書いたコードが意図した改善をしている」という確信が得られるようになります。
高頻度なフィードバックは意思決定までのリードタイムを縮めることができるため開発チームの生産性に重要です。しかし基本のコード変更でトランクベースの開発が実現できている場合でも、大きな機能の開発を例外だと捉えデプロイを避けたくなってしまうケースがあります。多くの場合この傾向がデプロイとリリースの混同によるものだと筆者は考えています。
デプロイをしてもリリースをしない
デプロイをしてもリリースをしない方法、つまり本番の更新をリスクなく行う方法はいくつかあります。機能を隠せば良い場合はとりあえず隠してデプロイして環境変数やフィーチャーフラグでリリースをすることができます。リリースするまでは機能変更は生じないのでデプロイリスクは少なくしつつ Testing in Production が可能になります。リリース時のリスクは後述するカナリアリリースや AB テストで軽減することができます。
段階的にリリースしても良い場合はデータ変更でできないかを検討しましょう。ドッグフーディングとして開発者のみに露出することもデータ変更によってできる実現できます。重要なのは「デプロイにリスクがある」と判断したときに「デプロイをしない」以外の方法がないかを考えることです。
リリースする 4 つの方法
デプロイをしてもリリースにしない方法にいくつかありそうな事がわかりました。基本的には機能を隠すということです。ここでデプロイは必ずしもリリースにならないことがわかりましたが逆もあります。つまりデプロイ以外のリリース方法です。筆者はこの方法に デプロイ / 環境変数変更 / フラグ変更 / データ変更の 4 つがあると考えています。この 4 つを考察することでリリースを行うまでのプロセスに求めるべき性質が浮かび上がるので改めてそれぞれを詳しく見ていきましょう。
デプロイ
リリースする 4 つの中で一般に最も高頻度で行われるものがデプロイでしょう。ここでは本番環境で動作するプロセス の revision の変更と考えます。一般的には本番で動作する Docker イメージやバイナリなどの build 生成物を決定する git の commit hash になります。
ただしデプロイ自体には前述のリリースには該当しないものが多数含まれることに注目するべきです。例えば、変数名の変更やデッドコードの削除など動作プロセスに実質的に変更が加わらないコード変更はリリースにはなりません。また、新たなログを追加するような変更は「本番環境からフィードバックが増える」という意味でデプロイ頻度を高めるモチベーションの一つですが、リリースにはなりません。 後述するような環境変数やフラグの背後に隠された機能変更のデプロイもリリースには該当しません。
このようにリリースにはデプロイがよく使われる手法でありつつ、デプロイは必ずしもリリースを意味しません。リリースを行う手法はデプロイに限定されず、デプロイも必ずしもリリースを意味しないことからこの2つは直行する概念だと言えます。
環境変数変更
デプロイを高頻度に行いつつリスクを抑える新機能開発に環境変数の後ろに隠す手法があります。例えば FOO_FEATURE_RELEASED
というような環境変数を用意して、それが false
の間は新しいコードは呼び出されない、 という状態にしておくと高頻度にデプロイを行っても十分にリスクを下げられるということです。
このシナリオではリリースと数えられるのは環境変数を更新するとき、つまり FOO_FEATURE_RELEASED
の値をtrue
などに変更するタイミングです。いきなりすべての挙動が true
とするだけだとただリスクを受け入れるタイミングをまとめているだけのようにも見えます。詳しくは後述しますが、カナリアリリース、ABテストと言ったような手法でリスク分散したり、本番相当の環境を複数作成することで開発者が質の高いフィードバックを得ることでリスクを減らすことを開発プロセスに組み込むことができるようになります。
フィーチャーフラグ変更
環境変数に似た概念としてフィーチャーフラグがあります。ここでフィーチャーフラグとはリクエストタイムに値が決まる設定値のことを指し、具体的な実装方法には触れません。ただしここでは、ユーザー起因のリクエストで生じるすべての処理において共通して参照できる値のことを指します。フィーチャーフラグを有効にすることで本番環境の挙動を変更することでリリースを行うことができます。
これは様々な実装方法がありますが代表例では HTTP ヘッダや gRPC メタデータを伝搬することで共通の値を参照する方法や、各マイクロサービスが trace id をキーとしてフィーチャーフラグ専用マイクロサービスに問い合わせる方法などがあります。ウォンテッドリーでは前者の HTTP ヘッダの伝搬を行うことでこの基盤を実現しています。
プログラムの挙動をアドホックに変える値という意味では環境変数と似ています。一方で下記のような違いがあります。 環境変数は一つのマイクロサービスに閉じ、リクエストに閉じません。したがって、リクエストごとに変えることができない一方で、起動オプションなどの広範な利用先があります。フィーチャーフラグは起動時に確定しないかわりに trace id を共通に持つ全ての処理で参照できるという特徴があります。つまりリクエストに閉じ、マイクロサービスに閉じません。
後述しますが、ABテストもこのフィーチャーフラグの一部と考えることができます。
データ変更
データ変更はデータの状態によってプログラムの挙動を変えていく手法です。例えば users
というテーブルに、use_new_ui
という boolean
のカラムを追加して、これが true
のときのみ新しい UI を用いる、という処理を挟む場合を考えます。 ここで use_new_ui
は開発者のみが変更できるとします。この場合少しずつこのカラムの値が変わっているユーザーを増やすことで段階的にリリースができます。
ウォンテッドリーの事例ではストーリーエディタを Draft.js から Lexical に移行した事例がこれに当たります。content_type
というカラムを追加し、これに応じてエディタ / 表示 / API の実装を分けました。その後大まかには下の手順でリリースを行いました
- 内部リリースとして自社の新規投稿のみを Lexical にする
- 自社の投稿をすべて Lexical に変換する
- ユーザーの新規投稿を Lexical にする
- ユーザーの過去投稿を徐々に Lexical に移行する
このようにデータを少しずつ移行することで段階的にリリースする手段がデータ変更です。 一般的にフィーチャーフラグと呼ばれる基盤も、実装手法によってはこのデータ変更で行われているものであると考えることもできます。
それ以外もこの 4 つに吸収できる
以上 4 つの手法を紹介しましたがこれ以外にリリースを行う方法は存在します。例えば時刻を決めておき Date.now > SOME_RELEASE_DATE
というようなコードを入れておく事ができます。これはすでに述べた 4 つよりもリスクを下げることが難しい一方で大きなメリットはありません。時刻起因で変化するものはその時刻に環境変数やフィーチャーフラグを変更する、という方法でも十分再現可能です。その他の検討できる手法も上記 4 つに吸収することができるため、筆者はこの 4 つを基本的なリリース方法であるとするのが妥当だと考えています。
段階リリース手法
さて、4 つのリリース方法が整理されても結局のところ、そのリリースの瞬間にリスクを受け入れる必要があります。このリスクを減らす方法にカナリアリリースや AB テストがあります。
それぞれ独立であると考えて基盤を作っていくこともできますが、 素直にそれぞれを支える基盤を構築するとそれぞれの手法でできることが大きく変わってしまいます。 AB テストとカナリアリリースの2つの例を用いて素直に実装した場合に見落としてしまうポイントについて考えます。
AB テスト
まずは AB テストについて考えます。 AB テストの元来の目的である有効性の検証という部分はここでは忘れ、基盤として何が起こっているかに注目します。すると AB テストは2つのフィーチャーフラグの値を確率的に混ぜ合わせる手法だと考えることができます。これを素直に作ると実装の考慮から漏れる可能性のある機能として下のようなものがあります。
- あるユーザーを特定の variant に固定することでデバッグを容易にする
- 簡単に variant を変更できる機構を作る
- 特定の variant を確実に表示させる URL を発行する
これらはなくても大きくは困らないものの、一度実装してみると生産性の向上を実感できます。
カナリアリリース
次にカナリアリリースについて考えます。 一般にカナリアリリースは新旧2つのシステムを並行して稼働させることで問題が起こったときの影響範囲を狭くする手法です。 ウォンテッドリーの初期のカナリアリリースは「本来 1 つしか存在しないはずの revision を複数混在させる」という形で実装しました。これは「デプロイの中間状態をあえて維持する」という状態だと解釈することもできます。これだけでも十分価値があるものの下のような機能は未実装でした。
- 環境変数のみを変更したシステムを並行して稼働させる
- 特定のフラグのみを変更したシステムを並行して稼働させる
- 特定の variant を確実に表示させる URL を発行する
- 開発者のみに新しい variant が表示できるようにする
つまり最初期にはデプロイというリリース方法のみに着目していましたが、4 つのリリース方法全てに対応できる基盤として作ることも可能だったということです。
クラスタという抽象
上のABテストとカナリアリリースの2つを見ると共通のニーズが引き出せます。
- 特定の人だけにリリースしたい
- 特定のリリースに確実に到達できる URL がほしい
そして、4 つのリリース方法をまとめ上げる概念もありそうだと言うイメージが湧きます。筆者はこれに対して「クラスタ」という抽象を提唱します。
クラスタの定義
そもそもリリースを考える段階で、4 つの方法があると示しました。これはつまり下の 4 つが決まると本番環境の挙動が決定できることを示しています。
- マイクロサービスの revision リスト
- マイクロサービス毎の環境変数リスト
- フィーチャーフラグリスト
- データベース
そこでこの 4 つの attribute で決まるシステムをクラスタと呼ぶことにします。ウォンテッドリーでは Kubernetes クラスタを拡張した概念として考えていますが、 ここでは具体的な実現方法ではなくあくまでこの4つの値で決まるシステムです。多くの場合、本番環境 / 検証環境 / 開発環境のように少なくとも数個用意されているでしょう。
クラスタが持つべき性質
クラスタがもつべき性質として下の 2 つがあります。
- ユニークな URL が発行できる(されている)
- 他のクラスタとアイソレーションが保たれている
どのクラスタにも URL が発行できると検証やデバッグが容易になります。本番環境と検証環境でそれぞれ URL が異なっているのがスタンダードであることから一般的な性質と考えることができます。2つ目のアイソレーションはあるクラスタの問題が他のクラスタに波及しないこと要求します。
この性質がどのような意味を持つかは具体例からアプローチするのが良いと考えるので具体例を見ていきましょう。
クラスタの具体例
具体的にクラスタがどのようなものを指すのかを実現が容易である順番に一つずつ見ていきます。
本番/開発クラスタ
おそらく殆どの開発現場で複数クラスタの運用が実現されているでしょう。ウォンテッドリーでも本番環境 / 検証環境などの分離では異なる URL が発行され、検証環境にバグを入れてしまっても問題になら状態が実現されています。 revision / 環境変数 / フラグ / データベースの 4 つの attribute も独立して管理できています。ただし、多くの場合本番環境と近い構成を取っているためコストが無視できません。そこで次にこれを動的に増やす手法について考えます。
動的な検証クラスタ作成
まずはウォンテッドリーでの例を示します。ウォンテッドリーではクラスタを動的に増やす方法が実現されています。大規模なサービスを運用していてもと各開発者が検証したいのはその一部分です。そこで共通で良い部分は共有して利用し、4 つのリリース方法によって変更される部分だけを copy on write で作成します。これによって新しいクラスタを発行するまでの時間は数分程度で更に追加コストは無視できる程度ですみます。例として potsbo.qa.wantedly.com
というような特定の開発者専用の環境を短時間で構築することができるため、他の開発者に影響を与えずにアグレッシブな検証ができます。また new-feature-released.qa.wantedly.com
のような新機能検証環境や、Pull Request ごとのプレビュー環境も簡単に用意できます。詳しくは マイクロサービスでもポチポチ確認するための Kubefork を参照してください。
このように検証環境相当のクラスタをいつでも作成できると様々な開発を同時に行うことができるため便利です。ここで URL が発行できるという性質が意味を持ちます。クラスタの 4 つの attribute のどれかが変わった環境を再現することは難しくなくともそこにユニークな URL がつけられる基盤の実現は簡単ではありません。 URL の存在により、複数の開発者で同じクラスタをいつでも参照できるため再現実験が容易になります。ある機能 A とある機能 B が両方有効になった環境がほしければ a-b-enabled
というラベルを共有するだけで良くなるのです。
本番環境での複数クラスタ混在
最後に本番で複数クラスタを混在させる方法について述べます。ウォンテッドリーではこの部分のサポートは限定的であるため次の技術投資の対象です。先に述べたカナリアリリースと AB テストの2つをクラスタという概念を用いるとどのように考えることができるのかを述べます。
カナリアリリース再考
カナリアリリースは本番クラスタのコピーを 1 つ作成し、それを既存の本番環境と混在させる手法であると考えることができます。このように考えると、複数の revision を同時に混在させるという狭義のカナリアリリースの概念を拡張できます。この考えに則ると revision 以外にも環境変数 / フラグ / データの一部を変更したカナリアリリースも可能であるべきであり、また、カナリアリリース中に既存クラスタ / 新規クラスタの両方に選択的にアクセスできる URL が発行できるべきです。 URL については具体的にはwww.wantedly.com
に対して canary-new.wwww.wantedly.com
や canary-old.www.wantedly.com
のようなドメインでそれぞれの variant にアクセスができるべきであるということです。一般ユーザーのアクセスは拒否するなどの工夫を行えば、カナリアリリース中の検証が非常に容易になります。「混在させる」とあえて抽象的に書いていますが、この割合を徐々に変えていくインターフェースを用意すれば「まずは開発者が本番データで確認を行い徐々にアクセスするユーザーを増やしていく」といったような段階リリースも考えることができます。
AB テスト再考
次に AB テストも考え直してみましょう。ここでも同様に A variant を利用する feature-a.www.wantedly.com
や B variant を利用する feature-b.www.wantedly.com
などを考え出すことができます。これだけでもあると便利ですが重要なのはカナリアリリースとの統合です。
元々は複数のフィーチャーフラグ値の混在でしたが、revision / データ / 環境変数の値を混在させる AB テストも考えて良いことになります。実際のところフィーチャーフラグ以外の AB テストは実質的にカナリアリリースと考えられるので実用性には富んでいません。しかし逆に考えると、AB テストとカナリアリリースが複数クラスタの混在という意味で同じであるという解釈を引き出せます。
全ては混在
クラスタという抽象を用いることで、カナリアリリースと AB テストをまとめ上げることができました。両方とも複数クラスタの混在という観点で同じものだと考えることができます。これは挑戦的な考えでもあるためウォンテッドリーではまだこの 2 つを行う基盤は同じものであるという見せ方をしていません。しかしそれぞれの手法でできることに違いがあると最も便利なものに引きずられてしまいます。例えばウォンテッドリーでは AB テストでは特定の variant を固定できるなどカナリアリリースにはない機能が多数あります。これによってカナリアリリースが不必要に避けられてしまっていると感じています。プロダクト開発をする中で最適なリリース方法を選ぶためには、それぞれの方法でできることの差分を減らして行くことが重要だと考えています。
クラスタがもたらすもの
以上 revision、環境変数、フィーチャーフラグ、データの 4 つの attribute を持つクラスタという概念を定義しました。クラスタという概念を用いると、個別に議論されていたカナリアリリースや AB テストをクラスタの混在扱うことができ、またデバッグにとって有用な機能を盛り込むことができることがわかりました。高頻度にデプロイを行い本番環境からのフィードバックを増やすためには適切なリリース方法の選択が不可欠です。適切なリリース方法の選択のためにはまずクラスタという概念を認知して 4 つのリリース方法、そして段階リリース方法のそれぞれを対等に扱っていくことが重要になります。
おわりに
ウォンテッドリーにおいては様々なリリース基盤がそれぞれ異なる機能を持った状態になっています。これらをすべて統合することが良いかについては筆者も強い自信は持てていません。しかし強く信じることとして、デプロイは高頻度にできなければならない、そのために安全なデプロイができる状態が担保されていなければならない、という点があります。良い基盤もプロダクト開発と同様に強い信念と試行錯誤の繰り返しによって作られるものです。強い基盤を作っていくために基盤も高速に変えてフィードバックを得られる状態を維持していこうと考えています。