Contenedores y Kubernetes

"Contenedores: 'funciona en mi máquina' es ahora 'funciona donde sea'."

Qué vas a aprender en este capítulo

Docker revolucionó el deploy de software al estandarizar cómo se empaqueta una aplicación. Kubernetes (K8s) revolucionó cómo se ejecutan miles de contenedores en producción. Este capítulo cubre ambos.


3.1 Contenedores con Docker

💡 Intuición

Una imagen Docker es una "receta" inmutable que contiene el OS mínimo, dependencias y tu aplicación. Un contenedor es una instancia ejecutándose de esa imagen.

Diferencia con VMs: las VMs virtualizan hardware (cada VM tiene su kernel). Los contenedores comparten el kernel del host pero aíslan procesos, redes y filesystems → mucho más livianos (MB vs GB) y rápidos (segundos vs minutos para arrancar).

📐 Fundamento

Dockerfile multi-stage para FastAPI:

# Stage 1: builder
FROM python:3.12-slim AS builder

WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /build/wheels -r requirements.txt

# Stage 2: runtime
FROM python:3.12-slim

# Crear usuario no-root por seguridad
RUN useradd --create-home --uid 1000 app
WORKDIR /app
USER app

# Copiar solo wheels desde builder (sin compilers, gcc, etc.)
COPY --from=builder --chown=app:app /build/wheels /tmp/wheels
RUN pip install --user --no-cache-dir /tmp/wheels/*.whl && rm -rf /tmp/wheels

COPY --chown=app:app . .

ENV PATH="/home/app/.local/bin:$PATH"

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
    CMD curl -f http://localhost:8000/health || exit 1

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Comandos esenciales:

# Build
docker build -t la-esquina-api:1.0 .

# Ejecutar
docker run -d -p 8000:8000 \
    -e DATABASE_URL=postgresql://... \
    --name api la-esquina-api:1.0

# Ver logs
docker logs -f api

# Entrar a un container
docker exec -it api /bin/bash

# Listar
docker ps              # corriendo
docker images          # imágenes
docker system df       # uso de disco

# Limpiar
docker system prune -a # eliminar todo lo no usado

docker-compose para entornos multi-servicio:

# docker-compose.yml
version: '3.8'

services:
  api:
    build: .
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgresql://user:pass@db:5432/esquina
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
  
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: esquina
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes: [pgdata:/var/lib/postgresql/data]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d esquina"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}

volumes:
  pgdata:
docker-compose up -d         # iniciar todo
docker-compose logs -f api   # logs de un servicio
docker-compose down -v       # parar y eliminar volúmenes

Mejores prácticas para imágenes:

Práctica Por qué
Usar imágenes base oficiales Mantenidas con parches de seguridad
Usar -slim o -alpine 10x menos tamaño
Multi-stage builds Sin compilers en imagen final
Usuario no-root Seguridad
.dockerignore No incluir node_modules, .git, etc.
Pinear versiones (python:3.12.1) Builds reproducibles
HEALTHCHECK Para que el orquestador sepa si está sano
No incluir secretos Usar variables de entorno o secret managers
Cachear layers Copiar requirements.txt antes que el código

Container registries:

  • Docker Hub — público, gratuito (con límites).
  • GitHub Container Registry (ghcr.io) — gratis para repos públicos.
  • AWS ECR / GCP Artifact Registry / Azure Container Registry — privados, integrados con su cloud.

3.2 ¿Qué es Kubernetes?

💡 Intuición

Tenés 100 contenedores corriendo. ¿Quién los reinicia si caen? ¿Quién distribuye carga entre ellos? ¿Quién los actualiza sin downtime? ¿Quién los re-ubica cuando un servidor falla?

Kubernetes hace todo eso automáticamente. Sos vos quien declara: "quiero 5 réplicas de este contenedor con 1GB de RAM cada una". K8s se encarga del resto: dónde correrlas, cómo distribuir tráfico, qué hacer si una falla.

📐 Fundamento

Arquitectura de un clúster:

┌─────────────────────────────────────────┐
│         Control Plane (Master)          │
│  - API Server (kubectl habla con esto)  │
│  - etcd (state del clúster, vía Raft)   │
│  - Scheduler (decide dónde correr Pods) │
│  - Controller Manager (mantiene estado) │
└───────────────┬─────────────────────────┘
                │
   ┌────────────┼────────────┐
   ▼            ▼            ▼
[Node 1]    [Node 2]    [Node 3]   ← worker nodes
 - kubelet   - kubelet   - kubelet  (agente que ejecuta Pods)
 - kube-     - kube-     - kube-
   proxy       proxy       proxy
 - Pods      - Pods      - Pods

Conceptos clave:

Recurso Qué es
Pod Una o más containers que comparten red y storage. La unidad mínima
Deployment Define cuántas réplicas de un Pod, cómo hacer rolling updates
Service Endpoint de red estable para acceder a Pods
Ingress Routing HTTP/HTTPS desde fuera del clúster
ConfigMap Configuración no-secreta
Secret Datos sensibles (passwords, API keys)
Namespace Aislamiento lógico (dev, prod, equipos)
PersistentVolume Storage persistente
HorizontalPodAutoscaler Autoescala Pods según métricas

3.3 Desplegar una app a K8s

📐 Fundamento

Deployment + Service para FastAPI:

# api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  labels: { app: api }
spec:
  replicas: 3
  selector:
    matchLabels: { app: api }
  template:
    metadata:
      labels: { app: api }
    spec:
      containers:
      - name: api
        image: ghcr.io/la-esquina/api:1.2.0
        ports:
        - containerPort: 8000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: url
        - name: ENV
          value: production
        resources:
          requests: { memory: "256Mi", cpu: "250m" }
          limits:   { memory: "512Mi", cpu: "500m" }
        livenessProbe:
          httpGet: { path: /health, port: 8000 }
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet: { path: /ready, port: 8000 }
          initialDelaySeconds: 5
          periodSeconds: 5

---
# api-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  selector: { app: api }
  ports:
  - port: 80
    targetPort: 8000
  type: ClusterIP   # solo accesible dentro del clúster

---
# api-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
  - hosts: [api.la-esquina.com]
    secretName: api-tls
  rules:
  - host: api.la-esquina.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: api-service
            port: { number: 80 }

Aplicar y gestionar:

# Aplicar manifests
kubectl apply -f api-deployment.yaml -f api-service.yaml -f api-ingress.yaml

# Ver estado
kubectl get pods                         # Pods corriendo
kubectl get deployments,services,ingress # todo lo del namespace actual
kubectl describe pod api-7d4b...         # detalles de un Pod
kubectl logs -f api-7d4b...              # logs en vivo
kubectl exec -it api-7d4b... -- /bin/bash  # entrar a un Pod

# Escalar manualmente
kubectl scale deployment api --replicas=10

# Rolling update (cambiar imagen)
kubectl set image deployment/api api=ghcr.io/la-esquina/api:1.2.1

# Ver rollout
kubectl rollout status deployment/api
kubectl rollout history deployment/api
kubectl rollout undo deployment/api      # rollback

# Eliminar
kubectl delete -f api-deployment.yaml

Autoscaling:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: api-hpa }
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 3
  maxReplicas: 50
  metrics:
  - type: Resource
    resource:
      name: cpu
      target: { type: Utilization, averageUtilization: 70 }

K8s autoescalará entre 3 y 50 réplicas para mantener CPU promedio en 70%.

Secrets:

# Crear secret desde literales
kubectl create secret generic db-credentials \
    --from-literal=url='postgresql://user:pass@host/db'

# O desde archivo
kubectl create secret generic api-keys --from-file=./keys.json

Para producción usar:

  • Sealed Secrets o External Secrets Operator — secrets cifrados en Git.
  • HashiCorp Vault — secret management completo.
  • AWS Secrets Manager / GCP Secret Manager — secret managers cloud-native.

3.4 Helm — el package manager de K8s

📐 Fundamento

Helm empaqueta apps K8s en charts parametrizables. Como apt-get o npm para Kubernetes.

# Estructura de un chart
mi-chart/
├── Chart.yaml          # metadata
├── values.yaml         # valores por default
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    └── ingress.yaml

templates/deployment.yaml usa template syntax:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-api
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
      - name: api
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        resources:
          {{- toYaml .Values.resources | nindent 10 }}

values.yaml:

replicaCount: 3
image:
  repository: ghcr.io/la-esquina/api
  tag: 1.2.0
resources:
  requests: { memory: "256Mi", cpu: "250m" }

Instalar:

helm install la-esquina ./mi-chart
helm install la-esquina ./mi-chart --set replicaCount=10
helm install la-esquina ./mi-chart -f values-prod.yaml

# Upgrade
helm upgrade la-esquina ./mi-chart --set image.tag=1.2.1

# Rollback
helm rollback la-esquina

# Charts públicos
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install postgres bitnami/postgresql

3.5 Kubernetes managed: EKS, GKE, AKS

📐 Fundamento

Operar el control plane de K8s manualmente es complejo (etcd, certificados, upgrades). Los proveedores cloud lo gestionan por vos.

Servicio Cloud Pricing
EKS (Elastic Kubernetes Service) AWS $0.10/h por clúster + nodos EC2
GKE (Google Kubernetes Engine) GCP Free tier: 1 zonal cluster gratis
AKS (Azure Kubernetes Service) Azure Control plane gratis

Crear EKS con Terraform:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.0"
  
  cluster_name    = "la-esquina-prod"
  cluster_version = "1.29"
  
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets
  
  eks_managed_node_groups = {
    workers = {
      min_size     = 2
      max_size     = 10
      desired_size = 3
      
      instance_types = ["t3.medium"]
    }
  }
}

# Configurar kubectl
output "kubectl_config" {
  value = "aws eks update-kubeconfig --name ${module.eks.cluster_name}"
}

Alternativas más simples a Kubernetes (cuando es overkill):

  • AWS ECS / Fargate — orquestación más simple, integrada con AWS.
  • Google Cloud Run — contenedores serverless.
  • Fly.io / Railway — deploy de contenedores sin gestionar nada.
  • Docker Swarm — orquestación incluida en Docker (menos features que K8s).

Cuándo NO usar Kubernetes:

  • Equipo pequeño (< 10 ingenieros).
  • App monolítica simple.
  • Sin necesidad de escalado complejo.

K8s tiene una curva de aprendizaje empinada y costos operacionales altos. Para muchas apps, Cloud Run o Fly.io son suficientes.


3.6 Ejercicios

✏️ Ejercicio 3.1 — Optimizar Dockerfile

El siguiente Dockerfile produce una imagen de 1.2GB y tarda 8 minutos en construirse. Identificá los problemas y proponé una versión optimizada.

FROM python:3.12

COPY . /app
WORKDIR /app

RUN apt-get update && apt-get install -y gcc make
RUN pip install -r requirements.txt
RUN pytest

EXPOSE 8000
CMD python -m uvicorn main:app --host 0.0.0.0 --port 8000

3.7 Para profundizar

3.8 Errores comunes

⚠️ Trampa común

Crear un Pod directamente en producción. kubectl run crea un Pod suelto. Si el nodo se cae o el Pod muere, no se reinicia: Kubernetes no sabe que debe haber uno corriendo. Solo Deployment (o StatefulSet, DaemonSet) garantiza que siempre haya nn réplicas.

Tip: Pods sueltos son útiles para debugging (kubectl run debug --rm -it --image=busybox). Para cualquier carga real: siempre Deployment.

⚠️ Trampa común

Usar la etiqueta :latest en imágenes. nginx:latest parece simple, pero hace tu deploy no reproducible: el "latest" de hoy es distinto al de mañana. Peor: kubectl rollout restart puede traer una versión nueva sin que vos lo decidas, y romper producción a las 3 a.m.

Tip: siempre fijar tag específico (nginx:1.27.1-alpine) o, mejor, usar digest (nginx@sha256:abc...) que es inmutable. Los pipelines de CI deben taggear con SHA del commit o semver.


Definiciones nuevas: contenedor, imagen, Dockerfile, multi-stage build, registry, docker-compose, Kubernetes, control plane, node, kubelet, Pod, Deployment, Service, Ingress, ConfigMap, Secret, namespace, HorizontalPodAutoscaler, Helm, chart, EKS, GKE, AKS.