SaaS

Kubernetes vs Serverless para SaaS: ¿Cuál Elegir en 2025?

Comparativa exhaustiva de Kubernetes vs Serverless (AWS Lambda) para plataformas SaaS. Costos, escalabilidad, complejidad y casos de uso reales.

Equipo Nexgen
11 min de lectura
#Kubernetes#Serverless#AWS Lambda#SaaS#Cloud Architecture#DevOps
Kubernetes vs Serverless para SaaS: ¿Cuál Elegir en 2025?

Respuesta Directa

Usa Kubernetes si: Tienes tráfico predecible alto, DevOps experto en el equipo, necesitas control total de infraestructura o ejecutas workloads con estado. Más económico a gran escala (10K+ RPS).

Usa Serverless si: Estás en MVP/early stage, tráfico esporádico/impredecible, equipo pequeño sin DevOps o prefieres enfocarte en desarrollo sobre infraestructura. Más económico para bajo tráfico.

Introducción

La arquitectura de infraestructura es una de las decisiones más críticas al construir un SaaS. En 2025, las dos opciones dominantes son Kubernetes (K8s) y Serverless (principalmente AWS Lambda). Ambas son excelentes, pero para escenarios muy diferentes.

Comparativa Detallada

1. Modelo de Costos

Kubernetes

Costo = Infraestructura base + Nodos adicionales

Ejemplo para 10K RPS constante:
- 3 worker nodes (t3.large): $0.0832/hora × 3 × 730 horas = ~$182/mes
- 1 master node managed (EKS): $73/mes
- Load Balancer: $16/mes
- Total: ~$271/mes

Por request: $271 / (10,000 × 60 × 60 × 24 × 30) ≈ $0.0000001

Características:

  • Costo fijo + variable
  • Economía de escala fuerte
  • Pagas por capacidad, no por uso
  • Subutilización = desperdicio

Serverless (AWS Lambda)

Costo = Invocaciones + Duración de compute

Ejemplo para 10K RPS, 200ms promedio:
- Requests: 25.9B/mes × $0.20 / 1M = $5,184
- Compute: 25.9B × 0.2s × $0.0000166667/GB-s = $86,000+

Total: ~$91,000/mes 😱

Por request: $91,000 / 25.9B ≈ $0.0000035

Características:

  • Pago por uso real
  • Sin costo en idle time
  • Economía de escala débil
  • Perfecto para tráfico bajo/esporádico

Break-even Point

┌─────────────────────────────────────────┐
│  Costo Mensual vs Requests/seg         │
│                                          │
│  $10K ┤           ╱ Serverless          │
│       │          ╱                       │
│  $5K  ┤         ╱                        │
│       │        ╱                         │
│  $1K  ┤    ╱━━━━━━━━━ Kubernetes       │
│       │   ╱                              │
│  $500 ┤  ╱                               │
│       │ ╱                                │
│  $100 ┤━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  │
│       └─────────────────────────────────│
│         1  10  100  1K  10K  RPS        │
└─────────────────────────────────────────┘

Break-even: ~500-1,000 RPS constante

2. Escalabilidad

Kubernetes

Auto-scaling:

# hpa.yaml (Horizontal Pod Autoscaler)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3
  maxReplicas: 50
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

Características:

  • Escala en minutos (arrancar nuevos pods)
  • Límite: capacidad de nodos disponibles
  • Requiere cluster autoscaler para nodos
  • Predictivo: programar scale antes de picos

Serverless

Auto-scaling:

# serverless.yml
functions:
  api:
    handler: handler.main
    events:
      - http:
          path: /{proxy+}
          method: ANY
    reservedConcurrency: 100 # Límite opcional
    provisionedConcurrency: 10 # Warm instances

Características:

  • Escala instantáneamente (segundos)
  • Límite: Concurrency limit (1000 default, ajustable)
  • Zero configuration
  • Reactivo: escala automático con demanda

3. Cold Starts

Kubernetes

Cold start: ~5-30 segundos
- Tiempo para arrancar nuevo pod
- Puede pre-escalarse
- Mitigación: Mantener minReplicas > 0

Serverless

