Universidad Nacional de Luján
Departamento de Ciencias Básicas
Sistemas Distribuidos y Programación Paralela 2026 Dr. David Petrocelli
TP 3 · Parte 0
📑 Índice del documento

TP 3 · Parte 0 — Bootstrap del cluster

Instalación de k3s y primeros pasos con Kubernetes

Prerrequisito obligatorio del TP 3 — Parte 1 (entrega 05/05/2026)

Esta es la Parte 0 del TP 3: el bootstrap del cluster que vas a usar en las Partes 1 y 2. Sin haber completado esta parte (cluster k3s/k3d funcional + checklist de validación al final), no se acepta la entrega del Hit #1 (Sobel distribuido sobre Kubernetes).


¿Por qué Kubernetes y por qué k3s?

Kubernetes (k8s) es el orquestador de contenedores estándar de la industria. Te permite declarar “quiero 4 réplicas de este worker”, “este servicio necesita una IP estable”, “este job tiene que reintentarse hasta 3 veces si falla”, y el cluster se encarga.

k3s es una distribución liviana de Kubernetes (~70 MB de binario) creada por Rancher y donada al CNCF. Es Kubernetes real (certificado), no una versión recortada. Lo eligieron porque es lo más simple de levantar localmente sin depender de snap (microk8s) ni de máquinas virtuales (minikube).

Para este TP vamos a usar k3s en dos sabores según el sistema operativo:

Tu sistema Qué instalás
Linux nativo o WSL2 k3s (instalación directa)
macOS o Windows sin WSL k3d — que es k3s corriendo adentro de Docker

Los manifiestos YAML son idénticos entre ambos. Solo cambia cómo levantás el cluster y cómo cargás imágenes Docker.


Aviso importante: cómo va a vivir este cluster en tu máquina

Lo que vamos a montar acá es la forma más simple posible de tener Kubernetes corriendo. Es perfecto para aprender, pero tiene implicaciones que conviene entender antes de empezar:

Por qué este TP exige Kubernetes y no un docker-compose

El objetivo no es “que el Sobel distribuido corra en algún lado”. Es que empiecen a pensar en términos de Kubernetes, que es donde la industria está parada hoy. Eso significa entender la diferencia entre:

Mundo Docker / docker-compose Mundo Kubernetes
docker run Pod (la unidad mínima — uno o varios containers que comparten red y storage)
restart: always en compose Deployment (mantiene N réplicas vivas, hace rolling updates)
docker run --rm worker Job (one-off batch que corre hasta completarse)
cron en el host disparando docker run CronJob (cron nativo del cluster)
volumes: en compose PersistentVolumeClaim (PVC) — almacenamiento desacoplado del nodo
Archivo .env montado ConfigMap (configuración) + Secret (datos sensibles)
ports: en compose Service (IP/DNS estable sobre pods que pueden morir y revivir)
Escalar manualmente con --scale kubectl scale + HorizontalPodAutoscaler
docker network Namespace + políticas de red

Cuando en el Hit #1 del TP 3 conviertan el docker run del worker Sobel en un Deployment + Service + ConfigMap + RabbitMQ como queue compartida, ese ejercicio mental es el verdadero aprendizaje del TP — más que los YAMLs en sí. Pasar de “tengo un Dockerfile y un compose” a “tengo manifiestos declarativos que cualquier cluster Kubernetes puede ejecutar y escalar a N réplicas” es el salto profesional que se les pide.


Requisitos previos


Camino A — Linux nativo o WSL2 (instalación de k3s)

Paso 1: Instalar k3s

curl -sfL https://get.k3s.io | sh -

Esto te baja el binario, lo deja en /usr/local/bin/k3s, lo registra como servicio systemd, y lo arranca. Duración: ~30 segundos.

Paso 2: Verificar que el cluster está vivo

sudo k3s kubectl get nodes

Tenés que ver algo así:

NAME            STATUS   ROLES                  AGE   VERSION
mi-laptop       Ready    control-plane,master   12s   v1.32.x+k3s1

Paso 3: Configurar kubectl para no necesitar sudo

