Listado de la etiqueta: k8s

Docker vs Kubernetes: cuándo me alcanza con uno y cuándo necesito el otro

Me pidieron «alta disponibilidad» para un sistema crítico. Tenía Docker Compose funcionando perfecto en un solo nodo. Pensé: «con –scale lo resuelvo». Hasta que entendí que escalar horizontalmente en múltiples nodos con Compose no es trivial. Ese fue el momento en que Kubernetes dejó de ser «esa tecnología complicada» y se convirtió en la herramienta correcta para el trabajo.

Docker standalone: cuándo alcanza

No todo necesita Kubernetes. Docker solo, o Docker Compose, es perfecto para muchos casos de uso:

  • Aplicaciones en un solo servidor
  • Entornos de desarrollo local
  • Proyectos personales o startups en etapa temprana
  • Workloads que no requieren alta disponibilidad real
  • Pipelines de CI/CD

La tabla que lo resume todo

CaracterísticaDocker soloDocker SwarmKubernetes
ComplejidadBajaMediaAlta
HA multi-nodoNoSí (básico)Sí (avanzado)
Auto-scalingManualBásicoHPA / VPA automático
Self-healingrestart policySí (avanzado)
Rolling updatesManualSí (con control fino)
SecretsEnv vars / archivosDocker SecretsKubernetes Secrets + Vault
NetworkingBridge / overlay básicoOverlayCNI (Calico, Flannel, etc.)
Observabilidaddocker logs / statsBásicaPrometheus, Grafana, Jaeger
Curva de aprendizajeDíasSemanasMeses

Docker Swarm: el punto medio

Swarm es el orquestador nativo de Docker. Más simple que Kubernetes, soporta multi-nodo y HA básica. Si necesitás distribuir contenedores en 2-5 nodos sin la complejidad de K8s, Swarm es una opción válida.

# Inicializar un swarm
docker swarm init --advertise-addr 192.168.1.10

# Agregar un nodo worker
docker swarm join --token SWMTKN-1-xxx 192.168.1.10:2377

# Desplegar un stack (similar a docker compose)
docker stack deploy -c docker-compose.yml mi-app

# Ver servicios del stack
docker service ls
docker service ps mi-app_api

# Escalar un servicio
docker service scale mi-app_api=3

Kubernetes: cuándo lo necesitás de verdad

En mi cluster SUSE Linux HA con dos nodos, Swarm funcionaba. Pero cuando los requisitos crecieron — deploys sin downtime garantizado, auto-scaling basado en métricas, gestión de secretos centralizada, rollbacks automáticos — Kubernetes fue la respuesta correcta.

La diferencia fundamental: Docker (y Swarm) son herramientas para correr contenedores. Kubernetes es una plataforma para gestionar aplicaciones. La distinción importa cuando tu aplicación crece.

# El mismo concepto en Docker Compose vs Kubernetes

# docker-compose.yml
services:
  api:
    image: mi-api:1.4.2
    replicas: 3
    restart: unless-stopped

# ─────────────────────────────────────────────
# En Kubernetes (deployment.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1    # nunca baja de 2 replicas durante update
      maxSurge: 1          # puede tener 4 temporalmente
  selector:
    matchLabels:
      app: api
  template:
    spec:
      containers:
      - name: api
        image: mi-api:1.4.2
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 30
        readinessProbe:
          httpGet:
            path: /ready
            port: 80

El camino natural: Docker → Compose → Kubernetes

No es un salto, es una progresión. Empecé con docker run, pasé a Compose para gestionar múltiples servicios, y cuando los requisitos de producción superaron lo que Compose podía manejar cómodamente, migramos a Kubernetes. Los conceptos son los mismos — imágenes, contenedores, redes, volúmenes — pero Kubernetes agrega la capa de orquestación inteligente que necesitás a escala.

El conocimiento de Docker no se descarta al llegar a Kubernetes: es el prerequisito. Todo pod de Kubernetes corre contenedores Docker. Los Dockerfiles que aprendiste a escribir son exactamente los mismos. La diferencia está en quién los gestiona y con qué nivel de sofisticación.

