新機能のリリースがいつも一大イベントになっていませんか?「この変更で、何か壊れないだろうか…」とか考えていませんか?
この記事では、開発プロセスを美しくする、Feature Flag(フィーチャーフラグ) について説明します。これさえあれば、コードを一切変更することなく、本番環境で機能を自由にON/OFFできるようになります。
「新UIをまず5%のユーザーにだけ見せたい」「バグが見つかった機能を、1秒で止めたい」そんな、やりたかったであろう柔軟なリリースが、実現します。この記事では、Go言語による具体的な実装コードから、KubernetesのConfigMapを使った実践的な運用方法、さらには自動化スクリプトまで、説明しようと思います。
エンジニアリングの実務経験が増えてくると、 Feature Flag(フィーチャーフラグ) は非常に重要な概念・手法だと実感します...
大きな変更や既存機能に影響がある開発時によく用いられる手法で、すでに使ったことがある方も多いのではないでしょうか?本記事では、Feature Flagの基本概念から実装方法までを幅広くまとめてみようと思います。
Feature Flagとは
Feature Flag(別名:Feature Toggle、Feature Switch)は、アプリケーションの機能を動的に有効/無効化できる仕組みです。コードを変更することなく、実行時に特定の機能の表示や動作を制御することができます。
Feature Flagという名前は実は使用する企業や開発環境によって色々な名前があるようです
基本的な仕組み
基本的な例としてUIを変更した際に新しいUIを出すか既存のUIを出すかどうかの切り替えをしたいとします。
Feature Flagを使用することで、開発者はコードベースを変更することなく、特定の機能を動的に制御することができます。これにより、リリースの柔軟性が向上し、リスクを最小限に抑えることが可能です。例えば、新しいUIを導入する際に、Feature Flagを使って一部のユーザーにのみ新しいUIを表示し、問題がないかを確認することができます。
// 簡単な例
if featureFlag.IsEnabled("new-ui") {
// 新しいUIを表示
renderNewUI()
} else {
// 従来のUIを表示
renderOldUI()
}
Feature Flagの主なユースケース
段階的リリース(Gradual Rollout)
新機能を全ユーザーに一度に公開するのではなく、段階的に公開することでリスクを軽減できます。
// ユーザーの10%に新機能を公開(IsEnabledForUser関数というものを自作したとする)
if featureFlag.IsEnabledForUser("new-feature", userID, 0.1) {
enableNewFeature()
}
A/Bテスト
異なる機能バージョンを異なるユーザーグループに提供し、効果を測定できます。
// ユーザーをグループAまたはBに振り分け(GetVariantとはexperimentというフラグがある場合に、そのユーザーがAかBかを返す関数とする)
if featureFlag.GetVariant("experiment", userID) == "A" {
showVariantA()
} else {
showVariantB()
}
緊急時の機能無効化
本番環境で問題が発生した場合、コードを再デプロイすることなく機能を無効化できます。
例えば環境変数を変更するのみで、特定の機能を無効化することができます。
// 問題のある機能を即座に無効化
if featureFlag.IsEnabled("problematic-feature") {
// この機能は無効化されているため実行されない
problematicFunction()
}
環境別の機能制御
開発環境、ステージング環境、本番環境で異なる機能を有効にできます。
// 開発環境でのみデバッグ機能を有効化
if featureFlag.IsEnabled("debug-mode") {
enableDebugFeatures()
}
ユーザー別の機能提供
特定のユーザーやユーザーグループにのみ機能を提供できます。
// プレミアムユーザーのみに機能を提供
if featureFlag.IsEnabledForUser("premium-feature", userID) {
enablePremiumFeature()
}
Feature Flagの種類
企業や開発者によってFeature Flagの種類は様々ですが、以下は一般的な種類があります。
Release Flags(リリースフラグ)
- 目的: 新機能の段階的リリース
- 期間: 短期的(数日〜数週間)
- 例: 新しいUI、新機能の段階的公開
Experiment Flags(実験フラグ)
- 目的: A/Bテストや実験
- 期間: 中期的(数週間〜数ヶ月)
- 例: 異なるUIデザインの効果測定
Operational Flags(運用フラグ)
- 目的: 運用時の制御
- 期間: 長期的(数ヶ月〜永続的)
- 例: 機能の有効/無効切り替え
Permission Flags(権限フラグ)
- 目的: ユーザー権限の制御
- 期間: 永続的
- 例: 管理者機能、プレミアム機能
Feature Flagのメリット
Feature Flagを使用することで、以下のようなメリットがあります。
リスク軽減
- 新機能の段階的リリースにより、問題の影響範囲を限定
- 緊急時の即座な機能無効化
開発効率の向上
- 機能ブランチの長期化を回避
- 継続的デリバリーの実現
ユーザー体験の向上
- 段階的な機能公開による安定性確保
- データに基づく機能改善
運用の柔軟性
- 環境別の機能制御
- ユーザー別の機能提供
Feature Flagの注意点
Feature Flagを使用する際には、以下のような注意点があります。
技術的負債
- 不要になったフラグの削除を忘れない
- フラグの管理とメンテナンスが必要
複雑性の増加
- 条件分岐の増加によるコードの複雑化
- テストケースの増加
パフォーマンスへの影響
- フラグの評価によるオーバーヘッド
- キャッシュ戦略の検討が必要
Go言語でのFeature Flag実装
ここでは、Go言語でのFeature Flag実装について詳しく説明します。
初期設定
まずは最短で動かすための構成を示します。
- JSONファイルでフラグの初期値を管理する
- 環境変数で本番・検証時に上書きする
フラグの初期値を設定する
{
"new-ui": {
"name": "new-ui",
"type": "release",
"enabled": false,
"percentage": 0.0
},
"debug-mode": {
"name": "debug-mode",
"type": "operational",
"enabled": false
}
}
FEATURE_FLAG_USE_ENV_VARS=true # 環境変数(この.envファイルを読み込むかどうか)つまり以下の環境変数をまとめて有効化するか
FEATURE_FLAG_NEW_UI_ENABLED=true # new-ui を有効化
FEATURE_FLAG_NEW_UI_PERCENTAGE=0.1 # 10%のユーザーに配信
// 最小の利用コード例(詳細は下のセクションに続きます)
cfg, _ := config.NewConfig()
ff := featureflag.NewService(cfg)
enabled := ff.IsEnabled(ctx, "new-ui", featureflag.Context{UserID: "u1"})
if enabled {
// 新UIを出す
}
実装
基本的な構造
まず、Feature Flagの基本構造を定義します。
// featureflag/types.go
package featureflag
import (
"context"
"time"
)
// FlagType はFeature Flagの種類を表す
type FlagType string
const (
ReleaseFlag FlagType = "release"
ExperimentFlag FlagType = "experiment"
OperationalFlag FlagType = "operational"
PermissionFlag FlagType = "permission"
)
// Flag は個別のFeature Flagの設定を表す
type Flag struct {
Name string `json:"name"`
Type FlagType `json:"type"`
Enabled bool `json:"enabled"`
Percentage float64 `json:"percentage,omitempty"`
Variants map[string]float64 `json:"variants,omitempty"`
Users []string `json:"users,omitempty"`
Groups []string `json:"groups,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// Context はFeature Flagの評価に必要なコンテキスト情報を表す
type Context struct {
UserID string `json:"user_id,omitempty"`
UserGroup string `json:"user_group,omitempty"`
Environment string `json:"environment,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
}
// Service はFeature Flagの評価を行うインターフェース
type Service interface {
IsEnabled(ctx context.Context, flagName string, evalCtx Context) bool
GetVariant(ctx context.Context, flagName string, evalCtx Context) string
GetAllFlags(ctx context.Context) map[string]Flag
}
config.goでの設定管理
config.go
でFeature Flagの設定を管理します。
// config/config.go
package config
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"your-project/featureflag"
)
// Config はアプリケーション全体の設定を管理する
type Config struct {
FeatureFlags FeatureFlagConfig `json:"feature_flags"`
// 他の設定項目...
}
// FeatureFlagConfig はFeature Flagの設定を管理する
type FeatureFlagConfig struct {
Flags map[string]featureflag.Flag `json:"flags"`
// 設定ファイルのパス
ConfigPath string `json:"config_path"`
// 環境変数から読み込むかどうか
UseEnvVars bool `json:"use_env_vars"`
// キャッシュの有効期限
CacheTTL time.Duration `json:"cache_ttl"`
}
// NewConfig は新しい設定インスタンスを作成する
func NewConfig() (*Config, error) {
config := &Config{
FeatureFlags: FeatureFlagConfig{
Flags: make(map[string]featureflag.Flag),
ConfigPath: getEnvOrDefault("FEATURE_FLAG_CONFIG_PATH", "./config/feature-flags.json"),
UseEnvVars: getEnvBoolOrDefault("FEATURE_FLAG_USE_ENV_VARS", true),
CacheTTL: getEnvDurationOrDefault("FEATURE_FLAG_CACHE_TTL", 5*time.Minute),
},
}
// 設定ファイルから読み込み
if err := config.loadFromFile(); err != nil {
return nil, fmt.Errorf("failed to load config from file: %w", err)
}
// 環境変数から読み込み(オプション)
if config.FeatureFlags.UseEnvVars {
config.loadFromEnvVars()
}
return config, nil
}
// loadFromFile は設定ファイルからFeature Flagを読み込む
func (c *Config) loadFromFile() error {
data, err := os.ReadFile(c.FeatureFlags.ConfigPath)
if err != nil {
if os.IsNotExist(err) {
// ファイルが存在しない場合はデフォルト設定を使用
c.setDefaultFlags()
return nil
}
return err
}
var flags map[string]featureflag.Flag
if err := json.Unmarshal(data, &flags); err != nil {
return fmt.Errorf("failed to parse feature flags config: %w", err)
}
c.FeatureFlags.Flags = flags
return nil
}
// loadFromEnvVars は環境変数からFeature Flagを読み込む
func (c *Config) loadFromEnvVars() {
// FEATURE_FLAG_<FLAG_NAME>_ENABLED=true/false
// FEATURE_FLAG_<FLAG_NAME>_PERCENTAGE=0.5
// FEATURE_FLAG_<FLAG_NAME>_USERS=user1,user2,user3
// FEATURE_FLAG_<FLAG_NAME>_GROUPS=premium,admin
for _, env := range os.Environ() {
if !strings.HasPrefix(env, "FEATURE_FLAG_") {
continue
}
parts := strings.SplitN(env, "=", 2)
if len(parts) != 2 {
continue
}
key, value := parts[0], parts[1]
flagName := extractFlagName(key)
if flagName == "" {
continue
}
// 既存のフラグを取得または新規作成
flag, exists := c.FeatureFlags.Flags[flagName]
if !exists {
flag = featureflag.Flag{
Name: flagName,
Type: featureflag.OperationalFlag,
Enabled: false,
}
}
// 環境変数の値に基づいてフラグを更新
c.updateFlagFromEnv(&flag, key, value)
c.FeatureFlags.Flags[flagName] = flag
}
}
// updateFlagFromEnv は環境変数の値に基づいてフラグを更新する
func (c *Config) updateFlagFromEnv(flag *featureflag.Flag, key, value string) {
switch {
case strings.HasSuffix(key, "_ENABLED"):
flag.Enabled = value == "true"
case strings.HasSuffix(key, "_PERCENTAGE"):
if percentage, err := strconv.ParseFloat(value, 64); err == nil {
flag.Percentage = percentage
}
case strings.HasSuffix(key, "_USERS"):
flag.Users = strings.Split(value, ",")
case strings.HasSuffix(key, "_GROUPS"):
flag.Groups = strings.Split(value, ",")
case strings.HasSuffix(key, "_TYPE"):
flag.Type = featureflag.FlagType(value)
}
}
// extractFlagName は環境変数キーからフラグ名を抽出する
func extractFlagName(key string) string {
// FEATURE_FLAG_MY_FLAG_ENABLED -> MY_FLAG
if !strings.HasPrefix(key, "FEATURE_FLAG_") {
return ""
}
// プレフィックスとサフィックスを除去
name := strings.TrimPrefix(key, "FEATURE_FLAG_")
// サフィックスを除去
suffixes := []string{"_ENABLED", "_PERCENTAGE", "_USERS", "_GROUPS", "_TYPE"}
for _, suffix := range suffixes {
if strings.HasSuffix(name, suffix) {
name = strings.TrimSuffix(name, suffix)
break
}
}
return name
}
// setDefaultFlags はデフォルトのFeature Flagを設定する
func (c *Config) setDefaultFlags() {
c.FeatureFlags.Flags = map[string]featureflag.Flag{
"debug-mode": {
Name: "debug-mode",
Type: featureflag.OperationalFlag,
Enabled: false,
},
"new-ui": {
Name: "new-ui",
Type: featureflag.ReleaseFlag,
Enabled: false,
Percentage: 0.0,
},
"premium-features": {
Name: "premium-features",
Type: featureflag.PermissionFlag,
Enabled: true,
},
}
}
// ヘルパー関数(環境変数が設定されていない場合にデフォルト値を返す)
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvBoolOrDefault(key string, defaultValue bool) bool {
if value := os.Getenv(key); value != "" {
return value == "true"
}
return defaultValue
}
func getEnvDurationOrDefault(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
}
return defaultValue
}
Feature Flag Serviceの実装
実際にFeature Flagを評価するサービスを実装します。
// featureflag/service.go
package featureflag
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"math"
"sync"
"time"
"your-project/config"
)
// service はFeature Flagの評価を行う実装です
type service struct {
config *config.Config
cache map[string]Flag
mutex sync.RWMutex
}
// NewService は新しいFeature Flagサービスを作成する
func NewService(cfg *config.Config) Service {
svc := &service{
config: cfg,
cache: make(map[string]Flag),
}
// 初期キャッシュの構築
svc.refreshCache()
// 定期的なキャッシュ更新
go svc.startCacheRefresh()
return svc
}
// IsEnabled は指定されたフラグが有効かどうかを判定
func (s *service) IsEnabled(ctx context.Context, flagName string, evalCtx Context) bool {
flag, exists := s.getFlag(flagName)
if !exists {
return false
}
if !flag.Enabled {
return false
}
// フラグの種類に応じた評価
switch flag.Type {
case ReleaseFlag:
return s.evaluateReleaseFlag(flag, evalCtx)
case ExperimentFlag:
return s.evaluateExperimentFlag(flag, evalCtx)
case OperationalFlag:
return s.evaluateOperationalFlag(flag, evalCtx)
case PermissionFlag:
return s.evaluatePermissionFlag(flag, evalCtx)
default:
return false
}
}
// GetVariant は実験フラグのバリアントを取得
func (s *service) GetVariant(ctx context.Context, flagName string, evalCtx Context) string {
flag, exists := s.getFlag(flagName)
if !exists || flag.Type != ExperimentFlag {
return ""
}
return s.determineVariant(flag, evalCtx)
}
// GetAllFlags は全てのフラグを取得
func (s *service) GetAllFlags(ctx context.Context) map[string]Flag {
s.mutex.RLock()
defer s.mutex.RUnlock()
result := make(map[string]Flag)
for name, flag := range s.cache {
result[name] = flag
}
return result
}
// 内部メソッド
func (s *service) getFlag(flagName string) (Flag, bool) {
s.mutex.RLock()
defer s.mutex.RUnlock()
flag, exists := s.cache[flagName]
return flag, exists
}
func (s *service) evaluateReleaseFlag(flag Flag, evalCtx Context) bool {
// 特定ユーザーが指定されている場合
if len(flag.Users) > 0 {
for _, user := range flag.Users {
if user == evalCtx.UserID {
return true
}
}
}
// 特定グループが指定されている場合
if len(flag.Groups) > 0 {
for _, group := range flag.Groups {
if group == evalCtx.UserGroup {
return true
}
}
}
// パーセンテージベースの評価
if flag.Percentage > 0 {
return s.isUserInPercentage(evalCtx.UserID, flag.Percentage)
}
return true
}
func (s *service) evaluateExperimentFlag(flag Flag, evalCtx Context) bool {
// 実験フラグは常に有効(バリアントの判定は別途行う)
return true
}
func (s *service) evaluateOperationalFlag(flag Flag, evalCtx Context) bool {
// 運用フラグは環境に応じて評価
if flag.Metadata != nil {
if allowedEnvs, ok := flag.Metadata["allowed_environments"].([]interface{}); ok {
for _, env := range allowedEnvs {
if envStr, ok := env.(string); ok && envStr == evalCtx.Environment {
return true
}
}
return false
}
}
return true
}
func (s *service) evaluatePermissionFlag(flag Flag, evalCtx Context) bool {
// 権限フラグはユーザーとグループで評価
if len(flag.Users) > 0 {
for _, user := range flag.Users {
if user == evalCtx.UserID {
return true
}
}
}
if len(flag.Groups) > 0 {
for _, group := range flag.Groups {
if group == evalCtx.UserGroup {
return true
}
}
}
return false
}
func (s *service) determineVariant(flag Flag, evalCtx Context) string {
if len(flag.Variants) == 0 {
return "control"
}
// ユーザーIDに基づいてバリアントを決定
hash := s.hashString(evalCtx.UserID)
normalized := float64(hash%100) / 100.0
cumulative := 0.0
for variant, percentage := range flag.Variants {
cumulative += percentage
if normalized < cumulative {
return variant
}
}
// デフォルトは最初のバリアント
for variant := range flag.Variants {
return variant
}
return "control"
}
func (s *service) isUserInPercentage(userID string, percentage float64) bool {
hash := s.hashString(userID)
normalized := float64(hash%100) / 100.0
return normalized < percentage
}
func (s *service) hashString(input string) uint32 {
hash := md5.Sum([]byte(input))
hexStr := hex.EncodeToString(hash[:])
// 最初の8文字をuint32に変換
var result uint32
for i := 0; i < 8 && i < len(hexStr); i++ {
result = result*16 + uint32(hexStr[i])
}
return result
}
func (s *service) refreshCache() {
s.mutex.Lock()
defer s.mutex.Unlock()
// 設定からフラグをコピー
for name, flag := range s.config.FeatureFlags.Flags {
s.cache[name] = flag
}
}
func (s *service) startCacheRefresh() {
ticker := time.NewTicker(s.config.FeatureFlags.CacheTTL)
defer ticker.Stop()
for range ticker.C {
s.refreshCache()
}
}
使用例(Go)
実際のアプリケーションでの使用例を示します。
// main.go
package main
import (
"context"
"log"
"net/http"
"your-project/config"
"your-project/featureflag"
)
func main() {
// 設定の読み込み
cfg, err := config.NewConfig()
if err != nil {
log.Fatal("Failed to load config:", err)
}
// Feature Flagサービスの初期化
ffService := featureflag.NewService(cfg)
// HTTPハンドラーの設定
http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-User-ID")
userGroup := r.Header.Get("X-User-Group")
evalCtx := featureflag.Context{
UserID: userID,
UserGroup: userGroup,
Environment: "production",
}
// 新しいUIフラグの確認
if ffService.IsEnabled(r.Context(), "new-ui", evalCtx) {
renderNewUI(w, r)
} else {
renderOldUI(w, r)
}
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
func renderNewUI(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("New UI"))
}
func renderOldUI(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Old UI"))
}
Kubernetesマニフェストでの環境変数設定とデプロイメント
インフラで環境変数として使用する手法を見てみましょう!
ここでは、Kubernetesマニフェストでの環境変数設定と、実際のデプロイメント例について説明します。
Kubernetesでの環境変数管理
基本的なDeploymentマニフェスト
まず、基本的なDeploymentマニフェストを作成します。
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: feature-flag-app # デプロイメント名
labels:
app: feature-flag-app
spec:
replicas: 3 # レプリカ数(本番は多め、検証は少なめ)
selector:
matchLabels:
app: feature-flag-app
template:
metadata:
labels:
app: feature-flag-app
spec:
containers:
- name: app
image: your-registry/feature-flag-app:latest # デプロイするイメージ
ports:
- containerPort: 8080
env:
# Feature Flag設定の基本環境変数(ConfigMapのJSONを読む/ENVで上書きする)
- name: FEATURE_FLAG_USE_ENV_VARS
value: "true" # ENVによる上書きを許可
- name: FEATURE_FLAG_CONFIG_PATH
value: "/app/config/feature-flags.json" # JSONの配置パス
- name: FEATURE_FLAG_CACHE_TTL
value: "5m" # キャッシュ更新間隔
# 個別のFeature Flag設定(デフォルトの上書き)
- name: FEATURE_FLAG_DEBUG_MODE_ENABLED
value: "false" # デバッグ機能を無効
- name: FEATURE_FLAG_NEW_UI_ENABLED
value: "false" # 新UIの有効/無効
- name: FEATURE_FLAG_NEW_UI_PERCENTAGE
value: "0.0" # 新UIの配信割合
- name: FEATURE_FLAG_PREMIUM_FEATURES_ENABLED
value: "true" # 権限フラグの既定
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
ConfigMapを使用した設定管理
環境変数が多くなってきた場合、ConfigMapを使用して設定を管理します。
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: feature-flag-config # 設定用ConfigMap
labels:
app: feature-flag-app
data:
# Feature Flag設定ファイル(アプリ内で /app/config/feature-flags.json として参照)
feature-flags.json: |
{
"debug-mode": {
"name": "debug-mode",
"type": "operational",
"enabled": false,
"metadata": {
"allowed_environments": ["development", "staging"]
}
},
"new-ui": {
"name": "new-ui",
"type": "release",
"enabled": false,
"percentage": 0.0,
"users": [],
"groups": []
},
"premium-features": {
"name": "premium-features",
"type": "permission",
"enabled": true,
"users": [],
"groups": ["premium", "admin"]
},
"ab-test-experiment": {
"name": "ab-test-experiment",
"type": "experiment",
"enabled": true,
"variants": {
"control": 0.5,
"variant-a": 0.25,
"variant-b": 0.25
}
}
}
# 環境変数設定(envFrom: configMapRef で取り込む想定のサンプル)
feature-flags.env: |
FEATURE_FLAG_USE_ENV_VARS=true
FEATURE_FLAG_CONFIG_PATH=/app/config/feature-flags.json
FEATURE_FLAG_CACHE_TTL=5m
FEATURE_FLAG_DEBUG_MODE_ENABLED=false
FEATURE_FLAG_NEW_UI_ENABLED=false
FEATURE_FLAG_NEW_UI_PERCENTAGE=0.0
FEATURE_FLAG_PREMIUM_FEATURES_ENABLED=true
FEATURE_FLAG_AB_TEST_EXPERIMENT_ENABLED=true
Secretを使用した機密設定管理
ユーザーIDやグループ情報など、機密性の高い設定はSecretで管理します。
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: feature-flag-secret
labels:
app: feature-flag-app
type: Opaque
data:
# Base64エンコードされた値
# echo -n "admin,premium" | base64
FEATURE_FLAG_PREMIUM_FEATURES_GROUPS: YWRtaW4scHJlbWl1bQ==
# echo -n "user1,user2,user3" | base64
FEATURE_FLAG_NEW_UI_USERS: dXNlcjEsdXNlcjIsdXNlcjM=
環境別の設定(ConfigMap)管理
開発、ステージング、本番環境で異なる設定を使用します。
Dev環境のconfigmapの例
# k8s/configmap-development.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: feature-flag-config-dev
namespace: development
labels:
app: feature-flag-app
environment: development
data:
feature-flags.json: |
{
"debug-mode": {
"name": "debug-mode",
"type": "operational",
"enabled": true,
"metadata": {
"allowed_environments": ["development"]
}
},
"new-ui": {
"name": "new-ui",
"type": "release",
"enabled": true,
"percentage": 1.0
},
"premium-features": {
"name": "premium-features",
"type": "permission",
"enabled": true,
"groups": ["premium", "admin"]
}
}
Prd環境のconfigmapの例
# k8s/configmap-production.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: feature-flag-config-prod
namespace: production
labels:
app: feature-flag-app
environment: production
data:
feature-flags.json: |
{
"debug-mode": {
"name": "debug-mode",
"type": "operational",
"enabled": false,
"metadata": {
"allowed_environments": ["development", "staging"]
}
},
"new-ui": {
"name": "new-ui",
"type": "release",
"enabled": true,
"percentage": 0.1
},
"premium-features": {
"name": "premium-features",
"type": "permission",
"enabled": true,
"groups": ["premium", "admin"]
}
}
環境別Deploymentマニフェスト
Dev環境のDeploymentの例
# k8s/deployment-development.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: feature-flag-app
namespace: development
labels:
app: feature-flag-app
environment: development
spec:
replicas: 2
selector:
matchLabels:
app: feature-flag-app
template:
metadata:
labels:
app: feature-flag-app
environment: development
spec:
containers:
- name: app
image: your-registry/feature-flag-app:dev
ports:
- containerPort: 8080
env:
- name: FEATURE_FLAG_USE_ENV_VARS
value: "true"
- name: FEATURE_FLAG_CONFIG_PATH
value: "/app/config/feature-flags.json"
- name: FEATURE_FLAG_CACHE_TTL
value: "1m"
- name: FEATURE_FLAG_DEBUG_MODE_ENABLED
value: "true"
- name: FEATURE_FLAG_NEW_UI_ENABLED
value: "true"
- name: FEATURE_FLAG_NEW_UI_PERCENTAGE
value: "1.0"
volumeMounts:
- name: feature-flag-config
mountPath: /app/config
readOnly: true
volumes:
- name: feature-flag-config
configMap:
name: feature-flag-config-dev
Prd環境ののDeploymentの例
# k8s/deployment-production.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: feature-flag-app
namespace: production
labels:
app: feature-flag-app
environment: production
spec:
replicas: 5
selector:
matchLabels:
app: feature-flag-app
template:
metadata:
labels:
app: feature-flag-app
environment: production
spec:
containers:
- name: app
image: your-registry/feature-flag-app:latest
ports:
- containerPort: 8080
env:
- name: FEATURE_FLAG_USE_ENV_VARS
value: "true"
- name: FEATURE_FLAG_CONFIG_PATH
value: "/app/config/feature-flags.json"
- name: FEATURE_FLAG_CACHE_TTL
value: "5m"
- name: FEATURE_FLAG_DEBUG_MODE_ENABLED
value: "false"
- name: FEATURE_FLAG_NEW_UI_ENABLED
value: "true"
- name: FEATURE_FLAG_NEW_UI_PERCENTAGE
value: "0.1"
envFrom:
- secretRef:
name: feature-flag-secret
volumeMounts:
- name: feature-flag-config
mountPath: /app/config
readOnly: true
volumes:
- name: feature-flag-config
configMap:
name: feature-flag-config-prod
ServiceとIngressの設定
ここではk8sで管理するためのServiceやIngressの設定の例を記載します。
本稿の本質には関係ないですが一応参考程度に記載しておきます
Serviceの設定の例
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: feature-flag-app-service # Podを公開する内部Service
labels:
app: feature-flag-app
spec:
selector:
app: feature-flag-app # Podのラベルと一致させる
ports:
- protocol: TCP
port: 80 # クラスター内での公開ポート
targetPort: 8080 # コンテナの実ポート
type: ClusterIP
Ingressの設定の例
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: feature-flag-app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: / # ルート配下をサービスに転送
nginx.ingress.kubernetes.io/ssl-redirect: "true" # HTTPSへリダイレクト
spec:
tls:
- hosts:
- feature-flag-app.example.com
secretName: feature-flag-app-tls
rules:
- host: feature-flag-app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: feature-flag-app-service
port:
number: 80
段階的リリースの実装
段階的リリース用のConfigMap
featureFlagを設定した機能に関してユーザーに段階的にリリースする際にどのようにconfigmapを設定すればいいかの手法を記載します
# k8s/configmap-rollout.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: feature-flag-rollout
labels:
app: feature-flag-app
data:
# 段階的リリースの設定
rollout-config.json: |
{
"stages": [
{
"name": "stage-1",
"percentage": 0.05,
"duration": "1h",
"description": "5%のユーザーに新機能を公開"
},
{
"name": "stage-2",
"percentage": 0.25,
"duration": "2h",
"description": "25%のユーザーに新機能を公開"
},
{
"name": "stage-3",
"percentage": 0.5,
"duration": "4h",
"description": "50%のユーザーに新機能を公開"
},
{
"name": "stage-4",
"percentage": 1.0,
"duration": "0h",
"description": "全ユーザーに新機能を公開"
}
]
}
段階的リリース用のDeployment
featureFlagを設定した機能に関してユーザーに段階的にリリースする際のDeploymentの例です。
# k8s/deployment-rollout.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: feature-flag-app-rollout
labels:
app: feature-flag-app
spec:
replicas: 3
selector:
matchLabels:
app: feature-flag-app
template:
metadata:
labels:
app: feature-flag-app
spec:
containers:
- name: app
image: your-registry/feature-flag-app:latest
ports:
- containerPort: 8080
env:
- name: FEATURE_FLAG_USE_ENV_VARS
value: "true"
- name: FEATURE_FLAG_CONFIG_PATH
value: "/app/config/feature-flags.json"
- name: FEATURE_FLAG_CACHE_TTL
value: "1m"
# 段階的リリース用の設定
- name: FEATURE_FLAG_NEW_UI_ENABLED
value: "true"
- name: FEATURE_FLAG_NEW_UI_PERCENTAGE
value: "0.05"
volumeMounts:
- name: feature-flag-config
mountPath: /app/config
readOnly: true
- name: rollout-config
mountPath: /app/config/rollout
readOnly: true
volumes:
- name: feature-flag-config
configMap:
name: feature-flag-config-prod
- name: rollout-config
configMap:
name: feature-flag-rollout
運用時の設定変更(コマンド)
運用時にfeatureFlagを使用した機能の適用範囲の指定や設定の方法を記載します。
以下の2種類の方法でfeatureFlagの設定値を変更することができます!
kubectlを使用した設定変更
アプリケーションの設定情報を ConfigMapに格納し、Pod はその ConfigMap をマウント(参照)して設定を読み込みます。
# 新機能を10%のユーザーに公開
kubectl patch configmap feature-flag-config-prod --patch '{"data":{"feature-flags.json":"{\"new-ui\":{\"name\":\"new-ui\",\"type\":\"release\",\"enabled\":true,\"percentage\":0.1}}"}}'
# 設定変更を反映するためにPodを再起動
kubectl rollout restart deployment/feature-flag-app -n production
# 設定変更の確認
kubectl get configmap feature-flag-config-prod -n production -o yaml
環境変数の直接変更
環境変数の直接変更は設定を各 Deployment の定義に直接埋め込む方法です。
# 環境変数を直接変更
kubectl set env deployment/feature-flag-app FEATURE_FLAG_NEW_UI_PERCENTAGE=0.25 -n production
# 変更の確認
kubectl get deployment feature-flag-app -n production -o yaml | grep -A 10 env:
監視とログ
ここからはfeatureFlagを導入した際のk8s環境の監視のためのマニフェストの設定方法を紹介します。
Prometheusメトリクスの設定
Prometheus Operatorに対して「どのようにアプリケーションの性能指標(メトリクス)を収集するか」を指示するための設定方法を紹介します。
これは「feature-flag-app
という名前のサービスが公開する /metrics
というURLの情報を30秒間隔で監視してね」とPrometheusに自動でお願いするための設定ファイルです。
# k8s/service-monitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: feature-flag-app-monitor
labels:
app: feature-flag-app
spec:
selector:
matchLabels:
app: feature-flag-app
endpoints:
- port: http
path: /metrics
interval: 30s
ログ設定
k8s環境のログの設定の例です
# k8s/deployment-with-logging.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: feature-flag-app
spec:
template:
spec:
containers:
- name: app
image: your-registry/feature-flag-app:latest
env:
- name: LOG_LEVEL
value: "info"
- name: FEATURE_FLAG_LOG_ENABLED
value: "true"
volumeMounts:
- name: logs
mountPath: /app/logs
volumes:
- name: logs
emptyDir: {}
デプロイメントスクリプト
段階的デプロイメントスクリプト
新機能をいきなり全ユーザーに公開するのではなく、 段階的に公開範囲を広げていく「カナリアリリース」 を自動化するためのスクリプト
#!/bin/bash
# deploy-rollout.sh
set -e
NAMESPACE="production"
DEPLOYMENT="feature-flag-app"
FEATURE_FLAG="new-ui"
# 段階的リリースの設定
STAGES=(
"0.05:1h:5%のユーザーに新機能を公開"
"0.25:2h:25%のユーザーに新機能を公開"
"0.5:4h:50%のユーザーに新機能を公開"
"1.0:0h:全ユーザーに新機能を公開"
)
echo "段階的リリースを開始します: $FEATURE_FLAG"
for stage in "${STAGES[@]}"; do
IFS=':' read -r percentage duration description <<< "$stage"
echo "=== $description ==="
echo "パーセンテージ: $percentage"
# 環境変数を更新
kubectl set env deployment/$DEPLOYMENT FEATURE_FLAG_${FEATURE_FLAG^^}_PERCENTAGE=$percentage -n $NAMESPACE
# デプロイメントの更新を確認
kubectl rollout status deployment/$DEPLOYMENT -n $NAMESPACE
echo "設定が適用されました。$duration 待機します..."
if [ "$duration" != "0h" ]; then
sleep $(echo $duration | sed 's/h$/*3600/' | bc)
fi
echo ""
done
echo "段階的リリースが完了しました!"
緊急時のロールバックスクリプト
新機能に重大なバグが見つかった場合など、 緊急で機能を無効化するための「キルスイッチ」 として機能するスクリプト
#!/bin/bash
# rollback-feature.sh
set -e
NAMESPACE="production"
DEPLOYMENT="feature-flag-app"
FEATURE_FLAG="new-ui"
echo "緊急時ロールバックを実行します: $FEATURE_FLAG"
# 機能を無効化
kubectl set env deployment/$DEPLOYMENT FEATURE_FLAG_${FEATURE_FLAG^^}_ENABLED=false -n $NAMESPACE
# デプロイメントの更新を確認
kubectl rollout status deployment/$DEPLOYMENT -n $NAMESPACE
echo "ロールバックが完了しました。$FEATURE_FLAG が無効化されました。"
Feature Flag運用のベストプラクティス
Feature Flag(フィーチャーフラグ)は、新機能のリリースやA/Bテストを安全かつ柔軟に行うための強力な手法です。しかし、その運用を場当たり的に行うと、技術的負債や予期せぬトラブルの原因にもなり得ます。ここでは、安定したフラグ運用を実現するためのベストプラクティスを解説します。
1. フラグの命名規則を統一する
フラグが増えてくると「このフラグは何だっけ?」となりがちです。一貫した命名規則を設けることで、フラグの目的が名前だけで直感的に理解できるようになり、管理コストが大幅に下がります。
-
目的ベース:
[機能名]_[用途]
(例:new-ui-release
,premium-features-trial
)- 新機能の段階的リリースなど、目的が明確なフラグに使います。
-
実験ベース:
[実験名]_[バリアント]
(例:checkout-button-test-variant-a
)- A/Bテストのように、複数のパターンを比較する場合に有効です。
-
環境ベース:
[環境名]_[機能名]
(例:dev-debug-mode
)- 開発環境やステージング環境でのみ使用する、一時的なフラグに用います。
このようにルールを決めておけば、フラグの一覧を見るだけで、その役割や影響範囲を容易に推測できます。
2. フラグのライフサイクルを管理する
「フラグは作ったら、必ず消す」 というのが鉄則です。不要になったフラグを放置すると、コードベースが複雑化し、誰も触れない「 ゾンビフラグ 」となって技術的負債が溜まっていきます。
よく私もfeatureFlagを削除するのを忘れてしまいます...
この問題を解決する最も効果的な方法は、ライフサイクル管理の自動化です。
-
有効期限の設定: フラグを作成する際に、必ず「有効期限(例:
2025-12-31
)」を設定するルールを設けます。 - 自動クリーンアップ: 1日に1回など、定期的に全フラグの有効期限をチェックするバッチ処理を動かします。
- 削除処理: 期限切れのフラグを見つけたら、自動的にフラグを削除し、関連チーム(Slackなど)に通知します。
この仕組みにより、人間の「うっかり忘れ」を防ぎ、コードベースを常にクリーンに保つことができます。
3. 設定のバリデーションを徹底する
設定ミスは、時として大規模な障害を引き起こします。「ロールアウトのパーセンテージを10%のつもりが100%にしてしまった」といったヒューマンエラーは誰にでも起こり得ます。
これを防ぐため、フラグの設定を保存する前に必ずバリデーション(検証)処理を挟みます。
- 名前の検証: 文字数や使用可能な文字(小文字、数字、ハイフンのみなど)を制限する。
-
タイプの検証:
release
,experiment
など、事前に定義されたタイプ以外はエラーにする。 -
パーセンテージの検証: 値が必ず
0.0
から1.0
の範囲に収まっているかチェックする。 -
A/Bテストの検証: 複数のバリアントの合計パーセンテージが
1.0
(100%) を超えていないかチェックする。
このバリデーション層があるだけで、不正な設定値による事故を未然に防ぐことができ、システムの安定性が格段に向上します。
4. 監視とメトリクスで可視化する
「フラグが正しく機能しているか?」「新機能はユーザーに使われているか?」を把握するために、フラグの利用状況を可視化することが不可欠です。Prometheusなどの監視ツールと連携し、以下のメトリクスを収集しましょう!
- 評価回数: どのフラグがどれくらいの頻度で評価されているか。パフォーマンスへの影響を把握できます。
- 有効/無効状態: フラグが現在ONかOFFか。ダッシュボードで一覧できれば、現在のリリース状況が一目瞭然です。
- ロールアウト率: 何%のユーザーに機能が公開されているか。段階的リリースの進捗を追跡できます。
- 評価レイテンシ: フラグの評価にどれくらい時間がかかっているか。この値が悪化している場合、フラグの判定ロジックがパフォーマンスのボトルネックになっている可能性があります。
これらのメトリクスをダッシュボードで監視することで、フラグシステムの健全性や機能の利用状況をリアルタイムに把握できます。
5. ログとトレーサビリティを確保する
障害発生時、「いつ、誰が、どのフラグを、どのように変更したか」また「特定ユーザーの評価結果はどうだったか」を追跡できることは、原因究明において極めて重要です。
- 評価ログ: フラグが評価されるたびに、「どのフラグが」「どのユーザーに対して」「ON/OFFどちらと判定されたか」をログに出力します。これにより、特定のユーザーで問題が起きた際、そのユーザーがどの機能を使っていたかを正確に追跡できます。
- 変更ログ: フラグの設定が変更された際には、「誰が」「どのフラグを」「古い値から新しい値へ」変更したか、必ず監査ログとして記録します。意図しない設定変更が原因のトラブルシューティングで絶大な効果を発揮します。