k3s deja el kubeconfig en /etc/rancher/k3s/k3s.yaml con permisos restrictivos. Para usar kubectl sin sudo:

mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
chmod 600 ~/.kube/config

Verificá:

kubectl get nodes

Si te devuelve el nodo sin pedir sudo, ya está.

Paso 4: Cómo cargar una imagen Docker en k3s

k3s no usa Docker como runtime — usa containerd. Por eso, una imagen que construiste con docker build no la “ve” k3s automáticamente. Hay que importarla:

docker save mi-imagen:latest -o mi-imagen.tar
sudo k3s ctr images import mi-imagen.tar
rm mi-imagen.tar

Para verificar que la imagen está cargada:

sudo k3s ctr images list | grep mi-imagen

Paso 5: Tirar abajo k3s cuando termines el TP

sudo /usr/local/bin/k3s-uninstall.sh

Limpia todo: binario, servicio systemd, datos del cluster, redes. Limpio.


Camino B — macOS o Windows (k3d)

Paso 1: Instalar k3d

macOS:

brew install k3d

Windows (con Chocolatey):

choco install k3d

Windows (con Scoop):

scoop install k3d

Verificá:

k3d version

Paso 2: Crear un cluster k3s adentro de Docker

k3d cluster create sobel

Esto levanta:

Duración: ~30 segundos.

Paso 3: Verificar el cluster

kubectl get nodes

Debería mostrar k3d-sobel-server-0 en estado Ready.

Paso 4: Cargar una imagen Docker en k3d

docker build -t mi-imagen:latest .
k3d image import mi-imagen:latest -c sobel

k3d image import se encarga de empaquetar la imagen y enviarla al cluster. Mucho más cómodo que el flujo de k3s nativo.

Paso 5: Tirar abajo el cluster cuando termines

k3d cluster delete sobel

Comandos básicos de kubectl que vas a usar todo el tiempo

Comando Qué hace
kubectl get nodes Lista los nodos del cluster
kubectl get pods Lista los pods en el namespace actual
kubectl get pods -A Lista los pods de TODOS los namespaces
kubectl get deploy Lista los Deployments
kubectl get svc Lista los Services
kubectl describe pod <nombre> Detalles + eventos de un pod (útil cuando algo no arranca)
kubectl logs <pod> Logs del pod
kubectl logs -f <pod> Logs en streaming
kubectl logs -l app=worker Logs de todos los pods con el label app=worker
kubectl apply -f archivo.yaml Crea/actualiza recursos del archivo
kubectl delete -f archivo.yaml Borra los recursos del archivo
kubectl scale deploy/worker --replicas=5 Escala un Deployment
kubectl exec -it <pod> -- /bin/sh Te abre un shell adentro del pod
kubectl port-forward svc/rabbitmq 15672:15672 Expone un Service en tu localhost
kubectl get events --sort-by='.lastTimestamp' Eventos del cluster ordenados por fecha (golden para debug)

Hello world: deployar nginx para validar que todo funciona

Creá un archivo nginx-test.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-test
spec:
  replicas: 2
  selector:
    matchLabels: { app: nginx-test }
  template:
    metadata:
      labels: { app: nginx-test }
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-test
spec:
  selector: { app: nginx-test }
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP

Aplicalo:

kubectl apply -f nginx-test.yaml

Esperá a que estén corriendo:

kubectl get pods -l app=nginx-test --watch
# salí con Ctrl+C cuando los 2 pods estén Running

Hacé port-forward al Service (no al pod — así pueden ver el balanceo):

kubectl port-forward svc/nginx-test 8080:80

En otra terminal:

curl http://localhost:8080

Tenés que ver el HTML de bienvenida de nginx. Si lo ves, felicitaciones — tu cluster funciona y ya estás haciendo balanceo entre 2 réplicas.

Demo extra: auto-healing

Matá uno de los pods y mirá cómo k8s lo recrea solo:

kubectl get pods -l app=nginx-test
kubectl delete pod <nombre-de-uno-de-los-pods>
kubectl get pods -l app=nginx-test --watch

