Feature flags w continuous deployment – Praktyczny przewodnik

TL;DR: Feature flags (feature toggles) to mechanizm pozwalający na włączanie/wyłączanie funkcjonalności bez deploymentu kodu. Kluczowy element continuous deployment – pozwala na bezpieczne wdrażanie, A/B testing i stopniowe rollout nowych features. Implementacja w Spring Boot + zarządzanie lifecycle toggles.

Dlaczego feature flags są kluczowe w CD?

W erze continuous deployment, gdzie zmiany trafiają do produkcji nawet kilka razy dziennie, potrzebujemy mechanizmów pozwalających na bezpieczne wdrażanie nowych funkcjonalności. Feature flags (znane również jako feature toggles) to jedna z najważniejszych praktyk DevOps, która oddziela deployment kodu od release funkcjonalności.

W 2020 roku, gdy remote work stał się standardem, a zespoły muszą jeszcze szybciej reagować na zmieniające się wymagania biznesowe, feature flags stają się niezbędnym narzędziem dla każdego zespołu developmentu.

Co się nauczysz:

  • Czym są feature flags i jak wspierają continuous deployment
  • Implementację feature flags w Spring Boot z różnymi strategiami
  • Zarządzanie lifecycle feature flags i unikanie technicznego długu
  • Integrację z narzędziami CI/CD jak Jenkins i GitLab CI
  • Monitoring i metryki dla feature flags w produkcji
  • Strategie rollout: canary deployment, blue-green z toggles
Wymagania wstępne: Doświadczenie ze Spring Boot 2.x, podstawy CI/CD, znajomość Docker i Kubernetes. Znajomość wzorców deployment (blue-green, canary) będzie pomocna.

Czym są feature flags?

Feature flag to warunková instrukcja w kodzie, która decyduje czy dana funkcjonalność ma być aktywna. W przeciwieństwie do komentowania kodu czy usuwania funkcji, feature flags pozwalają na dynamiczne kontrolowanie dostępnych features bez redeploymentu aplikacji.

Wyobraź sobie włącznik światła w pokoju. Instalacja elektryczna (kod) jest już gotowa, ale możesz decydować kiedy włączyć światło (feature). Feature flag to taki włącznik – infrastruktura jest wdrożona, ale funkcjonalność można aktywować w dowolnym momencie.

Typy feature flags w praktyce:

Typ toggleCzas życiaZarządzaniePrzykład użycia
Release togglesKrótki (dni/tygodnie)Zespół devNowa funkcja przed pełnym rollout
Experiment togglesŚredni (tygodnie/miesiące)Product teamA/B testing nowego UI
Ops togglesDługi (miesiące/lata)DevOps/SRECircuit breaker, graceful degradation
Permission togglesPermanentnySecurity teamDostęp do admin features

Implementacja w Spring Boot

Zobaczmy praktyczną implementację feature flags w Spring Boot 2.3:

1. Konfiguracja toggles:

# application.yml
features:
  new-payment-gateway: true
  advanced-search: false
  user-recommendations: 
    enabled: false
    rollout-percentage: 10
  maintenance-mode: false

# Dla różnych środowisk
---
spring:
  profiles: production
features:
  new-payment-gateway: false  # Bezpieczny start w prod
  user-recommendations:
    enabled: true
    rollout-percentage: 5     # Ostrożny rollout

2. Feature Toggle Service:

@Service
@RefreshScope  // Dla dynamicznej aktualizacji config
public class FeatureToggleService {
    
    @Value("${features.new-payment-gateway:false}")
    private boolean newPaymentGatewayEnabled;
    
    @Value("${features.user-recommendations.enabled:false}")
    private boolean userRecommendationsEnabled;
    
    @Value("${features.user-recommendations.rollout-percentage:0}")
    private int recommendationsRolloutPercentage;
    
    private final RedisTemplate redisTemplate;
    private final MeterRegistry meterRegistry;
    
    public FeatureToggleService(RedisTemplate redisTemplate, 
                               MeterRegistry meterRegistry) {
        this.redisTemplate = redisTemplate;
        this.meterRegistry = meterRegistry;
    }
    