Cold start: ~500ms-3s (depende del runtime y tamaño)

Factores:
- Runtime: Node.js (~500ms), Python (~1s), Java (~3s)
- Package size: Más grande = más lento
- VPC: +500ms-1s si está en VPC

Mitigación:
- Provisioned Concurrency (costo adicional)
- Keep functions warm con pings
- Optimizar bundle size

Comparación:

  • K8s: Cold starts raros pero largos
  • Serverless: Cold starts frecuentes pero cortos

4. Complejidad Operacional

Kubernetes

Setup inicial:

# 1. Crear cluster (EKS)
eksctl create cluster \
  --name prod-cluster \
  --region us-east-1 \
  --nodegroup-name standard-workers \
  --node-type t3.large \
  --nodes 3 \
  --nodes-min 3 \
  --nodes-max 10

# 2. Configurar kubectl
aws eks update-kubeconfig --name prod-cluster

# 3. Deploy app
kubectl apply -f k8s/

# 4. Setup monitoring
kubectl apply -f monitoring/prometheus.yaml

# 5. Setup logging
kubectl apply -f logging/fluentd.yaml

# 6. Setup ingress
kubectl apply -f ingress/nginx.yaml

Conocimientos requeridos:

  • YAML manifests
  • Deployments, Services, Ingress
  • ConfigMaps y Secrets
  • Networking policies
  • Resource limits
  • Health checks

Equipo necesario: DevOps engineer experimentado

Serverless

Setup inicial:

# 1. Instalar Serverless Framework
npm install -g serverless

# 2. Crear proyecto
serverless create --template aws-nodejs-typescript

# 3. Deploy
serverless deploy

# Eso es todo! 🎉

Conocimientos requeridos:

  • Configuración YAML básica
  • Conceptos de functions
  • API Gateway (opcional)

Equipo necesario: Cualquier developer

5. Observabilidad y Debugging

Kubernetes

# Logs en tiempo real
kubectl logs -f deployment/api-server

# Métricas
kubectl top pods

# Debug de pod
kubectl exec -it api-server-abc123 -- /bin/sh

# Port forwarding
kubectl port-forward svc/api-server 8080:80

Herramientas típicas:

  • Prometheus + Grafana (métricas)
  • ELK Stack o Loki (logs)
  • Jaeger (distributed tracing)
  • Configuración manual completa

Serverless

# Logs (AWS CloudWatch)
serverless logs -f apiHandler -t

# Métricas (AWS CloudWatch automático)
- Invocations
- Duration
- Errors
- Throttles

Herramientas típicas:

  • CloudWatch Logs/Metrics (incluido)
  • X-Ray (tracing, requiere setup)
  • Dashbird o Lumigo (third-party)
  • Setup mínimo

Casos de Uso Reales

Caso 1: SaaS B2B con Tráfico Predecible

Perfil:

  • 5,000 empresas clientes
  • Tráfico: 8am-6pm lunes-viernes
  • 2,000 RPS promedio en horas pico
  • 200 RPS en horas valle

Recomendación: Kubernetes

Por qué:

  • Tráfico predecible permite sizing exacto
  • Alto RPS constante = K8s más económico
  • Workloads con estado (WebSockets, caching)
  • Control sobre networking necesario

Ahorro: ~70% vs Serverless

Caso 2: MVP de SaaS Early Stage

Perfil:

  • 50 usuarios beta
  • Tráfico: Muy esporádico
  • 10 RPS máximo
  • Budget limitado

Recomendación: Serverless

Por qué:

  • Casi cero tráfico en horas valle
  • No necesitas DevOps engineer
  • Deploy en minutos
  • Escala automático si despegas

Ahorro: ~95% vs K8s (por capacidad subutilizada)

Caso 3: SaaS con Picos Impredecibles

Perfil:

  • App de eventos/tickets
  • Tráfico: Bursty (picos 100x normales)
  • Base: 100 RPS, Picos: 10K RPS
  • Duración de picos: minutos-horas

Recomendación: Híbrido

Arquitectura:

┌─────────────────────┐
│   API Gateway       │
└────────┬────────────┘
         │
    ┌────┴─────┐
    │          │
