Guía técnica

Kubernetes para SaaS: cuándo es la opción correcta, cuándo gana ECS, y qué elegimos

Kubernetes vs ECS vs Lambda para plataformas SaaS. Aislamiento multi-tenant, estrategias de despliegue, networking, optimización de costos, y el framework de decisión honesto tras operar los tres en producción.

26 de febrero de 202618 min de lecturaEquipo de Ingeniería Oronts

La decisión de Kubernetes

Todo equipo de ingeniería llega al punto de preguntarse: ¿deberíamos usar Kubernetes? La respuesta honesta: depende de lo que estés ejecutando, cuántos servicios tienes, y si puedes permitirte el overhead operacional.

Nosotros ejecutamos tres estrategias de compute diferentes en producción. Una plataforma corre sobre Kubernetes (7+ servicios, Pimcore, OpenSearch, workers). Otra corre sobre ECS Fargate + Lambda (serverless-first, event-driven). Una tercera usa una mezcla de ambos. Cada una fue la decisión correcta para su contexto.

Este artículo cubre el framework de decisión y los patrones de implementación para cada enfoque. Para cómo gestionamos la Infrastructure as Code detrás de estos despliegues, consulta nuestra guía de IaC. Para las arquitecturas de aplicación que corren encima, consulta nuestra guía de arquitectura de sistemas.

La comparación honesta

CriterioKubernetes (EKS/AKS/GKE)ECS FargateLambda
Complejidad operacionalAlta (actualizaciones de cluster, networking, RBAC)Media (definiciones de tareas, service mesh)Baja (solo despliega funciones)
Cold startNinguno (los pods siempre están corriendo)Ninguno (las tasks siempre están corriendo)100ms-5s (depende del runtime/paquete)
Velocidad de escaladoMinutos (scheduling de pods + scaling de nodes)Segundos (lanzamiento de tasks)Milisegundos (invocaciones concurrentes)
Costo en reposoAlto (mínimo 2-3 nodes corriendo siempre)Medio (pago por task en ejecución)Cero (pago por invocación)
Costo bajo cargaBajo (packing eficiente, spot instances)Medio (packing menos eficiente)Puede ser alto (precio por invocación)
Workloads statefulBueno (PVCs, StatefulSets)Limitado (solo EFS)No soportado
Procesos de larga duraciónIlimitadoIlimitado15 min máximo
EcosistemaEnorme (Helm, operators, service mesh)Nativo de AWSNativo de AWS
Multi-cloudSí (mismos manifests, diferentes providers)Solo AWSSolo AWS
Requisito de habilidades del equipoAlto (se necesita expertise en K8s)Medio (conocimiento de AWS)Bajo (solo escribe funciones)
Ideal paraSistemas multi-servicio complejos, workloads statefulMicroservicios simples, containers sin overhead de K8sEvent-driven, endpoints de API, tareas programadas

El desglose real de costos

Para una plataforma SaaS típica con 5 servicios:

ComponenteKubernetes (EKS)ECS FargateLambda + API Gateway
Compute (mensual)~$600 (3 nodes t3.large + pods)~$450 (5 servicios, 0.5 vCPU cada uno)~$50-500 (depende del tráfico)
Control plane$73/mes (tarifa EKS)GratisGratis
Load balancer$25/mes (ALB)$25/mes (ALB)Incluido en API GW
Networking (NAT)$45/mes$45/mes$45/mes
Monitoring$50-200/mes$50-200/mes$50-200/mes
Total (tráfico bajo)~$800-1,000/mes~$570-720/mes~$200-800/mes
Total (tráfico alto)~$1,500-3,000/mes~$2,000-4,000/mes~$3,000-10,000/mes

Kubernetes es el más barato bajo carga (bin-packing eficiente, spot instances, capacidad reservada). Lambda es el más barato con poco tráfico (no pagas nada en reposo). ECS Fargate es el punto medio.

Cuándo elegir Kubernetes

Elige Kubernetes cuando tengas:

Sistemas multi-servicio complejos. Si estás corriendo 7+ servicios con interdependencias, configuración compartida, service discovery y despliegues coordinados, Kubernetes orquesta esto bien. Los containers Docker individuales en ECS se vuelven difíciles de gestionar a esta escala.

Workloads stateful. Bases de datos, motores de búsqueda (OpenSearch, MeiliSearch), message brokers (RabbitMQ) y clusters de cache (Redis) se benefician de los StatefulSets, PersistentVolumeClaims y operators de Kubernetes. Ejecutarlos en ECS requiere servicios administrados externos para cada componente stateful.

