Katashin .info

お祈りデプロイを避けるためのフィーチャーフラグ運用

「今日リリースの機能ですが、マージ前に軽くレビューしてもらえますか?」

同僚にそう言われてプルリクエストを見ると1000行以上の差分がある。たしかにこの機能は大きく、開発の完了まで本番環境にリリースしてしまわないようにブランチを分けていたのだが、そうは言っても差分が大きすぎてレビューが大変そうだ。リリースまでの時間もないし、軽く動作チェックをして、問題がないことを祈ろう……


チーム開発でレビュワーの経験がある人なら、このような状況に覚えがある人もいるでしょう。しかもこういうのは大抵タイムリミットが短く、最終的には時間がないので問題が起きないことを祈りながら Approve するのです。検証やテストが不十分なまま、問題が起きないことを祈りながら本番環境にデプロイすることを俗にお祈りデプロイと呼ぶこともありますが、これはできたら避けるべきです。

筆者はこれに対処するため、フィーチャーフラグを用いた開発を数年間試したところ、開発体験が良くなった感触を得ています。この記事ではフィーチャーフラグの解説を交え、筆者が実際に試した開発方法を紹介します。

フィーチャーフラグとは #

フィーチャーフラグ(Feature Flag) はある機能の有効・無効を切り替えるためのフラグです。フィーチャートグル(Feature Toggle) と呼ばれることもあります。例えば、ドキュメント管理のアプリに新しく検索機能を開発する場合を考えます。この機能をフィーチャーフラグで管理するときは flags.searchDocument のようなフラグを定義し、以下の例のように使うことで検索フォームの表示・非表示を切り替えられるようにします。この記事では Vue.js のコードを例にしますが、他の UI ライブラリでも考え方は変わりません。

<template>
  <!-- ドキュメント管理のアプリ -->
  <div class="app">
    <!--
      検索フォームを表示するコンポーネント
      flags.searchDocument で表示・非表示を切り替える
    -->
    <DocumentSearchForm v-if="flags.searchDocument"></DocumentSearchForm>

    <!-- ユーザーのドキュメント一覧を表示するコンポーネント -->
    <DocumentList></DocumentList>
  </div>
</template>

フィーチャーフラグは様々な使い方ができますが、本記事ではリリースまでの開発工程を改善する目的のみで使うこととし、解説するコード例もそれを前提とします。

ブランチは小さく素早くマージする #

ブランチを分けて開発する時、最後にマージする工程を楽にするために、分けたブランチの差分は小さく保ち、なるべく早くマージします。

差分が大きくなると、マージ時に発生する競合が多くなり、解消に多くの時間をかける必要がでてきます。差分を確認するレビュワーの負担も大きくなります。リリースも大量の変更を一気にリリースする、いわゆるビッグバンリリースになりやすいです。リリース前の検証が薄くなり、問題が発生しやすく、どの変更が問題の原因なのか判別しづらくなります。

差分の大きいブランチが複数作られると、それぞれのブランチ間でコードが断片化してしまい、お互いに影響する変更を取り入れるのが難しくなります。例えば、頻出の UI パターンを共通のコンポーネントにする時、その時に分かれていたブランチでもそのパターンが使われていて、コンポーネント化に漏れが発生する場合があります。

小さく素早くマージするためのフィーチャーフラグ運用 #

上記の問題を解決するために、筆者が実際に数年間チーム開発で採用して良い感触が得られた、フィーチャーフラグを用いた開発方法を紹介します。

リリースの細分化を検討する #

まず、フィーチャーフラグ以前の話として、開発する機能を細分化し、リリース単位を最小限にします。リリースするコードが小さければブランチをこまめにマージするだけで良く、フィーチャーフラグを使う必要はありません。

例えば、ドキュメント管理アプリの検索機能を開発する場合、検索と一口に言ってもその機能を細分化できる場合があります。まずは単純な入力テキストとドキュメント名のマッチングをリリースし、その後ドキュメントの種類や作成者による絞り込みをリリースした後、検索結果の並びかえをリリースするというような分割を検討します。

機能の細分化をしない例を表した図機能の細分化をした例を表した図

リリースしたくないときはフィーチャーフラグで隠す #

様々な事情でリリースを分割できない場合や、そもそも機能の分割ができない場合は、フィーチャーフラグを使って、機能を隠したままコードを main ブランチにマージします。