    public boolean isNewPaymentGatewayEnabled() {
        // Logowanie użycia toggle dla monitoringu
        meterRegistry.counter("feature.toggle.check", 
            "feature", "new-payment-gateway", 
            "enabled", String.valueOf(newPaymentGatewayEnabled))
            .increment();
            
        return newPaymentGatewayEnabled;
    }
    
    public boolean isUserRecommendationsEnabled(String userId) {
        if (!userRecommendationsEnabled) {
            return false;
        }
        
        // Gradual rollout na podstawie user ID
        int userHash = Math.abs(userId.hashCode() % 100);
        boolean enabled = userHash < recommendationsRolloutPercentage;
        
        meterRegistry.counter("feature.toggle.rollout",
            "feature", "user-recommendations",
            "enabled", String.valueOf(enabled))
            .increment();
            
        return enabled;
    }
    
    // Circuit breaker pattern z Redis
    public boolean isFeatureHealthy(String featureName) {
        String key = "feature:health:" + featureName;
        String errorCount = redisTemplate.opsForValue().get(key);
        
        if (errorCount != null) {
            int errors = Integer.parseInt(errorCount);
            return errors < 10; // Threshold dla wyłączenia
        }
        return true;
    }
    
    public void recordFeatureError(String featureName) {
        String key = "feature:health:" + featureName;
        redisTemplate.opsForValue().increment(key);
        redisTemplate.expire(key, Duration.ofMinutes(5));
    }
}
Pro tip: Używaj @RefreshScope w Spring Cloud dla dynamicznej aktualizacji feature flags bez restartu aplikacji. Kombinuj z Spring Cloud Config Server dla centralizowanego zarządzania.

3. Użycie w warstwie biznesowej:

@Service
public class PaymentService {
    
    private final FeatureToggleService toggleService;
    private final LegacyPaymentGateway legacyGateway;
    private final NewPaymentGateway newGateway;
    
    public PaymentResult processPayment(PaymentRequest request) {
        try {
            if (toggleService.isNewPaymentGatewayEnabled() && 
                toggleService.isFeatureHealthy("new-payment-gateway")) {
                
                return processWithNewGateway(request);
            } else {
                return processWithLegacyGateway(request);
            }
        } catch (Exception e) {
            // Fallback + circuit breaker
            toggleService.recordFeatureError("new-payment-gateway");
            log.warn("New payment gateway failed, falling back to legacy", e);
            return processWithLegacyGateway(request);
        }
    }
    
    private PaymentResult processWithNewGateway(PaymentRequest request) {
        log.info("Processing payment with new gateway for amount: {}", request.getAmount());
        return newGateway.process(request);
    }
    
    private PaymentResult processWithLegacyGateway(PaymentRequest request) {
        return legacyGateway.process(request);
    }
}

@RestController
public class RecommendationController {
    
    private final FeatureToggleService toggleService;
    private final RecommendationService recommendationService;
    
    @GetMapping("/api/users/{userId}/recommendations")
    public ResponseEntity> getRecommendations(@PathVariable String userId) {
        
        if (toggleService.isUserRecommendationsEnabled(userId)) {
            List recommendations = recommendationService.getPersonalizedRecommendations(userId);
            return ResponseEntity.ok(recommendations);
        } else {
            // Fallback do podstawowych rekomendacji lub pustej listy
            return ResponseEntity.ok(recommendationService.getDefaultRecommendations());
        }
    }
}

Integracja z CI/CD pipeline

Feature flags pozwalają na oddzielenie deploymentu od release'u. Oto jak to wygląda w praktyce:

GitLab CI pipeline z feature flags:

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy-staging
  - deploy-production
  - feature-release

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

test:
  stage: test
  script:
    - ./gradlew test
    - ./gradlew integrationTest

build:
  stage: build
  script:
    - docker build -t $DOCKER_IMAGE .
    - docker push $DOCKER_IMAGE

deploy-staging:
  stage: deploy-staging
  script:
    - kubectl set image deployment/app app=$DOCKER_IMAGE -n staging
    - kubectl rollout status deployment/app -n staging
  environment:
    name: staging

deploy-production:
  stage: deploy-production
  script:
    - kubectl set image deployment/app app=$DOCKER_IMAGE -n production
    - kubectl rollout status deployment/app -n production
  environment:
    name: production
  when: manual

