Blog - Jorge Rodriguez Flores
← Volver al blog

Backend FinOps: Estrategias para Reducir Costos en Infraestructura Cloud sin Sacrificar Rendimiento

finops cloud cost aws infraestructura optimización kubernetes
Compartir: X / Twitter LinkedIn
Backend FinOps: Estrategias para Reducir Costos en Infraestructura Cloud sin Sacrificar Rendimiento

El gasto en infraestructura cloud se ha convertido en uno de los mayores rubros operativos de las empresas de tecnología. Según el informe State of the Cloud 2025 de Flexera, el 32% del presupuesto cloud se desperdicia en recursos sobredimensionados, instancias inactivas y arquitecturas ineficientes. El Backend FinOps emerge como la disciplina que une las prácticas financieras con la ingeniería de backend para atacar ese desperdicio de forma sistemática y sin comprometer la disponibilidad ni el rendimiento del sistema.

A diferencia del FinOps tradicional, que suele operar a nivel de cuentas y etiquetas de facturación, el Backend FinOps trabaja directamente en el código, la arquitectura y las decisiones de despliegue que generan esos costos. En este artículo exploramos las estrategias más efectivas que los equipos de ingeniería pueden aplicar hoy para recuperar eficiencia real en sus sistemas.

¿Por qué Backend FinOps y no Solo FinOps?

El FinOps convencional se centra en la visibilidad: dashboards, alertas de presupuesto, etiquetado de recursos y chargeback entre equipos. Es necesario, pero insuficiente. Saber que un servicio cuesta $40,000 al mes no te dice cómo reducirlo sin romper nada.

El Backend FinOps traslada la responsabilidad de los costos al equipo de ingeniería que tiene el contexto para actuar. Se apoya en tres pilares:

  • Observabilidad de costos a nivel de servicio: Cada microservicio conoce su costo por request, por usuario activo o por unidad de negocio procesada.
  • Eficiencia como métrica de producto: El costo por request o por transacción se trata con el mismo rigor que la latencia o el uptime.
  • Ingeniería guiada por costos: Las decisiones de arquitectura incluyen el análisis de costo-beneficio como factor de primer orden, no como reflexión posterior.

Rightsizing: El Punto de Partida Obligatorio

El rightsizing es el proceso de ajustar el tamaño de las instancias o contenedores al consumo real medido, no al estimado. Es la intervención con mayor retorno sobre la inversión porque requiere cero cambios en el código.

Identificar instancias sobredimensionadas

La mayoría de los proveedores cloud ofrecen recomendaciones de rightsizing nativas. En AWS, el servicio Compute Optimizer analiza los últimos 14 días de métricas de CPU, memoria, red y disco para recomendar el tipo de instancia óptimo:

# Obtener recomendaciones de rightsizing para EC2 con AWS CLI
aws compute-optimizer get-ec2-instance-recommendations \
  --region us-east-1 \
  --query 'instanceRecommendations[*].{Instance:instanceArn,Current:currentInstanceType,Recommended:recommendationOptions[0].instanceType,Saving:recommendationOptions[0].estimatedMonthlySavings.value}' \
  --output table

# Resultado típico:
# Instance                          Current      Recommended   Saving
# arn:aws:ec2:us-east-1:...i-abc123  m5.2xlarge   m5.large      $287/mes
# arn:aws:ec2:us-east-1:...i-def456  r5.4xlarge   r5.xlarge     $612/mes

Para contenedores en Kubernetes, la herramienta Vertical Pod Autoscaler (VPA) en modo recomendación analiza el uso histórico y sugiere los valores de requests y limits óptimos:

# Configurar VPA en modo recomendación (no modifica pods automáticamente)
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-gateway-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-gateway
  updatePolicy:
    updateMode: "Off"  # Solo recomienda, no modifica
  resourcePolicy:
    containerPolicies:
    - containerName: api-gateway
      minAllowed:
        cpu: 100m
        memory: 128Mi
      maxAllowed:
        cpu: 2000m
        memory: 2Gi
# Ver las recomendaciones generadas por VPA
kubectl describe vpa api-gateway-vpa