┌───▼──┐  ┌───▼────┐
│ K8s  │  │ Lambda │
│ Base │  │ Burst  │
└──────┘  └────────┘

K8s: Tráfico base constante
Lambda: Picos temporales

Por qué:

  • K8s maneja base económicamente
  • Lambda absorbe picos sin overprovisioning K8s
  • Best of both worlds

Arquitectura Híbrida en Detalle

API Gateway con Routing

// AWS CDK - Definir routing híbrido
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as lambda from "aws-cdk-lib/aws-lambda";

const api = new apigateway.RestApi(this, "HybridApi");

// Rutas a Kubernetes (NLB)
const k8sIntegration = new apigateway.HttpIntegration(
  "http://k8s-nlb.region.elb.amazonaws.com"
);

api.root.addResource("users").addMethod("GET", k8sIntegration);
api.root.addResource("products").addMethod("GET", k8sIntegration);

// Rutas a Lambda (burst traffic)
const lambdaHandler = new lambda.Function(this, "BurstHandler", {
  runtime: lambda.Runtime.NODEJS_18_X,
  handler: "index.handler",
  code: lambda.Code.fromAsset("lambda"),
  reservedConcurrentExecutions: 1000,
});

const lambdaIntegration = new apigateway.LambdaIntegration(lambdaHandler);

api.root.addResource("tickets").addMethod("POST", lambdaIntegration);
api.root.addResource("checkout").addMethod("POST", lambdaIntegration);

Decision Matrix

| Factor | K8s Score | Serverless Score | Peso | | --------------------------- | --------- | ---------------- | ----- | | Tráfico constante alto | 10 | 2 | Alta | | Tráfico bajo/esporádico | 2 | 10 | Alta | | Budget inicial limitado | 3 | 10 | Alta | | Equipo sin DevOps | 2 | 10 | Media | | Necesita control total | 10 | 4 | Media | | Workloads con estado | 10 | 3 | Media | | Time to market rápido | 4 | 10 | Alta | | Multi-tenant complejo | 9 | 6 | Media |

Calculadora de Decisión

def recomendar_infraestructura(
    rps_promedio: int,
    tiene_devops: bool,
    traffico_predecible: bool,
    budget_inicial: int
) -> str:
    score_k8s = 0
    score_serverless = 0

    # Tráfico
    if rps_promedio > 1000:
        score_k8s += 3
    elif rps_promedio < 100:
        score_serverless += 3

    # DevOps
    if tiene_devops:
        score_k8s += 2
    else:
        score_serverless += 3

    # Predictibilidad
    if traffico_predecible:
        score_k8s += 2
    else:
        score_serverless += 2

    # Budget
    if budget_inicial < 1000:
        score_serverless += 3
    elif budget_inicial > 5000:
        score_k8s += 2

    if score_k8s > score_serverless:
        return "Kubernetes"
    return "Serverless"

# Ejemplo
recomendacion = recomendar_infraestructura(
    rps_promedio=500,
    tiene_devops=False,
    traffico_predecible=False,
    budget_inicial=800
)
print(f"Recomendación: {recomendacion}")
# Output: Serverless

Implementación: Mismo SaaS en Ambos

API en Kubernetes

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api
          image: myregistry/api:latest
          ports:
            - containerPort: 3000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: url
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: api-server
spec:
  selector:
    app: api-server
  ports:
    - port: 80
      targetPort: 3000
  type: LoadBalancer

Mismo API en Serverless

// handler.ts
import { APIGatewayProxyHandler } from "aws-lambda";
import { Pool } from "pg";

// Connection pool reutilizable
let pool: Pool;

function getPool() {
  if (!pool) {
    pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 1, // Lambda = 1 conexión
    });
  }
  return pool;
}

export const handler: APIGatewayProxyHandler = async (event) => {
  const pool = getPool();

  try {
    const { httpMethod, path, body } = event;

    // Routing simple
    if (httpMethod === "GET" && path === "/users") {
      const result = await pool.query("SELECT * FROM users");

      return {
        statusCode: 200,
        headers: {
          "Content-Type": "application/json",
          "Access-Control-Allow-Origin": "*",
        },
        body: JSON.stringify(result.rows),
      };
    }

    // Más rutas...

    return {
      statusCode: 404,
      body: JSON.stringify({ error: "Not found" }),
    };
  } catch (error) {
    console.error(error);

    return {
      statusCode: 500,
      body: JSON.stringify({ error: "Internal server error" }),
    };
  }
};
# serverless.yml
service: saas-api

provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  environment:
    DATABASE_URL: ${env:DATABASE_URL}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - rds:*
          Resource: "*"

functions:
  api:
    handler: handler.handler
    events:
      - http:
          path: /{proxy+}
          method: ANY
          cors: true
    timeout: 30
    memorySize: 512
    reservedConcurrency: 100

Patrones de Migración

1. Serverless → Kubernetes (Scaling up)

Cuándo migrar:

  • Costos serverless > $3K/mes
  • Cold starts impactan UX
  • Necesitas workloads con estado

Estrategia de migración:

Fase 1: Containerizar functions existentes
├── Lambda function → Docker container
└── Probar localmente

Fase 2: Deploy a K8s gradualmente
├── Empezar con 1 servicio (menos crítico)
├── Monitorear métricas
└── Migrar resto si funciona bien

Fase 3: Ajustar y optimizar
├── Tuning de resource limits
├── Configurar auto-scaling
└── Setup monitoring completo

2. Kubernetes → Serverless (Simplificar)

Cuándo migrar:

  • Tráfico cayó significativamente
  • Equipo DevOps se redujo
  • Costos operacionales muy altos

Más raro, pero posible si cambió el negocio

Mejores Prácticas

Para Kubernetes

# 1. Siempre define resource limits
resources:
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "500m"

# 2. Usa liveness y readiness probes
livenessProbe:
  httpGet:
    path: /health
    port: 3000

# 3. Múltiples réplicas para HA
replicas: 3 # Mínimo

# 4. PodDisruptionBudget
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api-server

Para Serverless

// 1. Connection pooling
let cachedDb: any = null;

export const handler = async (event: any) => {
  if (!cachedDb) {
    cachedDb = await createConnection();
  }
  // Usa cachedDb...
};

// 2. Manejo de cold starts
export const handler = async (event: any) => {
  // Warmup check
  if (event.source === 'serverless-plugin-warmup') {
    return { statusCode: 200, body: 'Warmed' };
  }

  // Lógica normal...
};

// 3. Timeout apropiado
// serverless.yml
timeout: 30  # API Gateway max: 30s

// 4. Memory sizing
memorySize: 512  # Sweet spot para Node.js

Caso de Estudio: Nexgen SaaS

Antes (Full Serverless):

  • 200 usuarios
  • 50 RPS promedio
  • Costo: $400/mes
  • Cold starts ocasionales

Después (Híbrido):

  • 5,000 usuarios
  • 2,000 RPS promedio
  • K8s: Core APIs ($300/mes)
  • Lambda: Webhooks, cron jobs ($150/mes)
  • Costo total: $450/mes
  • Ahorro de $8K/mes vs full serverless a esa escala

Conclusión

| Escenario | Recomendación | Razón | | ------------------------ | ------------- | ---------------------------------- | | MVP/Startup | 🟢 Serverless | Rápido, económico, sin DevOps | | Growth (100-500 RPS) | 🟡 Cualquiera | En zona de transición | | Scale (1K+ RPS) | 🟢 Kubernetes | Economía de escala | | Enterprise | 🟢 Kubernetes | Control, compliance, VPCs privados | | Event-driven | 🟢 Serverless | Perfect fit para eventos async | | Streaming/WebSockets | 🟢 Kubernetes | Serverless no es ideal |

Nuestra recomendación en Nexgen:

  • Start serverless
  • Monitor costos y performance
  • Migrate a K8s cuando:
    • Costos > $2-3K/mes
    • Tráfico constante > 500 RPS
    • Cold starts impactan UX
    • Necesitas features avanzadas

No hay decisión incorrecta, solo decisión temprana o tardía. Optimiza para tu etapa actual.

Recursos Adicionales

¿Necesitas ayuda decidiendo tu arquitectura? Agenda una consulta gratuita.