# Osobny stage dla kontroli feature flags
feature-release:
  stage: feature-release
  script:
    - |
      # Aktualizacja feature flags przez API
      curl -X PUT "$CONFIG_SERVER_URL/features/new-payment-gateway" \
        -H "Authorization: Bearer $API_TOKEN" \
        -d '{"enabled": true, "rollout": 10}'
    
    # Monitoring rollout
    - ./scripts/monitor-feature-rollout.sh new-payment-gateway
  when: manual
  only:
    - master
Kluczowa zasada: Deploy kodu i release funkcjonalności to dwa różne procesy. Kod trafia do produkcji z wyłączonymi toggles, a następnie stopniowo włączamy features monitorując metryki.

Zarządzanie lifecycle feature flags

Jednym z największych wyzwań z feature flags jest zarządzanie ich cyklem życia i unikanie technicznego długu:

Strategia usuwania toggles:

@Component
public class FeatureFlagCleanupService {
    
    private final FeatureToggleService toggleService;
    private final ApplicationEventPublisher eventPublisher;
    
    @Scheduled(cron = "0 0 2 * * MON") // Każdy poniedziałek o 2:00
    public void analyzeFeatureFlags() {
        Map stats = collectFeatureFlagStats();
        
        stats.entrySet().stream()
            .filter(this::shouldBeRemoved)
            .forEach(entry -> {
                log.warn("Feature flag {} is candidate for removal: {}", 
                    entry.getKey(), entry.getValue());
                    
                eventPublisher.publishEvent(
                    new FeatureFlagCleanupEvent(entry.getKey(), entry.getValue())
                );
            });
    }
    
    private boolean shouldBeRemoved(Map.Entry entry) {
        FeatureFlagStats stats = entry.getValue();
        
        // Kryteria dla usunięcia toggle
        return (stats.isEnabledForAllUsers() && stats.getDaysEnabled() > 30) ||
               (stats.hasNoUsageInDays(14)) ||
               (stats.getType() == ToggleType.RELEASE && stats.getDaysEnabled() > 7);
    }
}

// Event listener dla notyfikacji zespołu
@EventListener
public void handleFeatureFlagCleanup(FeatureFlagCleanupEvent event) {
    // Integracja ze Slack/Teams
    slackService.sendMessage("#dev-team", 
        String.format("🧹 Feature flag '%s' should be removed: %s", 
            event.getFeatureName(), event.getReason()));
            
    // Utworzenie task w Jira
    jiraService.createCleanupTask(event.getFeatureName(), event.getStats());
}

Monitoring i metryki

Monitoring feature flags jest kluczowy dla sukcesu continuous deployment:

Kluczowe metryki do śledzenia:

@Component
public class FeatureFlagMetrics {
    
    private final MeterRegistry meterRegistry;
    private final Timer.Sample sample;
    
    @EventListener
    public void handleFeatureToggleUsage(FeatureToggleUsageEvent event) {
        // Licznik użyć feature flag
        Counter.builder("feature.toggle.usage")
            .tag("feature", event.getFeatureName())
            .tag("enabled", String.valueOf(event.isEnabled()))
            .tag("user.segment", event.getUserSegment())
            .register(meterRegistry)
            .increment();
            
        // Histogram czasu wykonania z włączonym/wyłączonym feature
        Timer.builder("feature.execution.time")
            .tag("feature", event.getFeatureName())
            .tag("enabled", String.valueOf(event.isEnabled()))
            .register(meterRegistry)
            .record(event.getExecutionTime(), TimeUnit.MILLISECONDS);
    }
    
    // Gauge dla aktualnego stanu rollout
    @Gauge(name = "feature.rollout.percentage", description = "Current rollout percentage")
    public double getCurrentRolloutPercentage(@Tag("feature") String featureName) {
        return toggleService.getRolloutPercentage(featureName);
    }
    
    // Custom metric dla błędów związanych z feature
    public void recordFeatureError(String featureName, String errorType) {
        Counter.builder("feature.errors")
            .tag("feature", featureName)
            .tag("error.type", errorType)
            .register(meterRegistry)
            .increment();
    }
}
Uwaga: Feature flags zwiększają complexity kodu i mogą prowadzić do trudnych do debugowania problemów. Zawsze dokumentuj toggles i regularnie je usuwaj po zakończeniu rollout.

