取り組みの背景 — リリースの恐怖は「コード」ではなく「タイミング」から生まれる
2014年に個人アプリ開発を始めてから 10 年以上、累計 5,000 万ダウンロードを超えるアプリを App Store と Google Play で運用してきました。その中で何度も思い知らされたのは、リリースで人を不安にさせる正体はコードの中身ではなく、コードを「いつ」「誰に」見せるかを設計できていないことだという事実です。
新しい壁紙生成ロジックを全ユーザーに一斉公開した翌朝、特定の端末で Out-of-Memory がじわじわと増え始めた経験があります。AdMob のメディエーション順序を全体に変更した直後、収益の主要セグメントだけ ARPDAU が 30% 落ちたこともありました。コード自体は十分テストされていました。問題は、影響を受けるユーザーの「面」が広すぎて、異常に気づいた時点ではもう手遅れだったことです。
Feature Flags(機能フラグ)は、この「コードのデプロイ」と「ユーザーへの公開」を分離する設計パターンです。コードを本番にデプロイしても、フラグが ON になるまでユーザーには何も起きません。これだけで、段階的なロールアウト、カナリアリリース、A/B テスト、緊急停止ボタンといった選択肢が一気に手に入ります。
Progressive Delivery はそこをさらに推し進め、ロールアウト中のメトリクスを継続的に観測し、しきい値を割ったら自動でフラグを OFF に戻す仕組みまでを含む戦略を指します。Antigravity の AI エージェントは、この一連のパイプラインを「コード生成」「リスク分析」「監視」「ロールバック判定」の各レイヤーで支援してくれる存在として、私のリリース運用に深く食い込んでいます。
対象読者 : CI/CD の基本を理解しており、本番環境でのリリース戦略を高度化したい中〜上級エンジニア。個人開発で複数アプリを並行運用していて、リリース事故が事業全体に波及するリスクを身近に感じている方には特に響く内容を意識しました。
この記事で扱う技術スタック :
TypeScript / Node.js
Next.js(App Router)
Cloudflare Workers
Antigravity AI エージェント
個人開発で 5,000 万 DL を越えるアプリを運用して気づいた、フラグ運用の現実
書籍やカンファレンス資料で語られる Feature Flags の解説は、組織で複数のチームが同時に開発している状況を前提にしていることが多い印象です。私のように個人開発で 4 〜 6 本のアプリと 6 つのサイトを並行運用していると、見えてくる景色が少し違ってきます。ここでは個人〜小規模チーム視点で、フラグを入れて初めて分かった「現実」を共有させてください。
第一に、フラグの最大の価値は「事故を防ぐこと」ではなく「事故が起きたときに 1 ボタンで止められる安心感」です。私の壁紙系アプリでは、新しいカテゴリ追加や画像生成ロジックの変更を Antigravity に頼んだあと、必ずリリースフラグで包んでから本番に出す運用にしています。問題が起きたら、Cloudflare KV のフラグ値を false に切り替えるだけで、アプリ再申請も再デプロイも不要で挙動を巻き戻せます。AdMob の SDK 統合変更時など、Apple や Google の審査を絡める余裕がない状況で、この「巻き戻しコストの低さ」が経営リスクを直接下げてくれます。
第二に、フラグは「決断の先送り」を可能にしてくれます。AdMob のメディエーション順序や、引き寄せ系アプリのプッシュ通知文面の A/B テストなど、最終判断のためのデータが揃っていない段階でも、フラグ越しに少数ユーザーへ出してメトリクスを取り、後から落ち着いて決められるようになりました。これが個人開発で一番大きな変化です。仮説を抱えたまま全体公開しなくて済むので、リリースの心理的負担が大きく下がります。
第三に、フラグは増えれば必ず腐ります。私の場合、3 ヶ月ほど運用しただけで「これ、なんのためのフラグだっけ」というものが 10 個以上溜まりました。フラグの寿命管理は人間の意思では絶対にやり切れません。後ほど触れる Antigravity による週次の自動監査スクリプトに任せて、はじめて運用に耐えるレベルになりました。
Feature Flags の設計パターン — 4つの基本型を理解する
Feature Flags は用途に応じて大きく4つのパターンに分類されます。Antigravity にこれらのパターンを説明し、プロジェクトに合った実装を生成させることで、開発速度を大幅に向上できます。
リリースフラグ(Release Flags) は最も一般的なパターンです。新機能をコードベースに含めたままデプロイし、フラグで表示を制御します。開発が完了し安定したらフラグを削除します。寿命が短く、数日〜数週間で除去されるのが理想です。
実験フラグ(Experiment Flags) は A/B テストに使います。ユーザーをランダムにグループ分けし、異なるバリアントを表示してコンバージョン率やエンゲージメントを比較します。統計的に有意な結果が出たら、勝者バリアントを採用してフラグを除去します。
オペレーションフラグ(Ops Flags) は、システムの動作を動的に変更するためのフラグです。たとえば、負荷が高いときに特定の機能を一時的に無効化するキルスイッチとして機能します。長期間残ることもあります。
パーミッションフラグ(Permission Flags) は、特定のユーザーグループにのみ機能を提供するためのフラグです。プレミアム機能のゲーティングや、ベータテスターへの先行公開に使います。
// feature-flags.ts — 型安全な Feature Flag システムの基盤設計
// Antigravity エージェントに「型安全な Feature Flag マネージャーを作って」と指示すると
// このような構造を自動生成できます
type FlagType = 'release' | 'experiment' | 'ops' | 'permission' ;
interface FeatureFlag < T = boolean > {
key : string ;
type : FlagType ;
defaultValue : T ;
description : string ;
owner : string ; // 責任者(フラグの寿命管理のため)
createdAt : string ;
expiresAt ?: string ; // リリースフラグは必ず有効期限を設定
}
interface FlagEvaluationContext {
userId : string ;
userAttributes : Record < string , string | number | boolean >;
environment : 'production' | 'staging' | 'development' ;
percentage ?: number ; // 段階ロールアウト用(0-100)
}
// フラグ定義の一元管理
const FLAGS = {
newCheckoutFlow: {
key: 'new-checkout-flow' ,
type: 'release' as FlagType ,
defaultValue: false ,
description: '新しい決済フローUI' ,
owner: 'payment-team' ,
createdAt: '2026-03-30' ,
expiresAt: '2026-04-30' , // 1ヶ月後に必ず評価
},
pricingExperiment: {
key: 'pricing-experiment' ,
type: 'experiment' as FlagType ,
defaultValue: 'control' , // 'control' | 'variant-a' | 'variant-b'
description: '料金ページのA/Bテスト' ,
owner: 'growth-team' ,
createdAt: '2026-03-30' ,
},
emergencyReadOnly: {
key: 'emergency-read-only' ,
type: 'ops' as FlagType ,
defaultValue: false ,
description: '緊急時の読み取り専用モード' ,
owner: 'sre-team' ,
createdAt: '2026-03-30' ,
},
} as const ;
// 出力例:
// FLAGS.newCheckoutFlow.key → 'new-checkout-flow'
// FLAGS.pricingExperiment.type → 'experiment'
Antigravity のエージェントに「このフラグ定義を元に、React コンポーネントで使えるカスタムフックを生成して」と指示すると、useFeatureFlag() フックを型安全に自動生成してくれます。
Antigravity エージェントによる Feature Flag コードの自動生成
Antigravity の強力な AI エージェントを使えば、Feature Flag の定型的なコード(フラグ評価ロジック、React フック、サーバーサイドミドルウェア)を効率的に生成できます。ここでは実践的なワークフローを紹介します。
まず、AGENTS.md にフラグ管理のルールを定義しておく点が肝心です。エージェントがフラグを作成する際に一貫したパターンに従うようになります。
<!-- .antigravity/rules/feature-flags.md -->
# Feature Flag ルール
## 命名規則
- ケバブケースを使用: `new-checkout-flow`
- プレフィックスでタイプを示す: `exp-` (実験), `ops-` (運用), `perm-` (権限)
- リリースフラグはプレフィックスなし
## 必須フィールド
- すべてのフラグに `owner` と `createdAt` を設定
- リリースフラグには `expiresAt` を必ず設定(最長30日)
- 実験フラグには `variants` と `metrics` を定義
## フラグの除去
- 有効期限を過ぎたフラグは、次のスプリントで必ず除去する
- フラグ除去時は関連するテストコードも同時に除去する
次に、Antigravity エージェントに具体的なフラグ実装を依頼します。
// hooks/useFeatureFlag.ts — Antigravity が生成するカスタムフック
// エージェントへの指示: 「Cloudflare KV をバックエンドに使う Feature Flag フックを作って」
import { useEffect, useState } from 'react' ;
interface FlagConfig {
key : string ;
defaultValue : boolean ;
userId ?: string ;
}
export function useFeatureFlag ({ key , defaultValue , userId } : FlagConfig ) : {
enabled : boolean ;
loading : boolean ;
} {
const [ enabled , setEnabled ] = useState (defaultValue);
const [ loading , setLoading ] = useState ( true );
useEffect (() => {
const controller = new AbortController ();
async function fetchFlag () {
try {
const params = new URLSearchParams ({ key });
if (userId) params. append ( 'userId' , userId);
const res = await fetch ( `/api/flags?${ params }` , {
signal: controller.signal,
});
if (res.ok) {
const data = await res. json ();
setEnabled (data.enabled);
}
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError' ) return ;
console. warn ( `Flag evaluation failed for ${ key }, using default` );
} finally {
setLoading ( false );
}
}
fetchFlag ();
return () => controller. abort ();
}, [key, userId]);
return { enabled, loading };
}
// コンポーネントでの使用例:
// const { enabled: showNewUI, loading } = useFeatureFlag({
// key: 'new-checkout-flow',
// defaultValue: false,
// userId: session.userId,
// });
// if (loading) return <Skeleton />;
// return showNewUI ? <NewCheckout /> : <LegacyCheckout />;
Cloudflare Workers で構築するフラグ評価エンジン
Feature Flags の評価をエッジで行うことで、レイテンシを最小化できます。Cloudflare Workers + KV を使ったフラグ評価エンジンの実装を、Antigravity エージェントと共に構築しましょう。
// api/flags/route.ts — エッジで動作するフラグ評価 API
// Antigravity にこのファイルの雛形を生成させ、ビジネスロジックを追加していく
import { NextRequest, NextResponse } from 'next/server' ;
import { getCloudflareContext } from '@opennextjs/cloudflare' ;
interface FlagRule {
enabled : boolean ;
percentage ?: number ; // 段階ロールアウト: 0-100
allowList ?: string []; // 特定ユーザーのホワイトリスト
startDate ?: string ; // スケジュールリリース開始
endDate ?: string ; // スケジュールリリース終了
}
function hashUserId ( userId : string , flagKey : string ) : number {
// 決定論的なハッシュ: 同じユーザーには常に同じ結果を返す
let hash = 0 ;
const input = `${ userId }:${ flagKey }` ;
for ( let i = 0 ; i < input. length ; i ++ ) {
const char = input. charCodeAt (i);
hash = ((hash << 5 ) - hash) + char;
hash = hash & hash; // 32bit整数に変換
}
return Math. abs (hash) % 100 ;
}
export async function GET ( request : NextRequest ) {
const { searchParams } = new URL (request.url);
const key = searchParams. get ( 'key' );
const userId = searchParams. get ( 'userId' );
if ( ! key) {
return NextResponse. json ({ error: 'key is required' }, { status: 400 });
}
try {
const { env } = await getCloudflareContext ();
const raw = await env. FLAGS_KV . get ( `flag:${ key }` );
if ( ! raw) {
return NextResponse. json ({ enabled: false , source: 'default' });
}
const rule : FlagRule = JSON . parse (raw);
// ホワイトリストチェック
if (userId && rule.allowList?. includes (userId)) {
return NextResponse. json ({ enabled: true , source: 'allowList' });
}
// スケジュールチェック
const now = new Date ();
if (rule.startDate && now < new Date (rule.startDate)) {
return NextResponse. json ({ enabled: false , source: 'scheduled' });
}
if (rule.endDate && now > new Date (rule.endDate)) {
return NextResponse. json ({ enabled: false , source: 'expired' });
}
// 段階ロールアウト
if (rule.percentage !== undefined && userId) {
const bucket = hashUserId (userId, key);
const enabled = bucket < rule.percentage;
return NextResponse. json ({ enabled, source: 'percentage' , bucket });
}
return NextResponse. json ({ enabled: rule.enabled, source: 'rule' });
} catch (err) {
console. error ( 'Flag evaluation error:' , err);
return NextResponse. json ({ enabled: false , source: 'error' });
}
}
// KV に格納するフラグデータの例:
// key: "flag:new-checkout-flow"
// value: {"enabled":true,"percentage":10,"allowList":["user-beta-1"]}
// → 全体の10%のユーザー + ベータテスターに新UIを表示
このエンジンの特徴は、hashUserId 関数による決定論的な振り分けです。同じユーザーには常に同じバリアントが表示されるため、A/B テストの結果が安定します。
カナリアリリースの実装 — 段階的に安全にロールアウトする
カナリアリリースは、新バージョンを少数のユーザーにまず公開し、問題がなければ徐々に対象を広げていく手法です。Antigravity エージェントを使って、自動的にロールアウト率を制御するパイプラインを構築します。
// scripts/canary-rollout.ts — Antigravity エージェントが管理するロールアウトスクリプト
// 「カナリアリリースの自動ロールアウトスクリプトを作って」と指示
interface RolloutStage {
percentage : number ;
duration : string ; // この段階を維持する時間
successCriteria : {
maxErrorRate : number ; // 許容エラー率(%)
maxLatencyP99 : number ; // 許容P99レイテンシ(ms)
minSuccessRate : number ; // 最低成功率(%)
};
}
const ROLLOUT_PLAN : RolloutStage [] = [
{
percentage: 1 ,
duration: '30m' ,
successCriteria: { maxErrorRate: 0.1 , maxLatencyP99: 500 , minSuccessRate: 99.9 },
},
{
percentage: 5 ,
duration: '1h' ,
successCriteria: { maxErrorRate: 0.5 , maxLatencyP99: 800 , minSuccessRate: 99.5 },
},
{
percentage: 25 ,
duration: '2h' ,
successCriteria: { maxErrorRate: 1.0 , maxLatencyP99: 1000 , minSuccessRate: 99.0 },
},
{
percentage: 50 ,
duration: '4h' ,
successCriteria: { maxErrorRate: 1.0 , maxLatencyP99: 1000 , minSuccessRate: 99.0 },
},
{
percentage: 100 ,
duration: 'permanent' ,
successCriteria: { maxErrorRate: 1.0 , maxLatencyP99: 1200 , minSuccessRate: 98.5 },
},
];
async function evaluateMetrics ( flagKey : string ) : Promise <{
errorRate : number ;
latencyP99 : number ;
successRate : number ;
}> {
// 実際にはモニタリングツール(Grafana, Datadog等)のAPIから取得
// Antigravity エージェントが使用中のモニタリングスタックに合わせて実装を生成
const response = await fetch ( `https://monitoring.example.com/api/metrics` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
query: `feature_flag{key="${ flagKey }"}` ,
range: '15m' ,
}),
});
return response. json ();
}
async function updateFlagPercentage ( flagKey : string , percentage : number ) : Promise < void > {
// Cloudflare KV のフラグ値を更新
await fetch ( `https://api.cloudflare.com/client/v4/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/flag:${ flagKey }` , {
method: 'PUT' ,
headers: {
'Authorization' : `Bearer YOUR_API_KEY` ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({ enabled: true , percentage }),
});
}
async function rollback ( flagKey : string , reason : string ) : Promise < void > {
console. error ( `🚨 ROLLBACK: ${ flagKey } — ${ reason }` );
await updateFlagPercentage (flagKey, 0 );
// Slack / Discord 通知
// Antigravity エージェントに「Slack通知機能を追加して」と依頼すると自動実装
}
async function executeRollout ( flagKey : string ) : Promise < void > {
for ( const stage of ROLLOUT_PLAN ) {
console. log ( `📊 Stage: ${ stage . percentage }% rollout` );
await updateFlagPercentage (flagKey, stage.percentage);
// 指定時間待機してメトリクスを収集
const waitMs = parseDuration (stage.duration);
if (waitMs === Infinity ) break ; // permanent = 最終段階
await new Promise ( resolve => setTimeout (resolve, waitMs));
// メトリクス評価
const metrics = await evaluateMetrics (flagKey);
if (metrics.errorRate > stage.successCriteria.maxErrorRate) {
await rollback (flagKey, `Error rate ${ metrics . errorRate }% exceeds threshold ${ stage . successCriteria . maxErrorRate }%` );
return ;
}
if (metrics.latencyP99 > stage.successCriteria.maxLatencyP99) {
await rollback (flagKey, `P99 latency ${ metrics . latencyP99 }ms exceeds threshold ${ stage . successCriteria . maxLatencyP99 }ms` );
return ;
}
if (metrics.successRate < stage.successCriteria.minSuccessRate) {
await rollback (flagKey, `Success rate ${ metrics . successRate }% below threshold ${ stage . successCriteria . minSuccessRate }%` );
return ;
}
console. log ( `✅ Stage ${ stage . percentage }% passed — metrics OK` );
}
console. log ( '🎉 Rollout complete — 100% traffic' );
}
function parseDuration ( d : string ) : number {
if (d === 'permanent' ) return Infinity ;
const match = d. match ( / ^ ( \d + )(m | h) $ / );
if ( ! match) throw new Error ( `Invalid duration: ${ d }` );
const [, num , unit ] = match;
return parseInt (num) * (unit === 'h' ? 3600000 : 60000 );
}
// 実行例:
// executeRollout('new-checkout-flow');
// → 1% → 5% → 25% → 50% → 100% と段階的にロールアウト
// → 各段階でメトリクスを自動チェック、異常があれば即座にロールバック
重要なのは、各段階の successCriteria を事前に定義しておくことです。Antigravity エージェントに「過去のデプロイメトリクスを分析して適切な閾値を提案して」と依頼すると、プロジェクトの実績に基づいた現実的な数値を提案してくれます。
A/B テストの設計と統計的検定
Feature Flags のもう一つの強力な活用法が A/B テストです。UI の変更がコンバージョン率に与える影響を、統計的に正しく測定する方法を解説します。
// lib/ab-testing.ts — A/Bテスト管理モジュール
// Antigravity に「統計的検定付きのA/Bテストライブラリを実装して」と指示
interface Experiment {
key : string ;
variants : string []; // ['control', 'variant-a', 'variant-b']
trafficAllocation : number ; // 実験に参加するトラフィックの割合(0-100)
metrics : string []; // 追跡するメトリクス名
}
interface ExperimentResult {
variant : string ;
sampleSize : number ;
conversions : number ;
conversionRate : number ;
}
// Z検定による統計的有意性の判定
function calculateZScore (
control : ExperimentResult ,
treatment : ExperimentResult
) : { zScore : number ; pValue : number ; significant : boolean } {
const p1 = control.conversionRate;
const p2 = treatment.conversionRate;
const n1 = control.sampleSize;
const n2 = treatment.sampleSize;
// プールされた比率
const pooled = (control.conversions + treatment.conversions) / (n1 + n2);
const se = Math. sqrt (pooled * ( 1 - pooled) * ( 1 / n1 + 1 / n2));
if (se === 0 ) return { zScore: 0 , pValue: 1 , significant: false };
const zScore = (p2 - p1) / se;
// 両側検定の p値(近似計算)
const pValue = 2 * ( 1 - normalCDF (Math. abs (zScore)));
return {
zScore,
pValue,
significant: pValue < 0.05 , // 95% 信頼水準
};
}
function normalCDF ( x : number ) : number {
// 標準正規分布の累積分布関数(Abramowitz & Stegun 近似)
const t = 1 / ( 1 + 0.2316419 * Math. abs (x));
const d = 0.3989422804014327 ;
const p =
d * Math. exp ( - x * x / 2 ) *
(t * ( 0.3193815 + t * ( - 0.3565638 + t * ( 1.781478 + t * ( - 1.821256 + t * 1.330274 )))));
return x > 0 ? 1 - p : p;
}
// 必要サンプルサイズの事前計算
function requiredSampleSize (
baselineRate : number , // 現在のコンバージョン率
minimumDetectableEffect : number , // 検出したい最小効果量
power : number = 0.8 , // 検出力(通常80%)
alpha : number = 0.05 // 有意水準(通常5%)
) : number {
const zAlpha = 1.96 ; // α=0.05 の z値
const zBeta = 0.842 ; // β=0.2(power=0.8)の z値
const p1 = baselineRate;
const p2 = baselineRate + minimumDetectableEffect;
const pBar = (p1 + p2) / 2 ;
const n = Math. ceil (
(Math. pow (zAlpha + zBeta, 2 ) * (p1 * ( 1 - p1) + p2 * ( 1 - p2))) /
Math. pow (p2 - p1, 2 )
);
return n;
}
// 出力例:
// requiredSampleSize(0.03, 0.005)
// → 約14,752(各バリアントに必要なサンプル数)
// 現在のCVR 3% で、0.5%ポイントの改善を検出するには
// 各バリアント約15,000サンプルが必要
Antigravity エージェントにプロジェクトの現在のコンバージョン率を伝え、「必要なサンプルサイズと実験期間を見積もって」と依頼すると、実験計画を自動で作成してくれます。これにより、統計的に意味のない実験を早期に打ち切る無駄を防げます。
AI 駆動のリリースリスク分析 — デプロイ前に問題を予測する
Antigravity エージェントの真価が発揮されるのが、リリース前のリスク分析です。コード差分を分析し、過去の障害パターンと照合して、リリースリスクを事前に評価するワークフローを構築します。
// scripts/release-risk-analysis.ts — AI駆動のリリースリスク分析
// Antigravity の Agent モードで実行し、コードベース全体を考慮した分析を行う
interface RiskFactor {
category : 'code' | 'dependency' | 'config' | 'database' | 'infrastructure' ;
severity : 'low' | 'medium' | 'high' | 'critical' ;
description : string ;
mitigation : string ;
}
interface ReleaseRiskReport {
overallRisk : 'low' | 'medium' | 'high' | 'critical' ;
factors : RiskFactor [];
recommendedRolloutStrategy : string ;
flagsToCreate : string [];
testsCoverage : {
newCode : number ; // 新規コードのテストカバレッジ
modifiedCode : number ; // 変更コードのテストカバレッジ
};
}
// Antigravity エージェントへの指示テンプレート(AGENTS.md に記載)
const RISK_ANALYSIS_PROMPT = `
## リリースリスク分析タスク
以下の観点でコード差分を分析し、リスクレポートを生成してください:
1. **コードリスク**
- 変更の影響範囲(ファイル数、関数数)
- 複雑度の変化(循環的複雑度)
- エラーハンドリングの網羅性
- 型安全性の確認
2. **依存関係リスク**
- 新しい外部依存の追加
- メジャーバージョンアップデート
- セキュリティアドバイザリの有無
3. **設定変更リスク**
- 環境変数の追加・変更
- インフラ設定の変更
- Feature Flags の追加
4. **データベースリスク**
- マイグレーションの有無
- 破壊的スキーマ変更の有無
- データバックフィルの必要性
5. **推奨ロールアウト戦略**
- リスクレベルに応じた段階ロールアウト計画
- 作成すべき Feature Flags のリスト
- ロールバック手順の提案
` ;
// GitHub Actions でリスク分析を自動実行する設定例
// .github/workflows/release-risk.yml の内容を
// Antigravity エージェントに生成させる
実際のワークフローでは、プルリクエストが作成されるたびに Antigravity エージェントがコード差分を分析し、リスクレポートをコメントとして投稿します。これにより、レビュアーはリスクの高い変更に集中でき、レビュー効率が大幅に向上します。
自動ロールバック機構の実装
Progressive Delivery の最後のピースが、問題検出時の自動ロールバックです。メトリクスの異常を検知したら、人間の介入なしに即座にロールバックする仕組みを構築します。
// lib/auto-rollback.ts — メトリクス監視と自動ロールバック
// Antigravity に「Cloudflare Analytics APIを使った自動ロールバックを実装して」と依頼
interface MetricThreshold {
metric : string ;
operator : 'gt' | 'lt' | 'gte' | 'lte' ;
value : number ;
window : string ; // 評価ウィンドウ: '5m', '15m', '1h'
}
interface RollbackPolicy {
flagKey : string ;
thresholds : MetricThreshold [];
cooldownMinutes : number ; // ロールバック後の再有効化禁止期間
notifyChannels : string [];
}
const ROLLBACK_POLICIES : RollbackPolicy [] = [
{
flagKey: 'new-checkout-flow' ,
thresholds: [
{ metric: 'error_rate' , operator: 'gt' , value: 2.0 , window: '5m' },
{ metric: 'p99_latency_ms' , operator: 'gt' , value: 2000 , window: '5m' },
{ metric: 'checkout_success_rate' , operator: 'lt' , value: 95 , window: '15m' },
],
cooldownMinutes: 60 ,
notifyChannels: [ 'slack:payments-alerts' , 'pagerduty:checkout' ],
},
];
async function monitorAndRollback ( policy : RollbackPolicy ) : Promise < void > {
const checkInterval = 60_000 ; // 1分ごとにチェック
while ( true ) {
await new Promise ( r => setTimeout (r, checkInterval));
for ( const threshold of policy.thresholds) {
const currentValue = await getMetricValue (
threshold.metric,
threshold.window
);
const violated = evaluateThreshold (currentValue, threshold);
if (violated) {
console. error (
`🚨 Threshold violated: ${ threshold . metric } = ${ currentValue } ` +
`(${ threshold . operator } ${ threshold . value })`
);
// 即座にロールバック
await executeRollback (policy.flagKey);
// 通知送信
for ( const channel of policy.notifyChannels) {
await sendNotification (channel, {
type: 'rollback' ,
flagKey: policy.flagKey,
reason: `${ threshold . metric } = ${ currentValue }` ,
timestamp: new Date (). toISOString (),
});
}
// クールダウン
console. log ( `⏸️ Cooldown: ${ policy . cooldownMinutes }m` );
await new Promise ( r =>
setTimeout (r, policy.cooldownMinutes * 60_000 )
);
break ;
}
}
}
}
function evaluateThreshold (
value : number ,
threshold : MetricThreshold
) : boolean {
switch (threshold.operator) {
case 'gt' : return value > threshold.value;
case 'lt' : return value < threshold.value;
case 'gte' : return value >= threshold.value;
case 'lte' : return value <= threshold.value;
}
}
async function getMetricValue ( metric : string , window : string ) : Promise < number > {
// Cloudflare Analytics API または外部モニタリングツールから取得
// 実装はプロジェクトのインフラに合わせて Antigravity が自動生成
return 0 ;
}
async function executeRollback ( flagKey : string ) : Promise < void > {
// KV のフラグを無効化
console. log ( `🔄 Rolling back flag: ${ flagKey }` );
}
async function sendNotification (
channel : string ,
payload : Record < string , string >
) : Promise < void > {
// Slack / PagerDuty / Discord への通知
console. log ( `📢 Notification sent to ${ channel }` );
}
// 出力例(ロールバック発動時):
// 🚨 Threshold violated: error_rate = 3.2 (gt 2.0)
// 🔄 Rolling back flag: new-checkout-flow
// 📢 Notification sent to slack:payments-alerts
// 📢 Notification sent to pagerduty:checkout
// ⏸️ Cooldown: 60m
フラグのライフサイクル管理 — 技術的負債を防ぐ
Feature Flags は便利ですが、放置すると技術的負債になります。Antigravity エージェントを活用して、フラグのライフサイクルを自動管理する仕組みを構築しましょう。
// scripts/flag-hygiene.ts — 期限切れフラグの検出と除去支援
// Antigravity に「期限切れフラグを検出するスクリプトを作って」と依頼
import { readFileSync, readdirSync } from 'fs' ;
import { join } from 'path' ;
interface FlagAuditResult {
key : string ;
status : 'active' | 'expired' | 'stale' | 'orphaned' ;
expiresAt ?: string ;
usageCount : number ; // コード内での参照回数
lastModified : string ; // 最終変更日
recommendation : string ;
}
async function auditFlags ( projectRoot : string ) : Promise < FlagAuditResult []> {
const results : FlagAuditResult [] = [];
const now = new Date ();
// フラグ定義ファイルを読み込み
const flagDefs = loadFlagDefinitions (projectRoot);
for ( const flag of flagDefs) {
// コード内での参照回数をカウント
const usageCount = countFlagUsages (projectRoot, flag.key);
// ステータス判定
let status : FlagAuditResult [ 'status' ] = 'active' ;
let recommendation = '' ;
if (flag.expiresAt && new Date (flag.expiresAt) < now) {
status = 'expired' ;
recommendation = `期限切れ(${ flag . expiresAt })。フラグと関連コードを除去してください。` ;
} else if (usageCount === 0 ) {
status = 'orphaned' ;
recommendation = 'コード内で未使用。定義を削除してください。' ;
} else if ( daysSince (flag.createdAt) > 90 && flag.type === 'release' ) {
status = 'stale' ;
recommendation = '90日以上経過したリリースフラグ。恒久化または除去を検討してください。' ;
}
results. push ({
key: flag.key,
status,
expiresAt: flag.expiresAt,
usageCount,
lastModified: flag.createdAt,
recommendation,
});
}
return results;
}
function countFlagUsages ( root : string , flagKey : string ) : number {
// grep -r で参照回数をカウント
// 実際には Antigravity エージェントがプロジェクト全体を検索
let count = 0 ;
const srcDir = join (root, 'src' );
// 再帰的にファイルを走査して flagKey の出現回数を数える
return count;
}
function daysSince ( dateStr : string ) : number {
return Math. floor (
(Date. now () - new Date (dateStr). getTime ()) / ( 1000 * 60 * 60 * 24 )
);
}
function loadFlagDefinitions ( root : string ) : any [] {
// プロジェクトのフラグ定義を読み込む
return [];
}
// CI で週次実行し、レポートを Slack に投稿:
// npx tsx scripts/flag-hygiene.ts
//
// 出力例:
// 🔍 Flag Audit Report — 2026-03-30
// ────────────────────────────────
// ❌ EXPIRED: new-checkout-flow (expired 2026-03-28)
// → フラグと関連コードを除去してください
// ⚠️ STALE: legacy-api-fallback (created 120 days ago)
// → 90日以上経過。恒久化または除去を検討してください
// ✅ ACTIVE: exp-pricing-v2 (expires 2026-04-15, 23 references)
// ✅ ACTIVE: ops-maintenance-mode (no expiry, 5 references)
Antigravity エージェントにこのスクリプトを定期実行させ、期限切れのフラグを自動で検出してプルリクエストを作成するように設定できます。「フラグ new-checkout-flow のコードをすべて除去して、常に新UIを表示するように変更して」と指示するだけで、条件分岐の除去、テストコードの更新、フラグ定義の削除までを一括で実行してくれます。
公式ドキュメントが触れない 7 つの運用上の落とし穴
ここからは、ベンダーのドキュメントや教科書には書かれていないものの、私が実運用で何度か痛みを伴って学んだ落とし穴を 7 つに絞って共有します。Antigravity に同じ失敗を回避させるための前提知識としても役立つはずです。
1. フラグ評価のレイテンシは「平均」ではなく「テールの 99 パーセンタイル」を見る : 平均 3ms でも、99 パーセンタイルが 200ms に跳ねていれば、特定ユーザー体験は確実に劣化します。Cloudflare Workers から KV を読む構成では、リージョンによってコールドリードが入ることがあります。私は p99 を常時メトリクスに残し、しきい値を超えたらフラグ評価をオンメモリキャッシュに切り替える分岐を入れました。
2. ユーザー識別子のハッシュは「一度決めたら絶対に変えない」 : 段階ロールアウトの 50% は、ハッシュ値で決定論的に決まるユーザー集合です。途中でハッシュアルゴリズムや種を変えると、対象ユーザー群がシャッフルされ、新機能を一度見たユーザーが翌日には見えなくなる、という最悪のユーザー体験を生みます。
3. フラグ ON のままサーバー側ロジックだけ削除しない : 「もうフラグ ON 状態のコードに統合したから」と古いブランチのコードを消した直後、リージョン障害で旧コードに切り戻したくなる場面が来ました。フラグを完全除去するまで、両系のコードを最低 1 リリース分は残しておくのが安全です。
4. A/B テストの結果は「最後の 1 週間」だけを見る : 立ち上げ初期はノベルティ効果でクリック率が跳ねます。私のアプリでも、新カテゴリ追加直後の 3 日間は CTR が 1.5 倍になり、その後 1 週間で平常値に戻る、というパターンを何度も観測しました。ロールアウト判断は安定期に入ってから行います。
5. 「課金ユーザーだけに見せる」フラグは課金状態の遅延を必ず考慮する : Stripe や RevenueCat の Webhook が数秒遅延することがあり、決済直後にフラグ評価すると非課金として扱われるリスクがあります。私はクライアント側で楽観的に課金フラグを ON にし、サーバー検証で正規化するパターンを使っています。
6. プッシュ通知や AdMob 等の外部 SDK 設定はフラグで包めない領域 : ストア審査を必要とするネイティブ層の挙動は、Feature Flag では真の意味では制御できません。私は「ストア審査が必要な範囲」と「サーバー側で切り替え可能な範囲」を最初に線引きする習慣をつけ、フラグ運用に過度な期待を持たないよう自分に釘を刺しています。
7. Antigravity に丸投げするとフラグが「増える方向」にしか動かない : AI エージェントは新規生成は得意ですが、消す判断は明確な指示がないと回避しがちです。「期限が来たフラグは必ず除去 PR を出す」「同じカテゴリのフラグが 3 つ以上溜まったら統合提案を出す」というルールをエージェントに先に渡しておくと、フラグの腐敗速度が目に見えて遅くなります。
これらの落とし穴は、どれも一度は事故を経験しないと身に染みないものばかりですが、Antigravity に運用を任せる前にチェックリスト化しておけば、同じ痛みを繰り返さずに済みます。
実践ワークフロー — Antigravity で Progressive Delivery を日常に組み込む
ここまで紹介したパターンを、日々の開発ワークフローに統合する方法をまとめます。
ステップ 1: 機能開発開始時 — Antigravity エージェントに「新機能 X のための Feature Flag を作成して」と依頼します。エージェントはフラグ定義、React フック、サーバーサイドの評価ロジック、テストコードを一括生成します。
ステップ 2: 開発中 — フラグで新旧の実装を切り替えながら開発します。development 環境ではフラグを常に有効にし、staging ではランダム振り分けでテストします。
ステップ 3: コードレビュー — Antigravity のリスク分析がプルリクエストに自動コメントし、推奨されるロールアウト戦略を提案します。
ステップ 4: デプロイ — メインブランチにマージすると、CI/CD パイプラインが自動デプロイ。しかしフラグは OFF のため、ユーザーへの影響はありません。
ステップ 5: カナリアリリース — フラグのロールアウト率を 1% → 5% → 25% → 50% → 100% と段階的に引き上げます。各段階でメトリクスを自動監視し、異常があれば即ロールバックします。
ステップ 6: クリーンアップ — 100% ロールアウト後、フラグ管理スクリプトが期限切れを検出し、除去のためのプルリクエストを自動作成します。
この一連の流れを Antigravity が支援することで、手動での煩雑な作業を最小限に抑えつつ、安全なリリースを実現できます。
ケーススタディ:Before / After — 壁紙アプリのカテゴリ追加で実際に起きた変化
抽象論だけでは伝わりにくいので、私が運用している壁紙系アプリで実際に起きた変化を具体例として残します。Before / After で振り返ると、フラグ導入の効果が体感的に把握しやすくなるはずです。
Before(Feature Flag 導入前) : 新しい壁紙カテゴリを追加するとき、私は完成したコードをそのまま本番にデプロイし、ストアの審査を経て一斉公開していました。問題発生時のロールバック手段はアプリのバージョン差し戻し申請しかなく、Apple の審査で 1 〜 3 日、Google で最大 7 日かかることもありました。実際、ある画像生成カテゴリで OutOfMemoryError が増えていることに気づいたのは公開から 14 時間後で、Google Play での緊急ロールバックには 4 日かかり、その間にレビュー評価が 4.6 から 4.4 まで下がるという痛みを伴いました。
After(Feature Flag + Progressive Delivery 導入後) : 同じカテゴリの追加を、Antigravity に「リリースフラグで囲って、ロールアウトは 1% → 5% → 25% → 100% で 24 時間ずつ間を空ける構成にして」と指示しました。エージェントはフラグ評価ロジック、クライアント側の表示制御、ロールアウト率を変更するための管理 API までを一括で生成しました。今回もメモリ使用量がしきい値を超えましたが、Cloudflare Workers 側のメトリクスがアラートを出した段階でロールアウト率はまだ 5%。フラグを false に切り替えるだけで影響が止まり、ストア審査も再申請も不要で平常運転に戻れました。修正版を Antigravity に依頼し、翌日には同じカテゴリを 1% から再ロールアウト開始できています。
この差は数値にも表れます。導入前は新カテゴリ追加にかける心理的コストが大きく、平均で 2 ヶ月に 1 回のペースでしかリリースしていませんでした。導入後は週 1 ペースで新カテゴリを試せるようになり、結果として CTR が高いカテゴリを早期に発見できるようになっています。フラグは「事故が起きないようにする道具」というより、「打席に立つ回数を増やすための道具」だと体感したのはこの時でした。
最後に
Feature Flags と Progressive Delivery を Antigravity の AI エージェントに織り込んでみて改めて思うのは、これらは派手な新技術ではなく「リリースを担当する人間の精神衛生のための仕組み」だということです。一度構築しておけば、深夜にメトリクスが荒れても、ボタン一つで巻き戻せる、という安心感がチーム全体(個人開発なら自分自身)の判断の質を底上げしてくれます。
私自身、はじめはリリースフラグ 1 本から導入しました。最初の数週間は「フラグを ON/OFF する勇気」を養うのに精いっぱいでしたが、慣れてくると、カナリアリリース、A/B テスト、緊急停止、と段階的に道具が増えていきます。アプリ事業もブログ事業も、ここから先の「打席に立てる回数」が大きく増えるはずです。
最初の一歩としておすすめしたいのは、すでに本番運用している機能のうち「再有効化したいかもしれない既存挙動」をフラグで囲うことです。新機能リリースから始めるよりも、既存機能の「巻き戻し可能化」から入った方が、フラグ運用の感覚を低リスクで掴めます。
ここまでお読みいただきありがとうございました。同じように個人開発で複数プロダクトを運用している方の参考になれば幸いです。
関連記事として、GitHub Actions 上級CI/CDパイプライン構築 では CI/CD の自動化をさらに深掘りしています。OpenTelemetry によるオブザーバビリティ構築 は、本記事で触れたメトリクス監視の基盤づくりを補完する内容ですので、合わせてご覧いただけると Progressive Delivery 全体の解像度が上がるはずです。