# Sección relevante del output:
# Recommendation:
#   Container Recommendations:
#     Container Name: api-gateway
#     Lower Bound:    cpu: 120m    memory: 256Mi
#     Target:         cpu: 380m    memory: 512Mi    ← usar estos valores
#     Upper Bound:    cpu: 800m    memory: 1Gi

Impacto real del rightsizing

Un caso representativo: un clúster de Kubernetes con 50 deployments configurados con requests: cpu: 1000m, memory: 2Gi por default (el valor que nadie cambió tras el prototipo). VPA revela que el consumo real promedio es cpu: 250m, memory: 400Mi. Reducir los requests al percentil 95 del consumo real permite al scheduler empaquetar más pods por nodo, reduciendo el número de nodos de 20 a 11 — un ahorro del 45% en el costo de cómputo sin ningún cambio funcional.

Spot Instances y Preemptible VMs: Cómputo al 70-90% de Descuento

Las spot instances (AWS), preemptible VMs (GCP) y spot VMs (Azure) ofrecen capacidad de cómputo no utilizada con descuentos de entre el 70% y el 90% respecto al precio bajo demanda. La contrapartida: el proveedor puede recuperarlas con un aviso de 2 minutos (AWS) o 30 segundos (GCP).

Esta restricción hace que muchos equipos las descarten por completo, pero con la arquitectura correcta son perfectamente viables para cargas de trabajo de producción:

Workloads aptos para spot instances

  • Workers de colas: Procesamiento de mensajes de SQS, Kafka o RabbitMQ. Si el worker muere, el mensaje vuelve a la cola y otro worker lo procesa.
  • Jobs batch y ETL: Pipelines de datos, reportes nocturnos, reentrenamientos de modelos ML. Deben ser idempotentes y tolerantes a reintentos.
  • Stateless API services con múltiples réplicas: Si tienes 10 réplicas de un servicio, perder 2 spot instances simultáneamente es tolerable.
  • CI/CD runners: Los agentes de build son efímeros por naturaleza, candidatos ideales para spot.

Estrategia de diversificación en AWS

{
  "LaunchTemplate": { "LaunchTemplateId": "lt-0abc123" },
  "MixedInstancesPolicy": {
    "InstancesDistribution": {
      "OnDemandBaseCapacity": 2,
      "OnDemandPercentageAboveBaseCapacity": 20,
      "SpotAllocationStrategy": "capacity-optimized"
    },
    "LaunchTemplate": {
      "Overrides": [
        { "InstanceType": "m5.xlarge" },
        { "InstanceType": "m5a.xlarge" },
        { "InstanceType": "m4.xlarge" },
        { "InstanceType": "m5d.xlarge" },
        { "InstanceType": "m5n.xlarge" }
      ]
    }
  },
  "MinSize": 4,
  "MaxSize": 20,
  "DesiredCapacity": 8
}

Esta configuración garantiza 2 instancias on-demand como base (para estabilidad), el 20% de la capacidad adicional en on-demand, y el 80% restante en spot distribuido entre 5 tipos de instancias similares. La estrategia capacity-optimized selecciona el pool spot con mayor capacidad disponible, minimizando las interrupciones.

Manejo de interrupciones en el código

import signal
import boto3
import requests
import threading

class SpotInterruptionHandler:
    """Detecta la señal de interrupción SIGTERM y drena el trabajo en curso."""

    def __init__(self, drain_timeout_seconds=90):
        self.drain_timeout = drain_timeout_seconds
        self.shutdown_event = threading.Event()
        signal.signal(signal.SIGTERM, self._handle_sigterm)

    def _handle_sigterm(self, signum, frame):
        print("SIGTERM recibido — iniciando drenado graceful")
        self.shutdown_event.set()

    def is_interrupted(self):
        """Verifica también el endpoint de metadata de AWS EC2."""
        try:
            resp = requests.get(
                "http://169.254.169.254/latest/meta-data/spot/termination-time",
                timeout=0.5
            )
            return resp.status_code == 200
        except Exception:
            return self.shutdown_event.is_set()

# Uso en un worker de cola
handler = SpotInterruptionHandler()

while not handler.is_interrupted():
    message = queue.receive(visibility_timeout=120)
    if message:
        process(message)         # lógica de negocio
        message.delete()         # solo se borra si terminó con éxito
    else:
        time.sleep(5)

print("Worker detenido limpiamente")

Autoscaling Inteligente: Pagar Solo por lo que se Usa

El autoscaling es la palanca más poderosa del Backend FinOps porque ataca el costo en tiempo real. El objetivo es que la capacidad provisionada se acerque lo máximo posible a la demanda en cada momento, eliminando el sobredimensionamiento estático que se provisiona para manejar picos que solo ocurren durante el 5% del tiempo.

Escalado horizontal basado en métricas de negocio

El error más común es escalar por CPU cuando la métrica relevante para el negocio es diferente. Un servicio de procesamiento de pagos debería escalar por número de transacciones por segundo, no por CPU. Un servicio de emails por profundidad de la cola.

# HPA basado en métrica personalizada: mensajes en cola SQS
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: payment-processor-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: payment-processor
  minReplicas: 2
  maxReplicas: 50
  metrics:
  - type: External
    external:
      metric:
        name: sqs_queue_depth
        selector:
          matchLabels:
            queue: payment-jobs
      target:
        type: AverageValue
        averageValue: "100"  # 1 réplica por cada 100 mensajes en cola
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # Esperar 5 min antes de reducir
      policies:
      - type: Percent
        value: 25
        periodSeconds: 60              # Reducir máximo 25% por minuto
    scaleUp:
      stabilizationWindowSeconds: 0   # Escalar hacia arriba inmediatamente
      policies:
      - type: Percent
        value: 100
        periodSeconds: 30              # Doblar capacidad cada 30s si es necesario

KEDA: Autoscaling hasta cero para workloads event-driven

KEDA (Kubernetes Event-Driven Autoscaling) permite escalar deployments hasta cero réplicas cuando no hay trabajo pendiente, eliminando el costo base de tener siempre instancias activas:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: email-sender-scaler
spec:
  scaleTargetRef:
    name: email-sender
  minReplicaCount: 0        # ← puede llegar a cero
  maxReplicaCount: 30
  cooldownPeriod: 300
  triggers:
  - type: aws-sqs-queue
    metadata:
      queueURL: https://sqs.us-east-1.amazonaws.com/123456789/email-queue
      queueLength: "50"     # 1 réplica por cada 50 mensajes
      awsRegion: us-east-1

Para un servicio de envío de emails que solo procesa carga durante ventanas específicas del día, pasar de 3 réplicas permanentes a escala-a-cero puede representar un ahorro del 70% en ese servicio específico.

Optimización de Bases de Datos: El Mayor Costo Oculto

Las bases de datos suelen ser el componente más costoso de la infraestructura backend y, paradójicamente, el menos optimizado desde la perspectiva de costos. Tres estrategias tienen el mayor impacto:

Connection pooling para reducir instancias

Cada conexión a una base de datos PostgreSQL consume entre 5 y 10 MB de RAM en el servidor. Una aplicación con 100 instancias que abre 10 conexiones cada una requiere una instancia de base de datos capaz de manejar 1,000 conexiones simultáneas — lo que implica provisionar más RAM de la que el workload real necesita.

Introducir PgBouncer como proxy de connection pooling permite que esas 1,000 conexiones de aplicación se multiplexen sobre 50-100 conexiones reales al servidor:

# pgbouncer.ini — configuración básica
[databases]
production = host=db.internal port=5432 dbname=myapp

[pgbouncer]
listen_port = 5432
listen_addr = 0.0.0.0
auth_type = md5
pool_mode = transaction     # Libera la conexión tras cada transacción
max_client_conn = 2000      # Conexiones desde aplicaciones
default_pool_size = 80      # Conexiones reales al servidor PostgreSQL
min_pool_size = 10
reserve_pool_size = 10
log_connections = 0
log_disconnections = 0

El resultado directo: se puede usar una instancia de RDS db.r6g.large (8 GB RAM, ~$200/mes) donde antes era necesaria una db.r6g.2xlarge (32 GB RAM, ~$700/mes) — solo por no gestionar las conexiones eficientemente.