Strategie rollout w praktyce

1. Canary deployment z feature flags:

@Service
public class CanaryRolloutService {
    
    public boolean shouldUseNewFeature(String userId, String featureName) {
        CanaryConfig config = getCanaryConfig(featureName);
        
        // Whitelist użytkowników (internal testing)
        if (config.getWhitelistedUsers().contains(userId)) {
            return true;
        }
        
        // Percentage rollout
        int userHash = Math.abs(userId.hashCode() % 100);
        if (userHash < config.getRolloutPercentage()) {
            return true;
        }
        
        // Geographic rollout
        String userRegion = getUserRegion(userId);
        if (config.getEnabledRegions().contains(userRegion)) {
            return true;
        }
        
        return false;
    }
    
    @Scheduled(fixedRate = 300000) // Co 5 minut
    public void adjustRolloutBasedOnMetrics() {
        for (String feature : getActiveCanaryFeatures()) {
            FeatureMetrics metrics = getFeatureMetrics(feature);
            
            if (metrics.getErrorRate() > 0.05) { // 5% error rate
                // Reduce rollout or disable
                reduceRollout(feature, metrics.getErrorRate());
            } else if (metrics.getErrorRate() < 0.01 && metrics.getSuccessRate() > 0.95) {
                // Increase rollout
                increaseRollout(feature);
            }
        }
    }
}

Best practices i pułapki

Częste błędy:

  • Pozostawianie toggles na lata - tech debt rośnie wykładniczo
  • Brak testowania wszystkich kombinacji flag - exponential explosion
  • Używanie feature flags dla business logic zamiast config
  • Brak monitoring i alertów dla toggles
Pro tips dla 2020:

  • Używaj type-safe enum zamiast stringów dla nazw features
  • Integruj z Kubernetes ConfigMaps dla dynamic config
  • Implementuj A/B testing framework na bazie feature flags
  • Dokumentuj każdy toggle w code + confluence
Jak radzić sobie z testowaniem aplikacji z feature flags?

Utwórz test profile z predefiniowanymi kombinacjami toggles. Używaj @TestPropertySource w Spring Boot do kontrolowania flags w testach. Nie testuj wszystkich kombinacji - skup się na krytycznych ścieżkach biznesowych.

Czy feature flags wpływają na performance aplikacji?

Minimal overhead - proste if statement nie ma znaczącego wpływu. Problemem mogą być częste zapytania do external config store. Używaj local cache z TTL lub push notifications dla aktualizacji flags.

Jak zarządzać feature flags w microservices?

Centralizowany config server (Spring Cloud Config) + distributed cache (Redis). Każdy service może mieć własne toggles, ale shared features wymagają koordinacji. Używaj correlation ID do śledzenia flag across services.

Kiedy usuwać feature flag z kodu?

Release toggles: po pełnym rollout (7-14 dni). Experiment toggles: po zakończeniu A/B test. Ops toggles: mogą pozostać długo. Ustaw reminder w kalendarzu na review toggles co miesiąc.

Jak radzić sobie z database migrations przy feature flags?

Backwards compatible migrations first, potem code changes. Używaj separate migrations dla new features z toggles. Consider feature-specific database schemas lub column flags.

Czy można kombinować feature flags z blue-green deployment?

Tak! Deploy do green environment z disabled flags, potem switch traffic + enable flags postupnie. Daje to dodatkową warstwę bezpieczeństwa i możliwość instant rollback.

Jak implementować user-specific feature flags?

Hash user ID dla consistent behavior, używaj user attributes (region, subscription tier) dla targeting. Store user assignments w cache dla performance. Consider GDPR implications dla user data usage.

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Zaimplementuj feature flag system w swojej aplikacji Spring Boot: 1) Stwórz FeatureToggleService z konfiguracją w YAML, 2) Dodaj gradual rollout na podstawie user ID, 3) Zintegruj z Micrometer dla metrics, 4) Napisz testy dla różnych kombinacji flags, 5) Stwórz Jenkins job do kontrolowania rollout przez API. Bonus: dodaj Slack notifications dla feature flag changes!

Jakie są Twoje doświadczenia z feature flags w continuous deployment? Czy używasz zewnętrznych narzędzi jak LaunchDarkly czy budujesz własne rozwiązania?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry