Módulo 6: Cloud Migration Patterns
6. Feature Flags para Cloud
Descripción
En esta cápsula vas a implementar feature flags para manejar servicios cloud que no están disponibles en todos los entornos. SageMaker existe en AWS pero no en LocalStack. CloudWatch Metrics funciona en AWS pero es limitado en local. Ciertas integraciones como SNS notifications o SES emails solo tienen sentido en staging/producción. En lugar de hardcodear if environment == "aws" por todo tu código, vas a diseñar un sistema de feature flags que habilita o deshabilita capacidades limpiamente, con fallbacks definidos para cuando una feature no está disponible.
Contexto: En la cápsula 04, tu ClientFactory ya lanza un error si pides SageMaker cuando no está habilitado. Eso es un inicio. Pero en un sistema real, no quieres que toda tu app falle porque una feature opcional no está disponible. Quieres que el flujo principal funcione siempre, y las features opcionales se habiliten cuando el entorno las soporte. Feature flags son el mecanismo para lograr eso — y en cloud, son especialmente importantes porque los entornos tienen capacidades diferentes.
El Problema: Capacidades Diferentes por Entorno
Qué está disponible dónde
Servicio/Feature LocalStack AWS Staging AWS Production
──────────────────────── ──────────── ──────────── ──────────────
S3 ✅ Completo ✅ Completo ✅ Completo
Lambda ✅ Completo ✅ Completo ✅ Completo
SageMaker Endpoints ❌ No soporta ✅ Disponible ✅ Disponible
CloudWatch Metrics ⚠️ Parcial ✅ Completo ✅ Completo
SNS Notifications ⚠️ Parcial ✅ Completo ✅ Completo
SES Email ❌ No soporta ✅ Sandbox ✅ Completo
Cost Tracking ❌ Sin sentido ✅ Disponible ✅ Disponible
IAM Enforcement ❌ No enforced ✅ Enforced ✅ Strict
El anti-patrón: condicionales por entorno en lógica de negocio
# ❌ Anti-patrón — tu código de negocio está lleno de condicionales de entorno
import os
def process_document(document: dict) -> dict:
result = run_inference(document)
if os.environ.get("ENVIRONMENT") == "production":
send_sns_notification(result)
if os.environ.get("ENVIRONMENT") in ("staging", "production"):
sagemaker_result = invoke_sagemaker(document)
result["sagemaker_enrichment"] = sagemaker_result
if os.environ.get("ENVIRONMENT") != "local":
publish_cloudwatch_metric("documents_processed", 1)
return result
Problemas:
- ❌ Lógica de negocio contaminada con decisiones de infraestructura
- ❌ Cada nuevo developer debe entender todos los condicionales
- ❌ Agregar un entorno nuevo (QA) requiere revisar cada
if - ❌ Imposible testear el flujo de producción en local
El Patrón: Feature Flags Declarativos
Diseño del sistema de feature flags
"""services/feature_flags.py — Sistema de feature flags para cloud."""
from dataclasses import dataclass, field
from typing import Any
from config.settings import Settings
@dataclass
class FeatureFlag:
"""Define una feature con su estado y metadata."""
name: str
enabled: bool
description: str
fallback: Any = None
requires_service: str | None = None
class FeatureFlags:
"""Gestiona feature flags basados en la configuración del entorno.
La lógica de negocio pregunta "¿está habilitada esta feature?"
en lugar de "¿estoy en AWS?"
"""
def __init__(self, settings: Settings):
self.settings = settings
self._flags: dict[str, FeatureFlag] = {}
self._register_defaults()
def _register_defaults(self):
"""Registra las feature flags del sistema."""
self.register(FeatureFlag(
name="sagemaker_enrichment",
enabled=self.settings.feature_sagemaker_enabled,
description="Enriquece resultados con SageMaker model inference",
fallback={"enriched": False, "reason": "SageMaker not available"},
requires_service="sagemaker-runtime",
))
self.register(FeatureFlag(
name="cloudwatch_metrics",
enabled=self.settings.is_aws,
description="Publica métricas a CloudWatch",
fallback=None,
))
self.register(FeatureFlag(
name="sns_notifications",
enabled=self.settings.is_aws,
description="Envía notificaciones via SNS",
fallback=None,
))
self.register(FeatureFlag(
name="cost_tracking",
enabled=self.settings.feature_cost_tracking,
description="Registra costos estimados por operación",
fallback=None,
))
self.register(FeatureFlag(
name="advanced_logging",
enabled=self.settings.feature_advanced_logging,
description="Logging detallado con request/response completo",
fallback=None,
))
self.register(FeatureFlag(
name="s3_versioning",
enabled=True,
description="Versionado de objetos en S3 (funciona en ambos entornos)",
fallback=None,
))
def register(self, flag: FeatureFlag):
"""Registra una feature flag."""
self._flags[flag.name] = flag
def is_enabled(self, name: str) -> bool:
"""Verifica si una feature está habilitada."""
flag = self._flags.get(name)
if flag is None:
return False
return flag.enabled
def get_fallback(self, name: str) -> Any:
"""Retorna el valor de fallback para una feature deshabilitada."""
flag = self._flags.get(name)
if flag is None:
return None
return flag.fallback
def execute_if_enabled(self, name: str, func, *args, **kwargs) -> Any:
"""Ejecuta una función solo si la feature está habilitada.
Si está deshabilitada, retorna el fallback.
Si está habilitada pero falla, retorna el fallback.
"""
if not self.is_enabled(name):
return self.get_fallback(name)
try:
return func(*args, **kwargs)
except Exception as e:
flag = self._flags.get(name)
if flag:
import logging
logging.getLogger(__name__).warning(
f"Feature '{name}' habilitada pero falló: {e}. "
f"Usando fallback."
)
return self.get_fallback(name)
def status(self) -> dict:
"""Retorna el estado de todas las feature flags."""
return {
name: {
"enabled": flag.enabled,
"description": flag.description,
"has_fallback": flag.fallback is not None,
}
for name, flag in sorted(self._flags.items())
}
def report(self):
"""Imprime un reporte de feature flags."""
print(f"\nFeature Flags ({self.settings.environment}):")
for name, flag in sorted(self._flags.items()):
icon = "✅" if flag.enabled else "❌"
fallback = " (fallback definido)" if flag.fallback is not None else ""
print(f" {icon} {name}: {flag.description}{fallback}")
Uso en Lógica de Negocio
DocumentProcessor con feature flags
"""services/document_processor.py — Processor con feature flags integrados."""
import json
import logging
from datetime import datetime
from typing import Any
logger = logging.getLogger(__name__)
class DocumentProcessor:
"""Procesa documentos con features opcionales controladas por flags."""
def __init__(
self,
s3_client: Any,
bucket: str,
feature_flags,
sagemaker_client: Any = None,
cloudwatch_client: Any = None,
):
self.s3 = s3_client
self.bucket = bucket
self.flags = feature_flags
self.sagemaker = sagemaker_client
self.cloudwatch = cloudwatch_client
def process(self, prompt_name: str, prompt_version: str, document: dict) -> dict:
"""Procesa un documento con todas las features habilitadas."""
result = self._core_processing(prompt_name, prompt_version, document)
enrichment = self.flags.execute_if_enabled(
"sagemaker_enrichment",
self._enrich_with_sagemaker,
document,
)
if enrichment:
result["sagemaker_enrichment"] = enrichment
self.flags.execute_if_enabled(
"cloudwatch_metrics",
self._publish_metric,
"documents_processed",
1,
)
self.flags.execute_if_enabled(
"cost_tracking",
self._track_cost,
result,
)
if self.flags.is_enabled("advanced_logging"):
logger.info(f"Full result: {json.dumps(result, default=str)}")
else:
logger.info(f"Processed: {result.get('document_key', 'unknown')}")
return result
def _core_processing(
self, prompt_name: str, prompt_version: str, document: dict
) -> dict:
"""Procesamiento core — siempre se ejecuta, sin feature flags."""
key = f"prompts/{prompt_name}/{prompt_version}/system.txt"
response = self.s3.get_object(Bucket=self.bucket, Key=key)
template = response["Body"].read().decode("utf-8")
doc_key = f"documents/inbox/{document.get('id', 'unknown')}.json"
self.s3.put_object(
Bucket=self.bucket,
Key=doc_key,
Body=json.dumps(document, ensure_ascii=False).encode("utf-8"),
)
return {
"prompt_used": f"{prompt_name}/{prompt_version}",
"document_key": doc_key,
"processed": True,
"template_preview": template[:100],
}
def _enrich_with_sagemaker(self, document: dict) -> dict:
"""Enriquece con SageMaker (solo AWS)."""
if not self.sagemaker:
return {"enriched": False, "reason": "SageMaker client not provided"}
response = self.sagemaker.invoke_endpoint(
EndpointName="document-enrichment",
ContentType="application/json",
Body=json.dumps(document).encode("utf-8"),
)
payload = json.loads(response["Body"].read().decode("utf-8"))
return {"enriched": True, "data": payload}
def _publish_metric(self, metric_name: str, value: float):
"""Publica métrica a CloudWatch (solo AWS)."""
if not self.cloudwatch:
return
self.cloudwatch.put_metric_data(
Namespace="AIService",
MetricData=[{
"MetricName": metric_name,
"Value": value,
"Unit": "Count",
}],
)
def _track_cost(self, result: dict):
"""Registra costo estimado (solo cuando cost tracking está habilitado)."""
estimated_cost = 0.0001
now = datetime.utcnow()
cost_key = f"costs/{now.strftime('%Y/%m/%d')}/estimate.json"
try:
existing = self.s3.get_object(Bucket=self.bucket, Key=cost_key)
costs = json.loads(existing["Body"].read().decode("utf-8"))
except Exception:
costs = {"date": now.strftime("%Y-%m-%d"), "total": 0, "operations": 0}
costs["total"] += estimated_cost
costs["operations"] += 1
self.s3.put_object(
Bucket=self.bucket,
Key=cost_key,
Body=json.dumps(costs).encode("utf-8"),
)
Wiring con feature flags
"""app.py — Wiring con feature flags."""
from config.loader import get_settings
from clients.factory import ClientFactory
from services.feature_flags import FeatureFlags
from services.document_processor import DocumentProcessor
def create_app():
settings = get_settings()
factory = ClientFactory(settings)
flags = FeatureFlags(settings)
flags.report()
sagemaker_client = None
if flags.is_enabled("sagemaker_enrichment"):
try:
sagemaker_client = factory.get_client("sagemaker-runtime")
except Exception as e:
import logging
logging.warning(f"SageMaker client no disponible: {e}")
cloudwatch_client = None
if flags.is_enabled("cloudwatch_metrics"):
cloudwatch_client = factory.get_client("cloudwatch")
processor = DocumentProcessor(
s3_client=factory.s3,
bucket=settings.s3_bucket,
feature_flags=flags,
sagemaker_client=sagemaker_client,
cloudwatch_client=cloudwatch_client,
)
return {
"settings": settings,
"factory": factory,
"flags": flags,
"processor": processor,
}
Feature Flags Dinámicos: Cambiar sin Re-deploy
Flags desde S3 (runtime update)
"""services/dynamic_flags.py — Feature flags que se actualizan en runtime."""
import json
import time
import logging
from typing import Any
logger = logging.getLogger(__name__)
class DynamicFeatureFlags:
"""Feature flags que se recargan desde S3 periódicamente.
Permite habilitar/deshabilitar features sin re-deploy.
"""
def __init__(
self,
s3_client: Any,
bucket: str,
config_key: str = "config/feature-flags.json",
refresh_interval: int = 60,
):
self.s3 = s3_client
self.bucket = bucket
self.config_key = config_key
self.refresh_interval = refresh_interval
self._flags: dict[str, bool] = {}
self._last_refresh: float = 0
self._defaults: dict[str, bool] = {}
def set_defaults(self, defaults: dict[str, bool]):
"""Establece defaults para cuando S3 no está disponible."""
self._defaults = defaults
self._flags = {**defaults}
def refresh(self) -> bool:
"""Recarga flags desde S3 si ha pasado el intervalo."""
now = time.time()
if now - self._last_refresh < self.refresh_interval:
return False
try:
response = self.s3.get_object(
Bucket=self.bucket, Key=self.config_key
)
content = response["Body"].read().decode("utf-8")
remote_flags = json.loads(content)
self._flags = {**self._defaults, **remote_flags}
self._last_refresh = now
logger.info(f"Feature flags recargados desde S3: {self._flags}")
return True
except Exception as e:
logger.warning(
f"No se pudieron recargar feature flags desde S3: {e}. "
f"Usando valores actuales."
)
self._last_refresh = now
return False
def is_enabled(self, name: str) -> bool:
self.refresh()
return self._flags.get(name, False)
def upload_flags(self, flags: dict[str, bool]):
"""Sube nuevos flags a S3 (para administración)."""
self.s3.put_object(
Bucket=self.bucket,
Key=self.config_key,
Body=json.dumps(flags, indent=2).encode("utf-8"),
ContentType="application/json",
)
logger.info(f"Feature flags actualizados en S3: {flags}")
self._last_refresh = 0 # Forzar recarga en próximo is_enabled
Uso de flags dinámicos
from config.loader import get_settings
from clients.factory import ClientFactory
from services.dynamic_flags import DynamicFeatureFlags
settings = get_settings()
factory = ClientFactory(settings)
dynamic_flags = DynamicFeatureFlags(
s3_client=factory.s3,
bucket=settings.s3_bucket,
refresh_interval=30,
)
dynamic_flags.set_defaults({
"sagemaker_enrichment": settings.feature_sagemaker_enabled,
"cloudwatch_metrics": settings.is_aws,
"new_model_v2": False, # Feature nueva, deshabilitada por default
})
# Subir flags iniciales a S3
dynamic_flags.upload_flags({
"sagemaker_enrichment": True,
"cloudwatch_metrics": True,
"new_model_v2": False,
})
# En runtime, verificar feature
if dynamic_flags.is_enabled("new_model_v2"):
result = invoke_new_model(document)
else:
result = invoke_current_model(document)
# Para habilitar sin re-deploy:
# dynamic_flags.upload_flags({"new_model_v2": True})
# → En 30 segundos, todos los workers lo recogen
Health Endpoint con Feature Flags
/health que reporta estado de features
"""handler.py — Health endpoint con feature flags."""
import json
def health_handler(container) -> dict:
"""Health check que incluye estado de feature flags."""
flags_status = container.flags.status()
client_health = container.factory.health_check()
features_summary = {}
for name, info in flags_status.items():
if info["enabled"]:
service = container.flags._flags.get(name)
if service and service.requires_service:
svc = service.requires_service
features_summary[name] = {
"enabled": True,
"service_health": client_health.get(svc, "unknown"),
}
else:
features_summary[name] = {"enabled": True}
else:
features_summary[name] = {
"enabled": False,
"has_fallback": info["has_fallback"],
}
return {
"statusCode": 200,
"body": json.dumps({
"status": "healthy",
"environment": container.settings.environment,
"features": features_summary,
"services": client_health,
}),
}
Ejemplo de response en LocalStack:
{
"status": "healthy",
"environment": "local",
"features": {
"sagemaker_enrichment": {"enabled": false, "has_fallback": true},
"cloudwatch_metrics": {"enabled": false, "has_fallback": false},
"cost_tracking": {"enabled": false, "has_fallback": false},
"advanced_logging": {"enabled": false, "has_fallback": false},
"s3_versioning": {"enabled": true}
},
"services": {
"s3": "healthy"
}
}
Ejemplo en AWS staging:
{
"status": "healthy",
"environment": "staging",
"features": {
"sagemaker_enrichment": {"enabled": true, "service_health": "healthy"},
"cloudwatch_metrics": {"enabled": true},
"cost_tracking": {"enabled": true},
"advanced_logging": {"enabled": true},
"s3_versioning": {"enabled": true}
},
"services": {
"s3": "healthy",
"sagemaker-runtime": "healthy",
"cloudwatch": "healthy"
}
}
Troubleshooting
Problema 1: Feature flag habilitado pero el servicio no responde
La feature está enabled=True pero SageMaker no tiene endpoint desplegado.
# execute_if_enabled ya maneja esto — si la función lanza excepción,
# retorna el fallback. Pero necesitas verificar en logs:
# Logger output:
# WARNING: Feature 'sagemaker_enrichment' habilitada pero falló:
# EndpointNotFound. Usando fallback.
# Solución: verificar que el endpoint existe antes de habilitar
# o usar el health_check del factory para detección automática
Problema 2: Flags dinámicos no se actualizan
El refresh_interval no ha pasado, o S3 no es accesible.
# Verificar:
print(f"Last refresh: {dynamic_flags._last_refresh}")
print(f"Interval: {dynamic_flags.refresh_interval}")
print(f"Current flags: {dynamic_flags._flags}")
# Forzar refresh:
dynamic_flags._last_refresh = 0
dynamic_flags.refresh()
Problema 3: Feature flag en .env no coincide con el código
El .env tiene FEATURE_SAGEMAKER_ENABLED=true pero el Settings tiene otro nombre.
# Pydantic Settings mapea FEATURE_SAGEMAKER_ENABLED → feature_sagemaker_enabled
# La convención es: VARIABLE_DE_ENTORNO en UPPER_SNAKE_CASE
# se mapea a campo Python en lower_snake_case
# Si no coinciden, verifica:
settings = Settings()
print(f"feature_sagemaker_enabled: {settings.feature_sagemaker_enabled}")
Problema 4: Tests ignoran feature flags
Tests unitarios con mock no pasan por execute_if_enabled.
# Solución: testear con FeatureFlags mockeado
from unittest.mock import MagicMock
mock_flags = MagicMock()
mock_flags.is_enabled.return_value = True
mock_flags.execute_if_enabled.side_effect = lambda name, func, *a, **kw: func(*a, **kw)
# O crear FeatureFlags con settings de test:
test_settings = Settings(feature_sagemaker_enabled=True)
flags = FeatureFlags(test_settings)
assert flags.is_enabled("sagemaker_enrichment")
Ejercicios Prácticos
Ejercicio 1: Feature flag con porcentaje de rollout
Implementa un feature flag que se habilite para un porcentaje de requests (ej: 10% de las invocaciones usan el modelo nuevo, 90% usan el actual).
Ver solución
import random
from dataclasses import dataclass
@dataclass
class GradualFeatureFlag:
"""Feature flag con rollout gradual por porcentaje."""
name: str
description: str
rollout_percentage: float # 0.0 a 1.0
enabled: bool = True
def should_activate(self, request_id: str | None = None) -> bool:
"""Decide si activar la feature para este request.
Si se provee request_id, el resultado es determinístico
(mismo request_id → mismo resultado).
"""
if not self.enabled:
return False
if self.rollout_percentage >= 1.0:
return True
if self.rollout_percentage <= 0.0:
return False
if request_id:
hash_value = hash(request_id) % 100
return hash_value < (self.rollout_percentage * 100)
return random.random() < self.rollout_percentage
class GradualFlags:
def __init__(self):
self._flags: dict[str, GradualFeatureFlag] = {}
def register(self, flag: GradualFeatureFlag):
self._flags[flag.name] = flag
def should_activate(self, name: str, request_id: str | None = None) -> bool:
flag = self._flags.get(name)
if not flag:
return False
return flag.should_activate(request_id)
def report(self):
for name, flag in self._flags.items():
pct = flag.rollout_percentage * 100
status = "✅" if flag.enabled else "❌"
print(f" {status} {name}: {pct:.0f}% rollout — {flag.description}")
# Uso
flags = GradualFlags()
flags.register(GradualFeatureFlag(
name="new_model_v2",
description="Nuevo modelo de clasificación",
rollout_percentage=0.1,
))
flags.report()
# Simular 100 requests
activations = sum(
flags.should_activate("new_model_v2", f"req-{i}")
for i in range(100)
)
print(f"\nActivaciones en 100 requests: {activations} (~10 esperados)")
Ejercicio 2: Feature flag con dependencias
Crea un sistema donde una feature depende de otra (ej: "cost_tracking" requiere "cloudwatch_metrics"). Si la dependencia no está habilitada, la feature dependiente tampoco se habilita.
Ver solución
from dataclasses import dataclass, field
@dataclass
class DependentFeatureFlag:
name: str
enabled: bool
description: str
depends_on: list[str] = field(default_factory=list)
class DependencyAwareFlags:
def __init__(self):
self._flags: dict[str, DependentFeatureFlag] = {}
def register(self, flag: DependentFeatureFlag):
self._flags[flag.name] = flag
def is_enabled(self, name: str, visited: set | None = None) -> bool:
"""Verifica si una feature está habilitada, considerando dependencias."""
if visited is None:
visited = set()
if name in visited:
return False # Circular dependency protection
visited.add(name)
flag = self._flags.get(name)
if not flag:
return False
if not flag.enabled:
return False
for dep in flag.depends_on:
if not self.is_enabled(dep, visited):
return False
return True
def status(self) -> dict:
result = {}
for name, flag in self._flags.items():
effective = self.is_enabled(name)
blocked_by = []
if flag.enabled and not effective:
for dep in flag.depends_on:
if not self.is_enabled(dep):
blocked_by.append(dep)
result[name] = {
"configured": flag.enabled,
"effective": effective,
"blocked_by": blocked_by if blocked_by else None,
}
return result
# Uso
flags = DependencyAwareFlags()
flags.register(DependentFeatureFlag(
name="cloudwatch_metrics",
enabled=True,
description="Publish metrics to CloudWatch",
))
flags.register(DependentFeatureFlag(
name="cost_tracking",
enabled=True,
description="Track estimated costs",
depends_on=["cloudwatch_metrics"],
))
flags.register(DependentFeatureFlag(
name="cost_alerts",
enabled=True,
description="Alert on cost thresholds",
depends_on=["cost_tracking", "sns_notifications"],
))
flags.register(DependentFeatureFlag(
name="sns_notifications",
enabled=False, # Deshabilitado
description="Send SNS notifications",
))
status = flags.status()
for name, info in status.items():
icon = "✅" if info["effective"] else "❌"
blocked = f" (blocked by: {info['blocked_by']})" if info["blocked_by"] else ""
print(f" {icon} {name}: configured={info['configured']}, effective={info['effective']}{blocked}")
Ejercicio 3: Feature flags audit log
Implementa un sistema que registra cada vez que una feature flag se evalúa, creando un audit log que se puede analizar para entender el uso de features por entorno.
Ver solución
import json
from datetime import datetime
from collections import defaultdict
class AuditedFeatureFlags:
"""Feature flags con audit log de evaluaciones."""
def __init__(self, flags: dict[str, bool], environment: str):
self._flags = flags
self._environment = environment
self._audit_log: list[dict] = []
self._stats: dict[str, dict] = defaultdict(
lambda: {"checked": 0, "enabled": 0, "disabled": 0}
)
def is_enabled(self, name: str, context: dict | None = None) -> bool:
enabled = self._flags.get(name, False)
entry = {
"timestamp": datetime.utcnow().isoformat(),
"environment": self._environment,
"flag": name,
"result": enabled,
"context": context,
}
self._audit_log.append(entry)
self._stats[name]["checked"] += 1
if enabled:
self._stats[name]["enabled"] += 1
else:
self._stats[name]["disabled"] += 1
return enabled
def get_audit_log(self, flag_name: str | None = None) -> list[dict]:
if flag_name:
return [e for e in self._audit_log if e["flag"] == flag_name]
return self._audit_log
def get_stats(self) -> dict:
return dict(self._stats)
def export_audit(self, filepath: str):
with open(filepath, "w") as f:
json.dump({
"environment": self._environment,
"exported_at": datetime.utcnow().isoformat(),
"total_evaluations": len(self._audit_log),
"stats": self.get_stats(),
"log": self._audit_log,
}, f, indent=2)
print(f"Audit log exportado: {filepath} ({len(self._audit_log)} entries)")
# Uso
flags = AuditedFeatureFlags(
flags={"sagemaker_enrichment": False, "cost_tracking": True},
environment="local",
)
for i in range(10):
flags.is_enabled("sagemaker_enrichment", context={"request_id": f"req-{i}"})
flags.is_enabled("cost_tracking", context={"request_id": f"req-{i}"})
print("Stats:")
for flag, stats in flags.get_stats().items():
print(f" {flag}: {stats}")
flags.export_audit("feature-flags-audit.json")
Ejercicio 4: Migration checklist basada en feature flags
Crea una función que compare las feature flags entre dos entornos y genere un checklist de migración: qué features se van a habilitar, cuáles se van a deshabilitar, y qué servicios necesitan estar disponibles.
Ver solución
from config.settings import Settings, EnvironmentName
from services.feature_flags import FeatureFlags
def generate_migration_checklist(
source_env: str,
target_env: str,
) -> dict:
"""Genera checklist de migración basada en feature flags."""
source_settings = Settings(environment=source_env)
target_settings = Settings(environment=target_env)
source_flags = FeatureFlags(source_settings)
target_flags = FeatureFlags(target_settings)
checklist = {
"migration": f"{source_env} → {target_env}",
"newly_enabled": [],
"newly_disabled": [],
"unchanged": [],
"services_required": [],
"warnings": [],
}
all_flag_names = set(
list(source_flags._flags.keys()) + list(target_flags._flags.keys())
)
for name in sorted(all_flag_names):
source_enabled = source_flags.is_enabled(name)
target_enabled = target_flags.is_enabled(name)
if source_enabled == target_enabled:
checklist["unchanged"].append(name)
elif target_enabled and not source_enabled:
checklist["newly_enabled"].append(name)
flag = target_flags._flags.get(name)
if flag and flag.requires_service:
checklist["services_required"].append({
"feature": name,
"service": flag.requires_service,
})
else:
checklist["newly_disabled"].append(name)
checklist["warnings"].append(
f"Feature '{name}' se deshabilita en {target_env}. "
f"Verificar que el fallback es aceptable."
)
print(f"\n{'='*60}")
print(f"MIGRATION CHECKLIST: {source_env} → {target_env}")
print(f"{'='*60}")
if checklist["newly_enabled"]:
print(f"\n🟢 Features que se HABILITAN:")
for name in checklist["newly_enabled"]:
print(f" ✅ {name}")
if checklist["newly_disabled"]:
print(f"\n🔴 Features que se DESHABILITAN:")
for name in checklist["newly_disabled"]:
print(f" ❌ {name}")
if checklist["services_required"]:
print(f"\n🔧 Servicios REQUERIDOS en {target_env}:")
for req in checklist["services_required"]:
print(f" → {req['service']} (para {req['feature']})")
if checklist["warnings"]:
print(f"\n⚠️ WARNINGS:")
for w in checklist["warnings"]:
print(f" {w}")
return checklist
generate_migration_checklist("local", "staging")
Resumen
- Feature flags desacoplan capacidades de entornos. Tu código pregunta "¿está habilitada esta feature?" en lugar de "¿estoy en AWS?" La diferencia es que agregar un entorno nuevo no requiere tocar lógica de negocio.
execute_if_enabledes el método central. Ejecuta la función si está habilitada, retorna el fallback si no. Maneja excepciones gracefully.- Flags estáticos (config) vs dinámicos (S3). Config-based para flags que no cambian en runtime. S3-based para cambiar sin re-deploy.
- El health endpoint reporta feature flags. Cada entorno muestra qué features están activas y cuál es la salud de los servicios que requieren.
- Feature flags facilitan la migración. Puedes migrar en pasos: primero migra sin SageMaker (
FEATURE_SAGEMAKER_ENABLED=false), verifica, luego habilita SageMaker. - En la siguiente cápsula, extenderás esto con graceful degradation — qué pasa cuando un servicio que está habilitado falla en runtime.
Recursos Adicionales
- Martin Fowler — Feature Toggles — El artículo definitivo sobre feature flags
- LaunchDarkly — Feature Flag Best Practices — Best practices de feature flags
- AWS AppConfig — Feature flags como servicio en AWS
- 12 Factor App — Config — Configuración externalizada
- Feature Flags in Python — Implementaciones de feature flags en Python
- Gradual Rollout Strategies — Estrategias de rollout gradual