Mi setup actual

Hoy corro Kubernetes en mi cluster on-premise SUSE con dos nodos en HA. Docker sigue presente — lo uso en CI/CD para buildear imágenes y en desarrollo local. Compose lo uso para levantar entornos de desarrollo con múltiples servicios. Kubernetes gestiona producción. Cada herramienta en su lugar.


Artículo anterior: Seguridad en Docker | Fin de la Serie Docker Completo

Esta fue la serie completa sobre Docker. Si llegaste hasta acá, tenés las bases para trabajar con contenedores en entornos reales. El siguiente paso natural es Kubernetes — cubriremos eso en una serie dedicada.


Artículo anterior: Seguridad en Docker: errores que cometí y cómo los corregí | Fin de la Serie Docker Completo

El parche perfecto: Cómo domé una fuga de memoria en Kubernetes con un CronJob inteligente

El Problema: Una API con hambre de RAM

No importa cuánta experiencia tengas, a veces el código te juega una mala pasada. Recientemente, me encontré con un desafío clásico pero persistente: una de nuestras APIs críticas desarrollada en .NET presentaba una fuga de memoria (memory leak).

En nuestro entorno On-Premise, corriendo sobre un cluster de SUSE Linux Enterprise (HA) con solo dos nodos, no podíamos permitir que un pod consumiera recursos hasta asfixiar al nodo o provocar un reinicio descontrolado por el OOMKiller. Si el pod llegaba al límite del Deployment (1500 Mi), la latencia subía y la experiencia del usuario se degradaba.

La Idea: Si no puedes curarlo (aún), manténlo limpio

Mientras el equipo de desarrollo investigaba el root cause en el código, necesitaba una solución operativa. Podría haber configurado un auto-reinicio simple, pero quería algo más «quirúrgico».

Mi premisa fue:

  1. No quiero reiniciar todos los pods a la vez (evitar downtime).
  2. Solo quiero matar al pod que esté realmente en peligro (umbral del 75-80%).
  3. Debo asegurar que el nuevo pod esté saludable antes de pasar al siguiente.

Como mi día a día es en la terminal con Zsh y mi fiel alias kub para kubectl, decidí automatizar mi propio flujo de trabajo.

La Implementación: RBAC y un Script de Bash

La solución no podía ser «toscamente» manual. Implementé un CronJob dentro del cluster que actúa como un recolector de basura inteligente.

1. Seguridad ante todo (RBAC)

No quería que el script corriera con permisos de administrador. Creé una ServiceAccount específica y un Role con los permisos mínimos necesarios: get, list y watch para pods y despliegues. Sin el permiso de watch, el script no podría esperar de forma segura a que la nueva réplica estuviera lista.

2. El «Cerebro» del Script

El script realiza un baile preciso:

  • Consulta las métricas reales mediante kubectl top pods.
  • Filtra los pods de mi API y calcula el porcentaje de uso basado en el límite real configurado en el Deployment.
  • Si un pod supera el 75%, lo elimina.
  • Inmediatamente ejecuta un kubectl rollout status. Esto es vital: el script se pausa hasta que Kubernetes confirma que el nuevo pod pasó sus Health Checks.

3. El Toque On-Premise

Al estar en un cluster con OCFS2 (Oracle Cluster File System 2) y almacenamiento compartido RWX, la conmutación de pods es extremadamente fluida. El nuevo pod monta los volúmenes en segundos, ya sea en el nodea o en el nodeb, sin errores de bloqueo.

El Resultado: Estabilidad 24/7

Programé el CronJob para ejecutarse dos veces al día: a las 3 AM y a las 3 PM.

¿El resultado? El sistema ahora se «limpia» solo. En los logs de la última prueba, pude ver cómo el script identificaba un pod al 80% (1202 Mi), lo eliminaba y esperaba a que la nueva réplica estuviera al 100% antes de seguir. El resto de los pods, que estaban en niveles normales, no fueron tocados.

Esta solución me dio la tranquilidad de que, mientras llega el parche definitivo en el código, mi infraestructura sigue siendo robusta, predecible y, sobre todo, altamente disponible.


Lo que aprendí:

  • Observabilidad: kubectl top es tu mejor amigo cuando el Metrics Server está bien configurado.
  • RBAC es clave: No escatimes en configurar los verbos correctos (watch me salvó la vida).
  • Automatiza tus parches: Un «parche» bien automatizado es una herramienta de ingeniería, no una chapuza.

 kub create job --from=cronjob/memory-leak-patch test-mem-clean -n gag
job.batch/test-mem-clean created
❯ kub logs -f -l job-name=test-mem-clean -n gag
--- Iniciando chequeo de memoria ---
Umbral: 75% de 1500 Mi
>> Pod deploy-gag-api-prod-849d5cb498-z6trv excedido: 80% (1202 Mi). Matando...
pod "deploy-gag-api-prod-849d5cb498-z6trv" deleted from gag namespace
>> Esperando a que el reemplazo esté Ready...
Waiting for deployment "deploy-gag-api-prod" rollout to finish: 4 of 5 updated replicas are available...
deployment "deploy-gag-api-prod" successfully rolled out
>> Pod deploy-gag-api-prod-849d5cb498-xzxtb OK: 21% (326 Mi).
>> Pod deploy-gag-api-prod-849d5cb498-x8krt OK: 15% (235 Mi).
>> Pod deploy-gag-api-prod-849d5cb498-d22w4 OK: 10% (161 Mi).
>> Pod deploy-gag-api-prod-849d5cb498-wtlpr OK: 9% (144 Mi).
--- Saneamiento finalizado ---

Este es el yaml

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: pod-memory-cleaner
  namespace: gag
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-memory-cleaner-role
  namespace: gag
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "delete", "watch"]
  - apiGroups: ["metrics.k8s.io"]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-memory-cleaner-binding
  namespace: gag
subjects:
  - kind: ServiceAccount
    name: pod-memory-cleaner
    namespace: gag
roleRef:
  kind: Role
  name: pod-memory-cleaner-role
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: memory-leak-patch
  namespace: gag
spec:
  # Se ejecuta a las 3:00 AM y 3:00 PM (15:00)
  schedule: "0 3,15 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: pod-memory-cleaner
          restartPolicy: OnFailure
          containers:
          - name: patch-script
            image: bitnami/kubectl:latest
            command:
            - /bin/sh
            - -c
            - |
              NAMESPACE="gag"
              DEPLOYMENT="deploy-gag-api-prod"
              THRESHOLD=75
              
              # Obtener límite de memoria del deployment
              LIMIT=$(kubectl get deploy $DEPLOYMENT -n $NAMESPACE -o jsonpath='{.spec.template.spec.containers[0].resources.limits.memory}' | sed 's/Mi//')
              
              echo "--- Iniciando saneamiento de memoria ---"
              echo "Umbral configurado: $THRESHOLD% de $LIMIT Mi"

              # Listar pods del deployment ordenados por mayor consumo de memoria
              kubectl top pods -n $NAMESPACE --no-headers | grep "$DEPLOYMENT" | sort -k3 -rn | while read line; do
                POD_NAME=$(echo $line | awk '{print $1}')
                USAGE=$(echo $line | awk '{print $3}' | sed 's/Mi//')
                
                # Cálculo de porcentaje
                PERCENT=$(( USAGE * 100 / LIMIT ))
                
                if [ "$PERCENT" -gt "$THRESHOLD" ]; then
                  echo ">> Pod $POD_NAME excedido: $PERCENT% ($USAGE Mi). Eliminando..."
                  kubectl delete pod $POD_NAME -n $NAMESPACE
                  
                  echo ">> Esperando que la nueva réplica esté Ready (Rollout Status)..."
                  kubectl rollout status deployment/$DEPLOYMENT -n $NAMESPACE
                  
                  # Margen para convergencia de red (MetalLB)
                  sleep 15
                else
                  echo ">> Pod $POD_NAME OK: $PERCENT% ($USAGE Mi)."
                fi
              done
              echo "--- Saneamiento finalizado ---"