先ほどの検索機能の例でいうと、検索機能をフィーチャーフラグで隠しながら、開発はテキストのマッチング、絞り込み、並びかえで別々のブランチを作り、それぞれの開発が終わり次第 main ブランチにマージします。これにより、ブランチを小さく保ち、マージを素早く行いながらリリースのタイミングを管理することができます。

フィーチャーフラグによるリリース管理を表した図

フィーチャーフラグの実装は、以下の例のように単なる変数に Boolean の値を入れるだけで十分です。外部サービスなどは使わなくても十分運用できます。開発環境では flags オブジェクトを window に代入しておくと、フラグのオン・オフをコンソールから行い、動作を確認することができます。

import { reactive } from 'vue'

// デフォルトで開発環境、ステージング環境では true にする
const defaultValue = import.meta.env.DEV || import.meta.env.MODE === 'staging'

// フィーチャーフラグを表すオブジェクト
export const flags = reactive({
  // 例: ドキュメント検索機能を管理するフラグ
  searchDocument: defaultValue,

  // 例: バージョン管理機能を管理するフラグ
  versionControl: defaultValue,
})

// 開発環境ではフィーチャーフラグを window オブジェクトに入れておくことで、
// コンソールから機能のオン・オフを行い、確認をすることができる。
if (import.meta.env.DEV) {
  window.__flags__ = flags
}

フィーチャーフラグのデフォルト値を開発環境やステージングでは true、本番環境では false にすることで、チーム内では開発中の機能を常に確認できるようにしつつ、本番環境では隠すことができます。例えば、ビジネス職の人に開発中の機能を見せる時、それ用のデプロイをしなくてもステージングで見られます。

上記の例は Vite の環境変数を利用しており、他のビルド環境では書き方が異なる場合があることに注意してください。また、import.meta.env.MODE === 'staging' をステージングで true にするには、--mode オプションで明示的に staging モードを指定する必要がある点にも注意が必要です。

リリースの準備ができたらフィーチャーフラグを true にする #

フィーチャーフラグで隠した機能の開発が完了し、リリースができる段階になったら、フィーチャーフラグを true にして本番環境にリリースします。

export const flags = reactive({
  // リリースする機能のフラグを true にする
  searchDocument: true,
  versionControl: defaultValue,
})

リリース後に問題がなければフィーチャーフラグを削除する #

リリースが終わって役目を終えたフィーチャーフラグは削除します。フィーチャーフラグは便利なものですが、存在することでコードが条件分岐によって複雑になることを忘れてはいけません。

// searchDocument のフィーチャーフラグを削除
export const flags = reactive({
  versionControl: defaultValue,
})
<template>
  <div class="app">
    <!-- フィーチャーフラグを削除したため、v-if ディレクティブも削除 -->
    <DocumentSearchForm></DocumentSearchForm>

    <DocumentList></DocumentList>
  </div>
</template>

TypeScript を導入しているプロジェクトであれば、削除したフィーチャーフラグを使っている部分は型エラーになるため、簡単にもれなく削除できるでしょう。そうでない場合も、基本的には条件分岐に直接記述して使うものなので、全文検索で簡単に見つけられます。

フィーチャーフラグが使われている部分が少なく、条件分岐が単純であれば、リリースと同時にフラグを削除しても問題ありません。リリースとフィーチャーフラグの削除のタイミングをずらすのは、フラグを true にしたコードは main にマージされる過程でレビューされており、開発環境やステージングで検証もされていて、比較的安全だからです。

一方、フィーチャーフラグの削除は条件分岐のロジックを読み間違えるなどで事故が起きる可能性があるので、フラグの使われ方が複雑なときは、リリースが終わってから落ち着いた後にゆっくりと削除し、必要があればレビューをするというのが安全です。

以上の開発方法を繰り返すことで、フィーチャーフラグでリリース対象を管理しながら、ブランチを小さく保ち、素早くマージできるようになります。

結論 #

ブランチのマージが大変にならないように、小さいうちに素早くマージしましょう。マージがリリースの都合で難しい場合は、フィーチャーフラグを使うことでリリース対象を管理します。フィーチャーフラグの実装は単なる Boolean 変数と条件分岐ですが、開発工程には大きく良い影響を及ぼします。大きな変更をお祈りデプロイするのではなく、小さな変更をしっかりと検証しながら、何度かに分けてリリースしましょう。