Requisitos multi-cloud. Los manifests de Kubernetes funcionan en cualquier proveedor cloud. ECS y Lambda son exclusivos de AWS. Si necesitas correr en AWS y Azure (o podrías necesitarlo en el futuro), Kubernetes es la opción portable.

Un equipo de plataforma. Kubernetes requiere mantenimiento continuo: actualizaciones del cluster (cada 3-4 meses para parches de seguridad), gestión de grupos de nodes, configuración de red (ingress controllers, network policies) y gestión de RBAC. Sin una persona o equipo dedicado manejando esto, el overhead operacional ralentizará toda la organización de ingeniería.

Arquitectura Kubernetes para una plataforma PIM/Commerce

┌─────────────────────────────────────────────────────────────┐
│                    Kubernetes Cluster                         │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
│  │ Ingress      │  │ Cert-Manager│  │ External DNS        │ │
│  │ (Nginx/Traefik)│ │ (Let's Encrypt)│ │ (Route53 sync)  │ │
│  └──────┬───────┘  └─────────────┘  └─────────────────────┘ │
│         │                                                     │
│  ┌──────▼──────────────────────────────────────────────────┐ │
│  │                    Namespaces                            │ │
│  │                                                          │ │
│  │  ┌──────────────────────────────────────────────────┐   │ │
│  │  │  production namespace                             │   │ │
│  │  │                                                    │   │ │
│  │  │  pimcore-web (2-4 réplicas)                       │   │ │
│  │  │  pimcore-worker (1-3 réplicas)                    │   │ │
│  │  │  pimcore-ops (1 réplica, mantenimiento)           │   │ │
│  │  │  frontend (2-3 réplicas)                          │   │ │
│  │  └──────────────────────────────────────────────────┘   │ │
│  │                                                          │ │
│  │  ┌──────────────────────────────────────────────────┐   │ │
│  │  │  data namespace                                   │   │ │
│  │  │                                                    │   │ │
│  │  │  mysql (StatefulSet, 1 réplica o administrado)    │   │ │
│  │  │  redis (StatefulSet, 1 réplica o administrado)    │   │ │
│  │  │  opensearch (StatefulSet, 2-3 réplicas)           │   │ │
│  │  │  rabbitmq (StatefulSet, 1-3 réplicas)             │   │ │
│  │  └──────────────────────────────────────────────────┘   │ │
│  │                                                          │ │
│  │  ┌──────────────────────────────────────────────────┐   │ │
│  │  │  flux-system namespace (controlador GitOps)       │   │ │
│  │  └──────────────────────────────────────────────────┘   │ │
│  └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Estrategia de despliegue: GitOps con Flux

Usamos Flux para despliegues basados en GitOps. El repositorio Git es la fuente única de verdad. Flux reconcilia el estado del cluster con el repositorio cada minuto.

# flux-system/kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
    name: platform
    namespace: flux-system
spec:
    interval: 1m
    sourceRef:
        kind: GitRepository
        name: infrastructure
    path: ./kubernetes/resources/overlay/prod
    prune: true  # Eliminar recursos borrados de Git
    healthChecks:
        - apiVersion: apps/v1
          kind: Deployment
          name: pimcore
          namespace: production

Ventajas sobre kubectl apply o despliegues dirigidos por CI:

  • Detección y corrección de drift. Si alguien cambia un recurso manualmente, Flux lo revierte en menos de 1 minuto.
  • Git como registro de auditoría. Cada cambio es un commit de Git con autor, timestamp y diff.
  • Sin credenciales del cluster en el CI. Flux tira desde Git. El CI empuja a Git. El pipeline de CI nunca necesita acceso a kubectl.
  • El rollback es git revert. Revierte el commit, Flux reconcilia, rollback completado.

Kustomize para overlays de entorno

kubernetes/resources/
├── base/
│   ├── deployments/
│   │   ├── pimcore.yaml
│   │   ├── frontend.yaml
│   │   └── worker.yaml
│   ├── services/
│   ├── configmaps/
│   └── kustomization.yaml
├── overlay/
│   ├── prod/
│   │   ├── patches/
│   │   │   ├── pimcore-replicas.yaml    # 4 réplicas
│   │   │   ├── resource-limits.yaml      # CPU/memoria más altos
│   │   │   └── env-secrets.yaml          # Secrets de producción
│   │   └── kustomization.yaml
│   ├── staging/
│   │   ├── patches/
│   │   │   ├── pimcore-replicas.yaml    # 1 réplica
│   │   │   └── resource-limits.yaml      # Límites más bajos
│   │   └── kustomization.yaml
│   └── dev/
│       └── kustomization.yaml

Los manifests base definen la estructura común. Los overlays parchean las diferencias específicas de cada entorno (réplicas, límites de recursos, secrets, dominios). Misma aplicación, diferente configuración por entorno.

Cuándo gana ECS Fargate

Elegimos ECS Fargate + Lambda para una plataforma de comercio en lugar de Kubernetes. Las razones:

Operaciones más simples. Sin actualizaciones de cluster, sin gestión de nodes, sin configuración RBAC. ECS maneja el scheduling, el scaling y los health checks. El equipo se enfoca en el código de la aplicación, no en la infraestructura.

Escalado más rápido. ECS Fargate lanza nuevas tasks en segundos. Kubernetes necesita programar pods, potencialmente esperar al scaling de nodes (minutos) y pasar health checks. Para picos de tráfico, Fargate responde más rápido.

Mejor costo para workloads variables. Pago por task en ejecución, no por node. Si el tráfico cae a cero por la noche, los costos bajan proporcionalmente. Los nodes de Kubernetes siguen corriendo (y cobrando) independientemente de la carga.

// Definición del servicio ECS (vía CDK)
const service = new ecs.FargateService(this, 'ApiService', {
    cluster,
    taskDefinition,
    desiredCount: 2,
    assignPublicIp: false,
    vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
    circuitBreaker: { rollback: true },  // Rollback automático en caso de fallo de despliegue
    capacityProviderStrategies: [
        { capacityProvider: 'FARGATE_SPOT', weight: 2 },  // 66% spot
        { capacityProvider: 'FARGATE', weight: 1 },        // 33% on-demand
    ],
});

// Auto-scaling
const scaling = service.autoScaleTaskCount({ minCapacity: 2, maxCapacity: 10 });
scaling.scaleOnCpuUtilization('CpuScaling', { targetUtilizationPercent: 70 });
scaling.scaleOnRequestCount('RequestScaling', {
    targetGroup,
    requestsPerTarget: 1000,
});

Lambda para workloads event-driven

Las funciones Lambda manejan workloads event-driven que no justifican un servicio persistente:

// Lambda para procesamiento de webhooks
const webhookHandler = new lambda.Function(this, 'WebhookHandler', {
    runtime: lambda.Runtime.NODEJS_20_X,
    handler: 'webhook.handler',
    timeout: cdk.Duration.seconds(30),
    memorySize: 256,
    environment: {
        TABLE_NAME: table.tableName,
        QUEUE_URL: queue.queueUrl,
    },
});

// API Gateway dispara Lambda
const api = new apigateway.RestApi(this, 'WebhookApi');
api.root.addResource('webhook').addMethod('POST',
    new apigateway.LambdaIntegration(webhookHandler)
);

El híbrido: ECS + Lambda

La arquitectura que usamos más frecuentemente para plataformas de comercio:

ComponenteCorre enPor qué
Commerce API (Vendure)ECS FargateDe larga duración, sesiones stateful
Servicio workerECS FargateConsumidor de cola persistente
Handlers de webhookLambdaEvent-driven, tráfico esporádico
Tareas programadasLambda + EventBridgeTipo cron, no necesita proceso persistente
Procesamiento de imágenesLambdaCPU-intensivo, paralelizable
Indexación de búsquedaLambda + SQSEvent-driven, por ráfagas
Dashboard de adminECS Fargate o S3+CloudFrontAssets estáticos o SSR

La API de comercio y los workers corren en Fargate (persistente, de larga duración). Todo lo event-driven corre en Lambda (pago por uso, auto-scaling). La combinación es más barata que correr todo en Kubernetes y más simple que correr todo en Lambda.

Aislamiento multi-tenant en Kubernetes

Si corres un SaaS multi-tenant en Kubernetes, el aislamiento de tenants necesita configuración explícita:

Aislamiento por namespace

# Network policy: los pods en el namespace tenant-a solo pueden comunicarse entre sí
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
    name: tenant-isolation
    namespace: tenant-a
spec:
    podSelector: {}
    policyTypes:
        - Ingress
        - Egress
    ingress:
        - from:
            - namespaceSelector:
                matchLabels:
                    tenant: tenant-a
    egress:
        - to:
            - namespaceSelector:
                matchLabels:
                    tenant: tenant-a
        - to:  # Permitir resolución DNS
            - namespaceSelector: {}
              podSelector:
                matchLabels:
                    k8s-app: kube-dns
            ports:
                - port: 53
                  protocol: UDP

Resource quotas

Evita que un tenant consuma todos los recursos del cluster:

apiVersion: v1
kind: ResourceQuota
metadata:
    name: tenant-a-quota
    namespace: tenant-a
spec:
    hard:
        requests.cpu: "4"          # Máx 4 núcleos CPU
        requests.memory: "8Gi"     # Máx 8GB de RAM
        limits.cpu: "8"
        limits.memory: "16Gi"
        pods: "20"                 # Máx 20 pods
        services: "10"
        persistentvolumeclaims: "5"

El problema del vecino ruidoso

Incluso con resource quotas, un workload con I/O intensivo de un tenant puede afectar a otros en el mismo node. Soluciones:

EstrategiaNivel de aislamientoImpacto en el costo
Nodes compartidos, resource quotasSuave (CPU/memoria limitados, I/O compartido)El más bajo
Node affinity (pools de nodes dedicados)Medio (nodes dedicados por tenant)Más alto
Clusters dedicadosCompleto (infraestructura completamente separada)El más alto

Para la mayoría de aplicaciones SaaS, los nodes compartidos con resource quotas son suficientes. Reserva pools de nodes dedicados para tenants enterprise con requisitos estrictos de aislamiento. Para los patrones de aislamiento a nivel de aplicación (middleware de API, filtros de consulta, políticas), consulta nuestra guía de diseño multi-tenant.

Optimización de costos

Spot instances (Kubernetes)

Las spot instances son un 60-90% más baratas que on-demand. Úsalas para workloads stateless que toleren interrupciones:

# EKS managed node group con spot instances
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
    name: production
    region: eu-central-1
managedNodeGroups:
    - name: spot-workers
      instanceTypes: ["t3.large", "t3.xlarge", "m5.large"]
      spot: true
      minSize: 2
      maxSize: 10
      desiredCapacity: 3
      labels:
          node-type: spot
    - name: on-demand-workers
      instanceTypes: ["t3.large"]
      minSize: 1
      maxSize: 3
      desiredCapacity: 1
      labels:
          node-type: on-demand

Corre los servicios stateless (servidores web, workers) en spot. Corre los servicios stateful (bases de datos, motores de búsqueda) en on-demand. Usa pod anti-affinity para distribuir las réplicas entre nodes de forma que una interrupción spot no tire todas las réplicas.

Dimensionamiento correcto

La mayoría de equipos sobre-aprovisionan. Un servicio solicitando 1 CPU y 2GB de RAM quizás en realidad usa 0.2 CPU y 400MB. El sobre-aprovisionamiento desperdicia dinero. El sub-aprovisionamiento causa OOM kills.

# Verificar el uso real de recursos vs las solicitudes
kubectl top pods -n production
# Comparar con las solicitudes de recursos en los manifests de despliegue
kubectl get pods -n production -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].resources.requests}{"\n"}{end}'

Usa el Vertical Pod Autoscaler (VPA) en modo recomendación para ver lo que tus pods realmente necesitan:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
    name: pimcore-vpa
spec:
    targetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: pimcore
    updatePolicy:
        updateMode: "Off"  # Solo recomendación, no aplicar automáticamente

Autoscaling

El Horizontal Pod Autoscaler (HPA) escala basándose en métricas:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
    name: pimcore-hpa
spec:
    scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: pimcore
    minReplicas: 2
    maxReplicas: 8
    metrics:
        - type: Resource
          resource:
              name: cpu
              target:
                  type: Utilization
                  averageUtilization: 70
        - type: Resource
          resource:
              name: memory
              target:
                  type: Utilization
                  averageUtilization: 80
    behavior:
        scaleDown:
            stabilizationWindowSeconds: 300  # Esperar 5 min antes de reducir
            policies:
                - type: Pods
                  value: 1
                  periodSeconds: 60  # Eliminar máx 1 pod por minuto

Los stabilizationWindowSeconds previenen el flapping (escalar arriba, escalar abajo, escalar arriba). Las scaleDown.policies previenen un scale-down agresivo que podría causar problemas de capacidad durante el siguiente pico de tráfico.

El campo minado del networking

El networking de Kubernetes es donde la mayoría de equipos se quedan atascados.