Aurora Serverless v2 para cargas variables

Para bases de datos con carga muy variable (alto tráfico diurno, casi cero nocturno), Aurora Serverless v2 ajusta automáticamente la capacidad en incrementos de 0.5 ACUs, cobrando solo por lo consumido:

# Crear cluster Aurora Serverless v2
aws rds create-db-cluster \
  --db-cluster-identifier mi-app-serverless \
  --engine aurora-postgresql \
  --engine-version 15.4 \
  --serverless-v2-scaling-configuration MinCapacity=0.5,MaxCapacity=16 \
  --master-username admin \
  --manage-master-user-password

# El costo se mide en ACU-horas:
# 0.5 ACU (idle)   = ~$0.007/hora  = ~$5/mes
# 4   ACU (normal) = ~$0.056/hora  = ~$40/mes
# 16  ACU (pico)   = ~$0.224/hora  (solo durante picos)
# vs. db.r6g.large fija = ~$200/mes independientemente de la carga

Estrategia de caché para reducir lectura de base de datos

Implementar Redis o Memcached para cachear respuestas frecuentes reduce dramáticamente la carga sobre la base de datos, permitiendo usar instancias más pequeñas:

import redis
import json
import hashlib
from functools import wraps

redis_client = redis.Redis(host='cache.internal', port=6379, decode_responses=True)

def cache_result(ttl_seconds=300, key_prefix=''):
    """Decorator para cachear el resultado de una función en Redis."""
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            cache_key = f"{key_prefix}:{func.__name__}:{hashlib.md5(str(args+tuple(sorted(kwargs.items()))).encode()).hexdigest()}"
            cached = redis_client.get(cache_key)
            if cached:
                return json.loads(cached)
            result = await func(*args, **kwargs)
            redis_client.setex(cache_key, ttl_seconds, json.dumps(result, default=str))
            return result
        return wrapper
    return decorator

# Uso: cachear lista de productos por 5 minutos
@cache_result(ttl_seconds=300, key_prefix='catalog')
async def get_products(category_id: int, page: int = 1):
    return await db.query("SELECT * FROM products WHERE category_id=$1 LIMIT 20 OFFSET $2",
                          category_id, (page-1)*20)

Observabilidad de Costos: Medir para Optimizar

No se puede optimizar lo que no se mide. El Backend FinOps requiere instrumentar el sistema para exponer el costo a nivel de servicio, endpoint o incluso a nivel de cliente.

Métricas de costo por request

Usar el costo horario de la infraestructura y el throughput medido para calcular el costo por request en tiempo real:

from prometheus_client import Gauge, Counter
import time

# Métricas Prometheus para FinOps
cost_per_request = Gauge(
    'backend_cost_per_request_usd',
    'Costo estimado en USD por request procesado',
    ['service', 'endpoint']
)
requests_total = Counter(
    'backend_requests_total',
    'Total de requests procesados',
    ['service', 'endpoint', 'status']
)

class CostTracker:
    # Costo horario del servicio (instancias + DB + cache + red)
    HOURLY_COST_USD = 2.80

    def update_cost_metric(self, service: str, endpoint: str, rps: float):
        """Actualiza la métrica de costo por request basándose en RPS actual."""
        if rps > 0:
            cost = self.HOURLY_COST_USD / 3600 / rps  # costo por segundo / requests por segundo
            cost_per_request.labels(service=service, endpoint=endpoint).set(cost)

# Configurar alerta en Grafana cuando el costo por request supera el umbral
# alert: cost_per_request > 0.0005 (medio centavo por request)
# → indica ineficiencia o pico inesperado de recursos

Etiquetado de costos en AWS por microservicio

# Terraform: etiquetar todos los recursos de un servicio para cost tracking
locals {
  common_tags = {
    Service     = var.service_name
    Environment = var.environment
    Team        = var.team_name
    CostCenter  = var.cost_center
    ManagedBy   = "terraform"
  }
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.app.id
  instance_type = var.instance_type
  tags          = merge(local.common_tags, { Name = "${var.service_name}-app" })
}