El Deployment mantiene el “estado deseado” (2 réplicas). Si matás uno, levanta otro. Esto es lo que reemplaza al restart: always de docker-compose, pero a nivel cluster.

Demo extra: rolling update

kubectl set image deployment/nginx-test nginx=nginx:1.25-alpine
kubectl rollout status deployment/nginx-test

Cambia la versión sin downtime — k8s va matando los pods viejos uno por uno mientras levanta los nuevos.

Limpieza

kubectl delete -f nginx-test.yaml

Conceptos mínimos que vas a usar en el TP 3

Concepto Para qué sirve en el TP 3
Pod La unidad mínima — uno o más containers que comparten red y storage
Deployment Mantiene N réplicas vivas. Es lo que vas a usar para los workers Sobel (Hit #1)
Service IP/DNS estable adentro del cluster. Lo usás para que los workers encuentren a RabbitMQ sin importar en qué nodo esté
ConfigMap Configuración no-secreta inyectada como env vars o archivos (ej: RABBIT_HOST, QUEUE_NAME, WORKER_PARALLELISM)
Secret Lo mismo pero para datos sensibles (passwords de RabbitMQ, tokens)
Job Tarea one-off que corre hasta completarse (útil para el productor que despacha el batch de imágenes)
Namespace Carpeta lógica del cluster — usen default o creen sobel para aislar

No hace falta entender todo de Kubernetes. Para el Hit #1 alcanza con saber qué hace cada uno de estos 7.


Mini-ejemplo: cómo se va a ver el Hit #1 (anticipo)

Para que vean el destino, así de simple es desplegar un worker pool replicado conectado a RabbitMQ en Kubernetes:

# rabbitmq.yaml — el broker (1 réplica, persistente)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rabbitmq
spec:
  replicas: 1
  selector: { matchLabels: { app: rabbitmq } }
  template:
    metadata: { labels: { app: rabbitmq } }
    spec:
      containers:
      - name: rabbitmq
        image: rabbitmq:3-management
        ports:
        - containerPort: 5672    # AMQP
        - containerPort: 15672   # UI
---
apiVersion: v1
kind: Service
metadata: { name: rabbitmq }
spec:
  selector: { app: rabbitmq }
  ports:
  - { name: amqp,   port: 5672,  targetPort: 5672 }
  - { name: admin,  port: 15672, targetPort: 15672 }
# workers.yaml — N réplicas del worker Sobel
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sobel-worker
spec:
  replicas: 3                           # ← acá está la magia distribuida
  selector: { matchLabels: { app: sobel-worker } }
  template:
    metadata: { labels: { app: sobel-worker } }
    spec:
      containers:
      - name: worker
        image: mi-usuario/sobel-worker:latest
        env:
        - name: RABBIT_HOST
          value: "rabbitmq"             # ← DNS interno del cluster, apunta al Service
        - name: QUEUE_NAME
          value: "image-jobs"
kubectl apply -f rabbitmq.yaml -f workers.yaml

# ¿Carga alta? Escalo a 10 workers en caliente:
kubectl scale deploy/sobel-worker --replicas=10
kubectl get pods -l app=sobel-worker

Eso es todo lo que cambia entre “lo corro a mano” y “tengo un sistema distribuido escalable”. En la Parte 2 del TP 3, ese mismo kubectl apply se ejecuta sobre GKE en lugar de tu laptop, con autoscaling real.


Troubleshooting frecuente

“El pod queda en Pending y no arranca”

Casi siempre es porque la imagen no está disponible. En k3s tenés que importar la imagen con sudo k3s ctr images import. En k3d con k3d image import. Después en el manifiesto del pod usá imagePullPolicy: IfNotPresent para que no intente bajarla de Docker Hub.

kubectl describe pod <nombre>
# leé la sección "Events" — ahí dice qué está fallando

“El pod arranca pero falla con CrashLoopBackOff

Es un error del proceso adentro del container. Mirá los logs:

kubectl logs <pod>
kubectl logs <pod> --previous   # logs de la corrida anterior si ya reinició

“Mis workers no se conectan a RabbitMQ”

99% de las veces es porque pusieron localhost o la IP del host como RABBIT_HOST. Adentro del cluster, la URL del broker es el nombre del Service (rabbitmq en el ejemplo). El DNS interno de Kubernetes resuelve eso.

kubectl exec -it deploy/sobel-worker -- nslookup rabbitmq
# tiene que devolver una IP del rango ClusterIP

“kubectl no encuentra el cluster (The connection to the server localhost:8080 was refused)”

Tu ~/.kube/config no apunta al cluster correcto. En k3s seguí el Paso 3 del Camino A. En k3d corré k3d kubeconfig merge sobel --kubeconfig-switch-context.

“k3s consume mucha RAM”

k3s idle anda en ~400-500 MB. Si tenés problemas, asegurate de no tener microk8s, minikube ni Docker Desktop con su Kubernetes activo en paralelo.


Container registries — para la Parte 2 (GKE)

En la Parte 1 podés usar k3s ctr import / k3d image import para cargar imágenes locales. Pero para la Parte 2 (GKE) vas a necesitar publicar las imágenes en un registry público o privado.

Registry Tipo Cuándo se elige Free tier para públicas
GitHub Container Registry (ghcr.io) Cloud, integrado con GitHub Stack basado en GitHub Actions, ergonomía top ✅ ilimitado
Docker Hub (docker.io) Cloud, el original Imágenes oficiales ⚠️ rate limit 100/6h anónimo, 200/6h auth — se siente en CI
Google Artifact Registry Cloud, GCP-native Recomendado para Parte 2 (GKE) — IAM-based auth, latencia mínima a GKE ❌ pago (módico)
Amazon ECR Cloud, AWS-native EKS / ECS / Fargate ❌ pago
Harbor Self-hosted, OSS (CNCF graduated) El estándar on-prem corporativo N/A — vos lo hosteás

Para este TP: usen ghcr.io en Parte 1 (gratis, sin rate limit) y migren a Google Artifact Registry en Parte 2 si quieren explotar la integración con GKE.


Lectura recomendada y referencias

No es lectura obligatoria, pero si van en serio con esto, las primeras 4-5 referencias les van a ahorrar meses.

Documentación oficial

Libros — ordenados de “primer contacto” a “producción/avanzado”

Libro Autores Editorial / Año Cuándo leerlo
The Kubernetes Book (3rd ed) Nigel Poulton, Pushkar Joglekar O’Reilly · 2024 Primer contacto, lectura de fin de semana
Kubernetes: Up and Running (3rd ed) Brendan Burns, Joe Beda, Kelsey Hightower, Lachlan Evenson O’Reilly · 2022 Intro canónica — escrita por quienes diseñaron k8s
Kubernetes in Action (2nd ed) Marko Lukša, Kevin Conner Manning · 2026 El best of class para deep-dive. ⭐
Kubernetes Patterns (2nd ed) Bilgin Ibryam, Roland Huß O’Reilly · 2023 Patterns reutilizables. Cap. 3 (Predictable Demands) y Cap. 6 (Stateless Service) son directamente Hit #1
Kubernetes Best Practices (2nd ed) Brendan Burns et al O’Reilly · 2024 Operación en producción: RBAC, observabilidad, GitOps
Cloud Native Patterns Cornelia Davis Manning · 2019 Patrones cloud-native independientes de k8s
GitOps and Kubernetes Yuen, Matyushentsev, Ekenstam, Suen Manning · 2021 Para Argo CD / Flux

Papers fundacionales — el ADN intelectual de Kubernetes

Los conceptos de Deployment, Service, scheduling, autoscaling no salieron de la nada — vienen de 15 años de operación a escala en Google con sus sistemas Borg y Omega. Estos 4 papers son lectura obligatoria conceptual:

Recursos comunitarios


Glosario — términos densos en una línea cada uno


Validación obligatoria previa al TP 3 — Parte 1

Para que la entrega del Hit #1 sea aceptada, tienen que poder responder afirmativamente a las 5 preguntas. Documenten en el README del Hit #1 (sección “Prerrequisitos cumplidos”) la evidencia de cada checkpoint (output del comando o screenshot).

Si las 5 son sí, están listos para el Hit #1.