Ingress controllers

ControladorIdeal paraComplejidad
Nginx IngressUso general, el más comúnBaja
TraefikAuto-discovery, Let's Encrypt integradoBaja
AWS ALB IngressNativo de AWS, integración con WAFMedia
Istio GatewayService mesh, mTLS, gestión de tráficoAlta

Para la mayoría de plataformas SaaS, Nginx Ingress + cert-manager (Let's Encrypt) es suficiente. Agrega un service mesh (Istio, Linkerd) solo si necesitas mTLS entre servicios, enrutamiento de tráfico avanzado (canary deployments, traffic splitting), u observabilidad detallada servicio-a-servicio.

Problemas de resolución DNS

Un problema frecuente en producción: los pods no pueden resolver hostnames externos porque la configuración DNS está mal.

# Encontrar la IP correcta del servicio DNS en tu cluster
kubectl get svc -n kube-system kube-dns -o jsonpath='{.spec.clusterIP}'

# Si las configs de nginx referencian un resolver, usa esta IP
# Error común: usar 10.0.0.10 cuando el DNS real está en 10.2.0.10

Si tu sidecar de nginx hace proxy de peticiones a servicios externos (almacenamiento cloud, APIs externas), la directiva resolver debe apuntar a la IP de kube-dns del cluster, no a un valor hardcodeado.

Errores comunes

  1. Elegir Kubernetes porque "todos lo usan." Si tienes 3 servicios y un equipo pequeño, ECS Fargate es más simple y más barato. Kubernetes tiene sentido a partir de 7+ servicios con un equipo de plataforma.

  2. Sin GitOps. kubectl apply desde el portátil de un desarrollador no es una estrategia de despliegue. Usa Flux o ArgoCD para despliegues basados en reconciliación.

  3. Cluster compartido sin resource quotas. Un tenant o un pod descontrolado consume todos los recursos. Cada namespace necesita resource quotas.

  4. Todos los pods en instancias on-demand. Las spot instances son un 60-90% más baratas para workloads stateless. Úsalas para servidores web y workers.

  5. Sobre-aprovisionar recursos. Pods solicitando 2 CPU y usando 0.2 CPU desperdician dinero. Usa las recomendaciones de VPA para dimensionar correctamente.

  6. Autoscaling agresivo. Reducir demasiado rápido causa problemas de capacidad en el siguiente pico. Usa ventanas de estabilización y políticas de scale-down graduales.

  7. Sin network policies. Sin ellas, cualquier pod puede comunicarse con cualquier otro pod en el cluster. En un setup multi-tenant, esto es un problema de seguridad.

  8. Ignorar las actualizaciones del cluster. Las versiones de Kubernetes llegan a su fin de vida cada 12-15 meses. Planifica ventanas de actualización trimestrales. Quedarte atrás crea vulnerabilidades de seguridad y bloquea nuevas funcionalidades.

  9. Mezclar stateful y stateless en los mismos nodes. Un pod de OpenSearch y un pod de servidor web compitiendo por I/O en el mismo node degrada a ambos. Usa node affinity para separarlos.

  10. Sin sealed secrets. Hacer commit de secrets en texto plano a Git es una brecha de seguridad esperando a ocurrir. Usa Sealed Secrets, External Secrets Operator, o AWS Secrets Manager.

Puntos clave

  • Kubernetes para plataformas multi-servicio complejas. 7+ servicios, workloads stateful, requisitos multi-cloud, y un equipo que pueda manejar el overhead operacional.

  • ECS Fargate para workloads de containers más simples. Mismos containers, menos complejidad operacional. Mejor para equipos sin expertise en Kubernetes.

  • Lambda para workloads event-driven. Webhooks, tareas programadas, procesamiento de imágenes, y cualquier workload que sea por ráfagas y de corta duración. Cero costo en reposo.

  • El híbrido (ECS + Lambda) suele ser la mejor respuesta. Servicios persistentes en Fargate, trabajo event-driven en Lambda. Más barato que todo en Kubernetes, más simple que todo en Lambda.

  • GitOps con Flux ofrece reconciliación real. No solo automatización de despliegues. Detección de drift, registro de auditoría, y rollback vía git revert.

  • Las spot instances ahorran un 60-90% en workloads stateless. Corre servidores web y workers en spot. Las bases de datos y motores de búsqueda en on-demand.

  • El aislamiento multi-tenant necesita network policies y resource quotas. El aislamiento por namespace solo no es suficiente. Impón fronteras de red y límites de recursos por tenant.

Nosotros desplegamos y gestionamos infraestructura Kubernetes, ECS y Lambda como parte de nuestros servicios cloud. Si necesitas ayuda eligiendo una estrategia de compute u optimizando tu despliegue actual, habla con nuestro equipo o solicita un presupuesto. Consulta también nuestra guía de migración de Pimcore para patrones de despliegue Kubernetes específicos de Pimcore.

Temas cubiertos

Kubernetes SaaSK8s producciónKubernetes vs ECSECS Fargateingeniería de plataforma K8sKubernetes multi-tenantoptimización costos Kubernetes

¿Listo para construir sistemas de IA listos para producción?

Nuestro equipo se especializa en sistemas de IA listos para producción. Hablemos de cómo podemos ayudar.

Iniciar una conversación