resource "aws_rds_cluster" "db" {
  cluster_identifier = "${var.service_name}-db"
  engine             = "aurora-postgresql"
  tags               = local.common_tags
}

# Activar Cost Allocation Tags en la consola de AWS para
# desglosar costos por Service, Team y Environment en Cost Explorer

Arquitecturas Eficientes en Costo: Decisiones de Diseño que Importan

Más allá de optimizar infraestructura existente, las decisiones de arquitectura tienen un impacto de largo plazo sobre el costo. Estas son las más relevantes desde una perspectiva FinOps:

Procesamiento asíncrono vs. síncrono

Mover operaciones costosas del path síncrono (request-response) a procesamiento asíncrono con colas permite:

  • Usar spot instances para los workers (70-90% más barato)
  • Procesar en lote para aprovechar mejor cada instancia
  • Escalar workers independientemente del API
  • Aplicar rate limiting para evitar picos de costo inesperados

Selección inteligente de almacenamiento

El storage tiene múltiples niveles de costo con diferente perfil de acceso. Una estrategia de lifecycle automático mueve datos al nivel adecuado:

{
  "Rules": [{
    "ID": "FinOps-lifecycle-policy",
    "Status": "Enabled",
    "Transitions": [
      {
        "Days": 30,
        "StorageClass": "STANDARD_IA"  // -58% vs Standard
      },
      {
        "Days": 90,
        "StorageClass": "GLACIER_IR"   // -84% vs Standard
      },
      {
        "Days": 365,
        "StorageClass": "DEEP_ARCHIVE" // -95% vs Standard
      }
    ],
    "Expiration": {
      "Days": 2555  // Expirar tras 7 años (si regulación lo permite)
    }
  }]
}

Committed Use Discounts: planificar el baseline

Una vez que el rightsizing y el autoscaling están optimizados, se puede identificar el consumo base constante y cubrirlo con Reserved Instances (AWS) o Committed Use Discounts (GCP), obteniendo descuentos adicionales del 30-60% sobre el precio on-demand para esa porción predecible de la carga.

La regla general: nunca comprar reservas antes de optimizar. Comprar reservas sobre infraestructura sobredimensionada es bloquear presupuesto en ineficiencia durante 1-3 años.

Resultados Esperados y Hoja de Ruta

Un programa de Backend FinOps bien ejecutado típicamente produce los siguientes resultados acumulativos en 6 meses:

FASE 1 — Mes 1-2: Visibilidad y victorias rápidas
  Acción:    Rightsizing de instancias EC2/ECS/GKE
  Acción:    Etiquetado completo de recursos
  Acción:    Apagar instancias de desarrollo fuera de horario
  Ahorro:    15-25% del gasto total

FASE 2 — Mes 3-4: Optimización de cómputo
  Acción:    Migración de workers batch a spot instances
  Acción:    Implementación de autoscaling basado en métricas de negocio
  Acción:    KEDA para workloads event-driven
  Ahorro:    +20-30% adicional

FASE 3 — Mes 5-6: Optimización de datos y arquitectura
  Acción:    Connection pooling + Aurora Serverless para DBs variables
  Acción:    Lifecycle policies en S3/GCS
  Acción:    Capa de caché para reducir lectura de DB
  Ahorro:    +10-15% adicional

RESULTADO TOTAL: 45-70% de reducción en el gasto cloud
sobre una línea base sin optimización previa

El Backend FinOps no es un proyecto puntual sino una práctica continua. Los costos tienden a crecer con las nuevas funcionalidades si no hay un proceso sistemático de revisión. La clave es institucionalizar la eficiencia: incluir el análisis de costo en los design reviews, establecer presupuestos por servicio con alertas automáticas, y celebrar las victorias de ahorro con el mismo entusiasmo que las nuevas funcionalidades entregadas.

El equipo de ingeniería que adopta esta mentalidad no solo reduce la factura cloud — obtiene también un sistema más robusto, más observable y más fácil de operar, porque la eficiencia y la calidad operativa van de la mano.

Artículos relacionados