フィーチャーフラグの段階的リリースと消し忘れ負債をClaude Codeで防ぐ
if(flag)を足すだけでは本番で破綻する。フラグの種類分け、環境変数→DB→SaaSの段階実装、段階的ロールアウト、寿命管理までを僕の事故込みで実装解説。
金曜の夜、僕は新しいチェックアウト画面に if (flag) を一行足して、満足して帰りました。
月曜に出社したら、その分岐が15箇所に増えていました。誰がどれを消すのかは決めていない。デフォルト値が true のものと false のものが混在していて、本番で何%のユーザーに新画面が出ているのか、正直もう誰にも説明できませんでした。
これがフィーチャーフラグの落とし穴です。スイッチを足すのは一瞬。でも、足したフラグは作った瞬間から負債になり始めます。
フィーチャーフラグ(機能フラグ)は、デプロイ済みのコードをあとから有効化・無効化する「実行時スイッチ」のこと。難しいのは if (flag) を書くことじゃなくて、どの種類のフラグを、どこに置いて、いつ誰が消すかを先に決めることです。今日はそこを、僕が踏んだ地雷込みで実装まで書きます。
この記事の要点
- フラグは「名前」より「寿命」で分ける。リリース/実験/運用(キルスイッチ)/権限の4種類は管理ルールが全部違う。
- 実装は最初から専用SaaSを入れない。環境変数 → JSON/DB → 専用SaaSの順で、必要になってから格上げする。
- 段階的ロールアウトは「1%から始める」ことじゃなく「増やす条件と戻す条件が決まっている」状態のこと。
- フラグの本番は削除まで。
ownerとremoveAfterを設定に持たせないと、半年後に誰も意味を知らない分岐が残る。 - 計測(どのフラグが効いたか)はA/Bテスト記事の担当。この記事は実装と運用に絞る。
フラグは「寿命」で4つに分けると設計が決まる
最初にやるべきは、コードを書くことじゃありません。そのフラグが何日生きるのかを決めることです。寿命が決まると、デフォルト値も、所有者も、消すタイミングも自動的に決まります。
僕はフラグを次の4種類で分けています。
| 種類 | 寿命 | デフォルト値 | 役割 | 消すタイミング |
|---|---|---|---|---|
| リリース | 数日〜数週間 | false | 未完成機能を隠して段階公開 | 100%公開して安定したら即削除 |
| 実験 | 1〜4週間 | control | A/Bで仮説検証 | 勝者を決めたら勝ち側に固定して削除 |
| 運用(キルスイッチ) | 長寿命 | 機能ON側 | 障害時に重い処理や外部APIを即停止 | 基本残す(監視とセット) |
| 権限 | 恒久 | 厳しい側 | プラン・ロールで機能を出し分け | 機能が廃止されるまで残す |
ここを混ぜると事故ります。たとえばリリースフラグをデフォルト true で作ると、フラグサービスが落ちた瞬間に未完成機能が全員に出ます。逆に権限フラグを実験フラグと同じノリで「とりあえず50%」にすると、課金していないユーザーに有料機能が漏れます。
LaunchDarklyもリリース/実験/キルスイッチの使い分けを公式で整理していますし、Unleashは Define → Develop → Production → Cleanup → Archived というライフサイクルを定義して「古いフラグを残すな」と言っています。種類分けは僕の趣味じゃなくて、運用の前提なんです。
実装は環境変数から。最初からSaaSを入れない
フィーチャーフラグと聞くと、いきなり専用SaaS(LaunchDarklyやUnleash)を契約したくなります。でも、ほとんどのプロジェクトはそこまで要りません。必要になってから格上げするのが正解です。
僕はこの3段階で考えています。
- 環境変数(フラグ1〜3個・全員一律でON/OFFしたいだけ)。
FEATURE_CHECKOUT_V2=trueを読むだけ。デプロイし直すまで変わらないのが弱点。 - JSON/DB(フラグ5〜20個・ユーザーごとに出し分けたい・再デプロイなしで切り替えたい)。設定をJSONファイルかDBに置き、アプリは評価器を通す。この記事の中心はここ。
- 専用SaaS(フラグが数十個・複数チーム・監査ログや承認フローが必要)。OpenFeatureの考え方で抽象化しておけば、ここへの移行が楽になる。
大事なのは、最初から差し替えられる形にしておくこと。OpenFeatureは「評価API」と「プロバイダー」を分け、アプリ側は キー・デフォルト値・評価コンテキスト だけを扱います。この形にしておけば、中身が環境変数でもSaaSでも、呼び出し側のコードは変わりません。
コピペで動く:種類と寿命を持つ最小評価器
説明より動かしたほうが早いです。1ファイルで動く最小の評価器を作ります。ポイントは、設定に kind(種類)・owner(持ち主)・removeAfter(削除期限) を持たせていること。ここがあるだけで、あとで「これ誰の何のフラグ?」が消えます。
flag-demo.ts として保存して npx tsx flag-demo.ts で動きます。
type FlagValue = boolean | string | number;
type FlagKind = "release" | "experiment" | "kill_switch" | "permission";
type Plan = "free" | "pro" | "enterprise";
type Role = "user" | "admin";
type Operator = "equals" | "in";
// 評価に使う「誰なのか」の情報。targetingKeyは割合分けの軸になる
type FlagContext = {
targetingKey: string;
plan: Plan;
country: string;
role: Role;
appVersion: string;
};
type FlagRule = {
attribute: keyof Omit<FlagContext, "targetingKey">;
operator: Operator;
values: string[];
value: FlagValue;
percentage?: number; // この割合のユーザーにだけ適用
};
type FlagConfig = {
key: string;
kind: FlagKind;
enabled: boolean;
defaultValue: FlagValue; // どのルールにも当たらなかったとき
offValue: FlagValue; // enabled=false のときに倒す安全側
owner: string; // 持ち主(ここが空のフラグを作らない)
removeAfter?: string; // 削除期限。リリース/実験には必須にする
rules: FlagRule[];
};
const registry: Record<string, FlagConfig> = {
// リリース:管理者には全部出す。Pro/Enterpriseの25%に段階公開
checkout_v2_release: {
key: "checkout_v2_release",
kind: "release",
enabled: true,
defaultValue: false,
offValue: false,
owner: "growth-platform",
removeAfter: "2026-07-15",
rules: [
{ attribute: "role", operator: "equals", values: ["admin"], value: true },
{
attribute: "plan",
operator: "in",
values: ["pro", "enterprise"],
value: true,
percentage: 25,
},
],
},
// 権限:有料プランだけに出す恒久フラグ。割合は使わない
ai_assistant_access: {
key: "ai_assistant_access",
kind: "permission",
enabled: true,
defaultValue: false,
offValue: false,
owner: "billing",
rules: [
{ attribute: "plan", operator: "in", values: ["pro", "enterprise"], value: true },
],
},
// キルスイッチ:障害時に推薦枠を即OFFにする長寿命フラグ
recommendations_enabled: {
key: "recommendations_enabled",
kind: "kill_switch",
enabled: true,
defaultValue: true,
offValue: false,
owner: "sre",
rules: [],
},
};
// targetingKeyから安定したbucket(0-99)を作る。同じ人は毎回同じ箱に入る
function bucketFor(flagKey: string, targetingKey: string): number {
const input = `${flagKey}:${targetingKey}`;
let hash = 0;
for (const char of input) {
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
}
return hash % 100;
}
function ruleMatches(flagKey: string, rule: FlagRule, ctx: FlagContext): boolean {
const actual = String(ctx[rule.attribute]);
const matched =
rule.operator === "equals"
? actual === rule.values[0]
: rule.values.includes(actual);
if (!matched) return false;
if (rule.percentage === undefined) return true;
return bucketFor(flagKey, ctx.targetingKey) < rule.percentage;
}
export function evaluateFlag<T extends FlagValue = FlagValue>(
key: string,
ctx: FlagContext,
): T {
const flag = registry[key];
if (!flag) return false as T; // 未定義フラグは必ず安全側へ倒す
if (!flag.enabled) return flag.offValue as T;
for (const rule of flag.rules) {
if (ruleMatches(flag.key, rule, ctx)) return rule.value as T;
}
return flag.defaultValue as T;
}
const demoContexts: FlagContext[] = [
{ targetingKey: "user_001", plan: "pro", country: "JP", role: "user", appVersion: "1.8.0" },
{ targetingKey: "user_002", plan: "free", country: "BR", role: "admin", appVersion: "1.8.0" },
];
for (const ctx of demoContexts) {
console.log(ctx.targetingKey, {
checkout: evaluateFlag<boolean>("checkout_v2_release", ctx),
aiAssistant: evaluateFlag<boolean>("ai_assistant_access", ctx),
recommendations: evaluateFlag<boolean>("recommendations_enabled", ctx),
});
}
本番ではこの registry を管理画面やJSONファイル、DBから読み込みます。コードはそのままで、データだけ差し替わるイメージです。覚えておくべきは3つだけ。未定義フラグは安全側に倒す・割合分けは安定した targetingKey を使う・owner と removeAfter を設定に必ず持たせる。この3つを守れば、冒頭の「15分岐の迷宮」は起きません。
段階的ロールアウトは「戻す条件」を先に書く
段階的リリース(カナリアリリース)というと「まず1%に出す」話だと思われがちですが、本質は割合じゃありません。増やす条件と戻す条件が、出す前に決まっているかどうかです。
僕はロールアウトの前に、必ずこの3つの数字を先に書きます。
- 増やす条件: エラー率が前週と同等、p95応答が悪化していない → 翌日 5% → 25% → 50% → 100%。
- 戻す条件(ガードレール): 5xx率が基準の2倍、決済失敗率が1.5倍、問い合わせ急増 → 即
offValueに倒す。 - 観測する指標: 露出ログ(誰がどのフラグ値を見たか)、主要指標(狙った行動が増えたか)、ガードレール(壊れていないか)。
露出ログがないと、あとで「新画面を見た人」と「見ていない人」を比較できません。割合を上げる前に、まず露出を記録する仕組みを入れます。
type FlagExposure = {
flagKey: string;
value: FlagValue;
targetingKey: string;
route: string;
evaluatedAt: string;
};
// 露出ログ。本番ではconsoleの代わりに分析基盤へ送る
export function trackFlagExposure(event: FlagExposure): void {
console.log(
JSON.stringify({ event_name: "feature_flag_exposure", ...event }),
);
}
Unleashの段階的ロールアウトは割合・stickiness・制約を組み合わせますし、LaunchDarklyの guarded rollout はメトリクスを見て回帰したら自動停止する発想です。自作でも考え方は同じで、人間が監視ダッシュボードに張り付かなくても、基準を割ったら戻せる状態を先に作ります。
監視の中身はサービスによって違います。チェックアウトなら5xx率・決済失敗率・問い合わせ件数。ブログの収益枠ならクリック率だけじゃなく読了率・直帰・LCP。AI機能の解放ならトークン費用・レイテンシ・ユーザー単価。ここの設計は構造化ログとアラートの記事に詳しく書いたので、ロールアウトとセットで読むと監視まで一直線になります。
サーバー評価とクライアント評価を混ぜない
権限フラグでいちばんやりがちな事故が、クライアントだけで有料機能を隠すこと。ボタンを display:none にしても、APIが開いていれば誰でも叩けます。フラグはUXのスイッチであって、認可(認証・権限)の代わりにはなりません。
線引きはシンプルです。課金・権限・在庫・API呼び出し量のように悪用されると困る判定はサーバーで評価。すでに許可済みのUI表示や文言、低リスクなレイアウト切り替えだけクライアントで評価。ブラウザに全ルールや秘密の条件を丸ごと渡す設計は避けます。
type User = {
id: string;
plan: "free" | "pro" | "enterprise";
role: "user" | "admin";
};
type RequestLike = { headers: { get(name: string): string | null } };
// サーバー側で評価コンテキストを組み立てる
export function buildFlagContext(user: User, request: RequestLike): FlagContext {
return {
targetingKey: user.id,
plan: user.plan,
role: user.role,
country: request.headers.get("x-country") ?? "US",
appVersion: process.env.APP_VERSION ?? "dev",
};
}
// クライアントへ渡すのは「評価し終えた結果」だけ。ルールは渡さない
export function getServerFlagSnapshot(context: FlagContext) {
return {
aiAssistant: evaluateFlag<boolean>("ai_assistant_access", context),
checkoutV2: evaluateFlag<boolean>("checkout_v2_release", context),
};
}
クライアント側は、受け取ったスナップショットを表示するだけにします。
type UiFlags = { checkoutV2: boolean };
export function CheckoutButton({ flags }: { flags: UiFlags }) {
const label = flags.checkoutV2 ? "新しい決済へ進む" : "決済へ進む";
return <a href="/checkout">{label}</a>;
}
Claude Codeに依頼するときも「権限判定はサーバー、UI文言だけクライアント」と一行入れるだけで、危ない実装がかなり減ります。
僕がやらかしたフラグの失敗3つ
正直に書きます。フラグ運用は、最初ほぼ全部踏みました。
ひとつ目は、毎回ランダムに割り当てた実験。ページを更新するたびにAとBが入れ替わって、露出ログもコンバージョンも全部信用できなくなりました。Math.random() じゃなく targetingKey から安定したbucketを作る。同じ人は毎回同じ箱に入る。これだけで数字が嘘をつかなくなりました。
ふたつ目は、デフォルト値 true のままリリースフラグを置いたこと。キー名を一文字タイプミスしただけで、評価器が未定義フラグ扱いにして…ならまだ良くて、僕の場合はデフォルトが true だったので、未完成機能が全員に出ました。以来、未検証のリリースフラグは必ず false、キルスイッチは offValue を明示しています。
みっつ目は、冒頭の消し忘れ。100%公開したリリースフラグを消さずに放置したら、半年後に「この分岐、消していいんだっけ?」と誰も答えられない化石になりました。Unleashが stale state を警告するのはこのためです。removeAfter を過ぎたフラグは、機械的に棚卸しの対象にします。
クリーンアップまでが実装。消すPRを最初に予約する
フラグは作った瞬間から負債化が始まります。だから僕は、フラグを足すPRの説明欄に、消すPRの予定を最初から書くようにしました。
種類ごとの線引きはこうです。リリースフラグは100%公開後に削除。実験フラグは勝敗を決めたら勝ち側のコードに固定して削除。キルスイッチと権限フラグだけは、運用手順と監視に残す。
PRテンプレートに次の4項目を入れるだけで、半年後の自分(と同僚)が救われます。
## フィーチャーフラグ チェック
- key: checkout_v2_release
- kind: release
- owner: growth-platform
- removeAfter: 2026-07-15
- 監視指標: 5xx率 / 決済失敗率 / 問い合わせ件数
- 戻す条件: 5xx率が基準の2倍で offValue へ
- 削除PRの予定: 100%公開の翌スプリントで分岐を削除
ここまでテンプレに乗せておけば、フラグの棚卸しが「気合い」じゃなく「手順」になります。
Claude Codeにフラグ実装を安全に頼む依頼文
Claude Codeはファイルを読んで変更し、テストも実行できます。Anthropicのベストプラクティスにもある通り、検証方法を渡すほど成果物の質が上がります。ふわっと「フラグ実装して」と頼むとUI分岐だけ返ってきますが、所有範囲・失敗時の安全側・確認コマンドまで書くと、運用できる設計に寄ります。
このリポジトリにフィーチャーフラグの仕組みを追加してください。
目的は checkout_v2_release の段階的ロールアウトです。
制約:
- 権限と課金に関わる判定はサーバー側で評価する
- 未定義フラグは安全側の false へ倒す
- targetingKey で安定した割合割り当てにする(Math.random は使わない)
- 設定に kind / owner / removeAfter を含める
- 関係ないファイルは変更しない
必要な出力:
- 最小の flag registry と evaluateFlag 関数
- 露出ログのイベント型
- リリース / 実験 / キルスイッチ / 権限の例を1つずつ
- 失敗例とロールバック手順
- 実行したテストコマンド
レビューを頼むときは、観点を先に固定すると指摘が散りません。
フィーチャーフラグ実装をレビューしてください。
観点は、デフォルト値の安全性、サーバー/クライアント境界、
targetingKey の安定性、露出ログの漏れ、removeAfter の有無です。
重大度順に並べ、修正が必要なファイルだけ提案してください。
この2つを使うだけで、Claude Codeが単なる if (flag) ではなく、寿命まで意識したフラグ設計を出してくれます。
よくある質問
Q. フィーチャーフラグとA/Bテストは何が違いますか? フラグは「機能をON/OFFする実装の仕組み」、A/Bテストは「どちらが良いか数字で決める計測の手法」です。実験フラグはA/Bテストの土台に使いますが、計測・有意差・勝敗判定の話はA/Bテストの記事が担当です。この記事はフラグの実装と運用に絞っています。
Q. 最初から専用SaaSを入れるべきですか? フラグが数個なら不要です。環境変数 → JSON/DB → 専用SaaSの順で、ユーザー出し分けや承認フローが必要になってから格上げします。OpenFeatureで抽象化しておけば、あとで中身を差し替えても呼び出し側は変わりません。
Q. デフォルト値は true と false どちらにすべきですか?
未検証のリリース・実験フラグは false(安全側)です。フラグサービス障害やキー名のタイプミスで、未完成機能が全員に出る事故を防げます。キルスイッチは「普段ON・障害時OFF」なので defaultValue: true と offValue: false を明示します。
Q. 段階的ロールアウトの割合はどう刻めばいいですか? 僕は admin → 1% → 5% → 25% → 50% → 100% で進め、各段階でガードレール指標が悪化していないことを確認してから次へ進みます。割合の数字より、戻す条件を出す前に決めておくことのほうが大事です。
Q. 古いフラグはいつ消せばいいですか?
リリースは100%公開して安定したら即削除、実験は勝敗確定後に勝ち側へ固定して削除です。removeAfter を設定に持たせ、期限を過ぎたフラグをPRレビューやCIで棚卸しすると、消し忘れ負債が溜まりません。
実際に試した結果
この記事の評価器は、TypeScriptの最小実装としてそのまま動く形で確認しました。特に targetingKey を変えない限り同じユーザーが同じbucketに入ること、未知のフラグが安全側の false に倒れることは手元で見ています。
冒頭の「15分岐の迷宮」をやってから、僕がフラグで見る場所は変わりました。賢い切り替えロジックより、まず owner と removeAfter が入っているかを見ます。種類を4つに分けて、リリースフラグを1つ・キルスイッチを1つだけで小さく始める。それだけで、消し忘れ負債はほぼ止まりました。
フラグの運用が固まってきたら、効果を数字で判断するA/Bテストとアナリティクス実装もセットで読むと、実装から計測まで一本につながります。チーム導入やレビュー体制まで整えたいならClaude Code導入相談、個人で手を動かして学ぶなら教材一覧から始めてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。