feature-flags について
LaunchDarkly、Unleash、自作方式でのフィーチャーフラグ実装と運用を扱うスキルです。段階的リリース、実験、実行時制御、フラグのライフサイクルを管理するチーム向けです。
主な機能
- 01リリース、実験、運用フラグの設定
- 02ターゲティングと有効化戦略の設定
- 03LaunchDarkly と Unleash の SDK 例
- 04データベースまたは Redis によるフラグ構築
- 05フラグの各バリエーションのテスト
- 06評価の追跡と古いフラグの検出
活用シーン
- 01機能を段階的にリリース
- 02ユーザーグループで A/B テストを実施
- 03キルスイッチで本番機能を停止
- 04古いフラグの検出と整理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ bagelhole/ devops-security-agent-skills --skill feature-flagsインストール後、Claude Code で/feature-flagsを入力して読み込まれたか確認します。
詳細
機能フラグ
機能フラグシステムを使って、機能のリリースを制御し、段階的なロールアウトを実現します。
このスキルを使用する場面
次のような場合にこのスキルを使用します。
- 機能を段階的にロールアウトする
- トランクベース開発を有効にする
- A/B テストや実験を実施する
- 機能のライフサイクルを管理する
- 本番環境向けのキルスイッチを実装する
前提条件
- アプリケーションコードへのアクセス
- 機能フラグサービス、またはセルフホスト可能なソリューション
- デプロイパターンに関する基本的な理解
機能フラグの種類
| 種類 | 目的 | 例 |
|---|---|---|
| リリース | 機能の表示を制御する | 新しいチェックアウトフロー |
| 実験 | A/B テスト | ボタンの色のテスト |
| 運用 | 実行時の設定 | レート制限 |
| 権限 | ユーザーアクセスを制御する | プレミアム機能 |
| キルスイッチ | 緊急時に無効化する | サードパーティとの連携 |
LaunchDarkly
SDK のセットアップ(Node.js)
const LaunchDarkly = require('launchdarkly-node-server-sdk');
const client = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY);
await client.waitForInitialization();
// Evaluate flag
const user = {
key: 'user-123',
email: '[email protected]',
custom: {
plan: 'premium',
company: 'acme'
}
};
const showNewFeature = await client.variation('new-checkout', user, false);
if (showNewFeature) {
// New feature code
} else {
// Existing code
}
React SDK
import { withLDProvider, useFlags, useLDClient } from 'launchdarkly-react-client-sdk';
// Provider setup
export default withLDProvider({
clientSideID: 'your-client-side-id',
user: {
key: 'user-123',
email: '[email protected]'
}
})(App);
// Using flags in component
function FeatureComponent() {
const { newCheckout, experimentVariant } = useFlags();
const ldClient = useLDClient();
// Track events
const handleClick = () => {
ldClient.track('checkout-started');
};
if (newCheckout) {
return <NewCheckout onClick={handleClick} />;
}
return <OldCheckout onClick={handleClick} />;
}
ターゲティングルール
# LaunchDarkly targeting configuration
flag: new-checkout
targeting:
# Individual users
targets:
- variation: true
values: ['user-123', 'user-456']
# Rules
rules:
# Beta users
- variation: true
clauses:
- attribute: email
op: endsWith
values: ['@company.com']
# Premium plan
- variation: true
clauses:
- attribute: plan
op: in
values: ['premium', 'enterprise']
# Percentage rollout
- variation: true
rollout:
variations:
- variation: true
weight: 20000 # 20%
- variation: false
weight: 80000 # 80%
# Default
fallthrough:
variation: false
Unleash
サーバーのセットアップ
# docker-compose.yml
version: '3.8'
services:
unleash:
image: unleashorg/unleash-server:latest
ports:
- "4242:4242"
environment:
- DATABASE_URL=postgres://postgres:password@db/unleash
- DATABASE_SSL=false
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=unleash
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
SDK のセットアップ(Node.js)
const { initialize } = require('unleash-client');
const unleash = initialize({
url: 'http://localhost:4242/api',
appName: 'my-app',
customHeaders: {
Authorization: 'your-api-token'
}
});
unleash.on('ready', () => {
// Check feature
const isEnabled = unleash.isEnabled('new-checkout');
// With context
const context = {
userId: 'user-123',
properties: {
plan: 'premium'
}
};
const isEnabledForUser = unleash.isEnabled('new-checkout', context);
// Get variant
const variant = unleash.getVariant('experiment-flag', context);
console.log(variant.name); // 'control' or 'treatment'
});
有効化戦略
# Standard strategies
strategies:
- name: default
# On/off for everyone
- name: userWithId
parameters:
userIds: 'user-1,user-2,user-3'
- name: gradualRolloutUserId
parameters:
percentage: 25
groupId: 'new-feature'
- name: gradualRolloutRandom
parameters:
percentage: 50
- name: flexibleRollout
parameters:
rollout: 30
stickiness: userId
groupId: 'checkout-exp'
カスタム実装
データベースを利用したフラグ
# models.py
from django.db import models
class FeatureFlag(models.Model):
name = models.CharField(max_length=100, unique=True)
enabled = models.BooleanField(default=False)
rollout_percentage = models.IntegerField(default=0)
allowed_users = models.JSONField(default=list)
rules = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# service.py
import hashlib
class FeatureFlagService:
def __init__(self):
self._cache = {}
def is_enabled(self, flag_name, user_id=None, context=None):
flag = self._get_flag(flag_name)
if not flag or not flag.enabled:
return False
# Check user allowlist
if user_id and user_id in flag.allowed_users:
return True
# Check rules
if context and self._evaluate_rules(flag.rules, context):
return True
# Check percentage rollout
if flag.rollout_percentage > 0 and user_id:
return self._is_in_rollout(flag_name, user_id, flag.rollout_percentage)
return flag.rollout_percentage == 100
def _is_in_rollout(self, flag_name, user_id, percentage):
hash_input = f"{flag_name}:{user_id}"
hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
return (hash_value % 100) < percentage
def _evaluate_rules(self, rules, context):
for rule in rules.get('rules', []):
if self._evaluate_rule(rule, context):
return True
return False
Redis を利用したフラグ
import redis
import json
class RedisFeatureFlags:
def __init__(self, redis_url):
self.redis = redis.from_url(redis_url)
self.prefix = 'feature_flag:'
def set_flag(self, name, config):
key = f"{self.prefix}{name}"
self.redis.set(key, json.dumps(config))
def is_enabled(self, name, user_id=None):
key = f"{self.prefix}{name}"
data = self.redis.get(key)
if not data:
return False
config = json.loads(data)
if not config.get('enabled', False):
return False
# User allowlist
if user_id in config.get('users', []):
return True
# Percentage rollout
percentage = config.get('percentage', 0)
if percentage == 100:
return True
if percentage > 0 and user_id:
return self._hash_user(name, user_id) < percentage
return False
def _hash_user(self, flag, user_id):
import hashlib
hash_input = f"{flag}:{user_id}"
return int(hashlib.sha256(hash_input.encode()).hexdigest(), 16) % 100
機能フラグを使ったテスト
ユニットテスト
// Jest mocking
jest.mock('launchdarkly-node-server-sdk', () => ({
init: jest.fn(() => ({
waitForInitialization: jest.fn().mockResolvedValue(undefined),
variation: jest.fn()
}))
}));
describe('Checkout', () => {
it('shows new checkout when flag enabled', async () => {
const ldClient = require('launchdarkly-node-server-sdk').init();
ldClient.variation.mockResolvedValue(true);
const result = await renderCheckout(user);
expect(result).toContain('NewCheckout');
});
it('shows old checkout when flag disabled', async () => {
const ldClient = require('launchdarkly-node-server-sdk').init();
ldClient.variation.mockResolvedValue(false);
const result = await renderCheckout(user);
expect(result).toContain('OldCheckout');
});
});
統合テスト
# pytest fixtures
import pytest
@pytest.fixture
def feature_flags():
"""Provide controllable feature flags for testing."""
flags = {}
class TestFlags:
def set(self, name, value):
flags[name] = value
def is_enabled(self, name, **kwargs):
return flags.get(name, False)
return TestFlags()
def test_new_checkout(feature_flags):
feature_flags.set('new-checkout', True)
response = client.get('/checkout')
assert 'new-checkout-form' in response.content
モニタリングと分析
フラグの使用状況の追跡
// Track flag evaluations
const flagMetrics = {
evaluations: new Map(),
track(flagName, variation, user) {
const key = `${flagName}:${variation}`;
const count = this.evaluations.get(key) || 0;
this.evaluations.set(key, count + 1);
// Send to analytics
analytics.track('feature_flag_evaluated', {
flag: flagName,
variation: variation,
userId: user.key
});
}
};
古くなったフラグの検出
from datetime import datetime, timedelta
def detect_stale_flags():
"""Find flags that haven't been evaluated recently."""
stale_threshold = timedelta(days=30)
now = datetime.utcnow()
stale_flags = []
for flag in FeatureFlag.objects.all():
if flag.last_evaluated:
age = now - flag.last_evaluated
if age > stale_threshold:
stale_flags.append({
'name': flag.name,
'last_evaluated': flag.last_evaluated,
'age_days': age.days
})
return stale_flags
よくある問題
問題:フラグの評価結果が一貫しない
問題:同じユーザーに異なるバリエーションが表示されます 解決策:一貫性のあるハッシュを使い、キャッシュ戦略を確認します
問題:フラグによる負債の蓄積
問題:コードベースに古いフラグが多すぎます 解決策:フラグのライフサイクルを実装し、定期的なクリーンアップスプリントを実施します
問題:パフォーマンスへの影響
問題:フラグの評価によってリクエストが遅くなります 解決策:ローカルキャッシュを使い、評価をまとめて実行します
ベストプラクティス
- 一貫性のある命名規則を使う
- フラグの目的と担当者を記録する
- 一時的なフラグには有効期限を設定する
- フラグのライフサイクル管理を実装する
- 段階的にロールアウトする(0→100 は避ける)
- フラグ評価のメトリクスを監視する
- 古いフラグを定期的に削除する
- CI で両方のバリエーションをテストする
関連スキル
- blue-green-deploy - デプロイ戦略
- git-workflow - トランクベース開発
- alerting-oncall - ロールアウトのモニタリング
コメント
Markdown 対応