Contexto académico: Este proyecto es un laboratorio práctico para la especialidad Fullstack 3 - Ingeniería de Software (DUOC). Ilustra cómo pasar de desarrollo local → containerización → orquestación de microservicios en Kubernetes.
Contenido:
product-service: Spring Boot + Gradle, puerto4020(backend Java)user-service: Express.js, puerto4021(backend Node.js)auth-service: Express.js + JWT + PostgreSQL, puerto3002(autenticación)cart-service: FastAPI + PostgreSQL, puerto4022(carrito)
Características:
product-serviceyuser-serviceresponden con datos mock para base del laboratoriouser-servicepersiste usuarios en archivo JSON (data/data.json) — ejemplo de persistencia localauth-serviceemite JWT ycart-servicevalida JWT y consumeproduct-servicepor red interna del cluster- Arquitectura diseñada para mostrar cómo múltiples microservicios independientes se comunican y se orquestan
Fases del laboratorio: Veras cómo evolucionan estos servicios: 1️⃣ manual local → 2️⃣ Docker individualizado → 3️⃣ Docker Compose → 4️⃣ Kubernetes → 5️⃣ API Gateway + Frontend → 6️⃣ Auth Service (JWT) → 7️⃣ Cart Service (FastAPI) → 8️⃣ resiliencia opcional con Circuit Breaker en KrakenD.
Material adicional:
- Ver
CIRCUIT_BREAKER_KRAKEND.mdpara la guia incremental de Circuit Breaker (v3 opcional).
- Configuración Windows ← Si usas Windows, lee primero
- Ejecución manual (local) ← Comienza aquí si es tu primera vez
- Docker Dockerfiles
- Docker Compose
- Kubernetes
- API Gateway + Frontend React
- Auth Service (JWT + PostgreSQL)
- Cart Service (FastAPI + inter-microservicio) ← Fase final del laboratorio
Esta guia esta escrita originalmente para macOS/Linux. Si usas Windows, lee esta seccion antes de continuar: la mayoria de los comandos funcionan igual en Git Bash, con algunas diferencias puntuales que se detallan aqui y se repiten en cada seccion donde apliquen.
- Git for Windows (incluye Git Bash): disponible en https://git-scm.com/downloads Durante la instalacion, seleccionar "Git Bash Here" como opcion de menu contextual.
- Java 25: instalador
.msidisponible en https://adoptium.net - Node.js 20+: instalador
.msidisponible en https://nodejs.org - Python 3.12+: instalador disponible en https://www.python.org/downloads/
- Docker Desktop: ya instalado (incluye Kubernetes integrado para el laboratorio).
Todos los comandos de esta guia deben ejecutarse desde Git Bash, no desde PowerShell ni desde CMD, salvo que se indique explicitamente lo contrario.
Docker Desktop incluye un cluster Kubernetes local que funciona perfectamente para este laboratorio.
- Abrir Docker Desktop → Settings → Kubernetes.
- Marcar la casilla Enable Kubernetes si no esta habilitado.
- Esperar a que el cluster inicie (status icon verde abajo a la derecha).
- Verificar desde Git Bash que funciona:
kubectl config current-context
kubectl cluster-infoLa salida de current-context debe ser docker-desktop. Con esto, docker y kubectl usan
el mismo entorno: las imagenes que construyes con docker build son automaticamente visibles
para el cluster sin necesidad de registry remoto.
1. Bind mounts en docker run (volumenes con rutas absolutas):
Git Bash en Windows puede convertir automaticamente las rutas POSIX a rutas Windows, lo que
rompe el montaje del volumen en Docker. Para evitarlo, agrega MSYS_NO_PATHCONV=1 antes del
comando docker run que usa -v:
macOS/Linux:
docker run -d --name user-service -p 4021:4021 \
-v $(pwd)/user-service/data/data.json:/app/data/data.json \
user-service:1.0Windows (Git Bash):
MSYS_NO_PATHCONV=1 docker run -d --name user-service -p 4021:4021 \
-v $(pwd)/user-service/data/data.json:/app/data/data.json \
user-service:1.0MSYS_NO_PATHCONV=1: desactiva la conversion automatica de rutas de Git Bash para ese comando.- El resto del comando es identico.
2. Agregar entrada al archivo hosts:
En Windows el archivo equivalente a /etc/hosts esta en
C:\Windows\System32\drivers\etc\hosts y no existe sudo. Se requiere PowerShell
ejecutado como Administrador:
macOS/Linux:
echo "127.0.0.1 micro.local" | sudo tee -a /etc/hostsWindows (PowerShell como Administrador):
Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "127.0.0.1 micro.local"O manualmente: clic derecho en Notepad → "Ejecutar como administrador" → Archivo → Abrir
→ navegar a C:\Windows\System32\drivers\etc\hosts → agregar 127.0.0.1 micro.local
al final → guardar.
3. Eliminar entrada del archivo hosts al FINALIZAR el laboratorio (limpieza completa):
127.0.0.1 micro.local del archivo hosts.
Solo ejecuta esto cuando hayas terminado completamente con el laboratorio.
macOS/Linux:
sudo sed -i '' '/micro\.local/d' /etc/hostsExplicacion:
sudo sed -i '' '/micro\.local/d' /etc/hosts: elimina permanentemente la linea que contienemicro.local.-i '': edita el archivo en su lugar (requerido en macOS).'/micro\.local/d': patrón de busqueda + accion de borrado (d= delete).- Despues de esto,
micro.localya no sera resuelto a127.0.0.1.
Windows (PowerShell como Administrador):
(Get-Content "C:\Windows\System32\drivers\etc\hosts") |
Where-Object { $_ -notmatch 'micro\.local' } |
Set-Content "C:\Windows\System32\drivers\etc\hosts"Explicacion:
- Lee todas las lineas de
hosts, filtra las que NO contenganmicro.localy reescribe el archivo. Where-Object { $_ -notmatch ... }: conserva solo las lineas que NO coinciden con el patron.- Resultado:
127.0.0.1 micro.locales eliminada permanentemente.
El resto de los comandos (./gradlew, docker build, docker compose, kubectl, curl)
funcionan igual en Git Bash sin ninguna modificacion.
Objetivo: Levantar ambos microservicios en tu máquina local sin contenedores. Por qué empezar aquí: Entiender qué hace cada servicio en aislamiento antes de containerizarlos.
- Java 25 (instalado y en PATH)
- Node.js 20+ con npm (instalado y en PATH)
- Python 3.12+ (instalado y en PATH)
Nota técnica: En product-service se usa Gradle Wrapper (./gradlew), una herramienta que descarga la versión correcta de Gradle automáticamente. Esto evita conflictos de versiones y no requiere Gradle instalado globalmente — es una best practice en Java.
Desde la raíz del proyecto:
cd product-service
./gradlew bootRunEndpoints:
GET http://localhost:4020/api/productsGET http://localhost:4020/api/products/{id}
En otra terminal, desde la raíz del proyecto:
cd user-service
npm install
npm startEndpoints:
GET http://localhost:4021/api/usersGET http://localhost:4021/api/users/{id}POST http://localhost:4021/api/users
Ejemplo body para crear usuario:
{
"name": "Maria Lopez",
"email": "maria.lopez@duocuc.cl",
"role": "student"
}Objetivo: Containerizar cada microservicio individuamente con Docker. Concepto clave: Cada Dockerfile define cómo empaquetar una aplicación en una imagen reutilizable. Por qué este paso: Antes de orquestar con Compose, necesitas entender cómo crear y ejecutar contenedores.
Desde la raíz microservicios:
docker build -t product-service:1.0 ./product-service
docker build -t user-service:1.0 ./user-serviceAtajo recomendado (ambas imagenes en un solo comando):
./scripts/build-images.shEste script construye siempre las imagenes locales con tag fijo 1.0:
product-service:1.0user-service:1.0
Explicacion de cada linea:
docker build -t product-service:1.0 ./product-service: construye la imagen de Java usando el Dockerfile dentro deproduct-servicey la etiqueta comoproduct-service:1.0.docker build -t user-service:1.0 ./user-service: construye la imagen de Node.js usando el Dockerfile dentro deuser-servicey la etiqueta comouser-service:1.0.
docker run -d --name product-service -p 4020:4020 product-service:1.0Explicacion del comando anterior:
docker run: crea y ejecuta un contenedor nuevo.-d: lo deja corriendo en segundo plano.--name product-service: nombre fijo para administrarlo facil.-p 4020:4020: publica el puerto del contenedor en tu maquina.product-service:1.0: imagen que se ejecuta.
Para users con volumen al archivo JSON:
docker run -d --name user-service -p 4021:4021 \
-v $(pwd)/user-service/data/data.json:/app/data/data.json \
user-service:1.0Explicacion del comando anterior:
docker run: crea y ejecuta un contenedor nuevo.-d: lo deja corriendo en segundo plano (detached mode).--name user-service: nombre fijo del contenedor para administrarlo facil.-p 4021:4021: publica el puerto4021del contenedor en el puerto4021de tu maquina.-v $(pwd)/user-service/data/data.json:/app/data/data.json: bind mount — vincula el archivo del host directamente con el archivo del contenedor. NO es un enlace simbólico; es una conexion directa a nivel del kernel: son el MISMO archivo físico accesible desde dos rutas distintas.$(pwd)/user-service/data/data.json: ruta del archivo en tu máquina (host).:/app/data/data.json: ruta donde el contenedor ve el mismo archivo.- Cuando el contenedor escribe en
/app/data/data.json, los cambios se guardan en el host endata/data.jsony viceversa. - Persistencia: aunque eliminemos el contenedor con
docker rm, el archivo del host permanece intacto con todos los datos. El contenedor es efímero; el archivo en el host es permanente. - Flujo de datos: host → [kernel] ← contenedor. Ambos lados leen/escriben en el mismo lugar.
user-service:1.0: imagen que se utiliza para crear el contenedor.\: solo permite partir el comando en varias lineas para que sea mas legible.
En Windows (Git Bash), agrega MSYS_NO_PATHCONV=1 al inicio para evitar que Git Bash
convierta la ruta del volumen (ver seccion 0 para detalle):
MSYS_NO_PATHCONV=1 docker run -d --name user-service -p 4021:4021 \
-v $(pwd)/user-service/data/data.json:/app/data/data.json \
user-service:1.0Diagrama del bind mount (persistencia de datos):
flowchart TD
A["Tu máquina Host<br/>data/data.json<br/>(archivo persistente)"]
K["Kernel Linux<br/>(conecta host ↔ contenedor)"]
B["Contenedor user-service<br/>/app/data/data.json<br/>(MISMO archivo físico)<br/>Node.js lee/escribe aquí"]
A -->|docker run -v| K
K -->|bind mount| B
C["Ciclo de vida"]
D["1. docker run → vincula el bind mount"]
E["2. App modifica datos en /app/data"]
F["3. Cambios persisten en HOST"]
G["4. docker rm → bind mount se desvincula"]
H["5. data/data.json en HOST permanece intacto"]
I["6. docker run nuevo → re-vincula al MISMO archivo"]
C --> D --> E --> F --> G --> H --> I
H -.->|datos intactos| A
I -.->|acceso a datos originales| B
curl http://localhost:4020/api/products
curl http://localhost:4021/api/usersObjetivo: Lanzar múltiples contenedores (products + users) con networking configurado automáticamente.
Concepto clave: Compose es un orquestador local para desarrollo; define el estado deseado en docker-compose.yml.
Por qué este paso: Simula lo que harás en Kubernetes, pero en tu máquina local de forma rápida.
Desde la raíz microservicios:
docker compose up --build -dExplicacion:
docker compose: levanta varios servicios definidos endocker-compose.yml.up: crea y arranca los servicios.--build: reconstruye imagenes antes de levantar.-d: deja los servicios corriendo en segundo plano.
Ver logs:
docker compose logs -fExplicacion:
logs: muestra logs de todos los servicios del compose.-f: sigue los logs en tiempo real.
Detener:
docker compose downExplicacion:
down: detiene y elimina contenedores y red del compose.
Servicios publicados:
product-serviceenhttp://localhost:4020user-serviceenhttp://localhost:4021
Importante sobre Docker Compose:
- Docker Compose es principalmente para desarrollo, pruebas locales y demos de integracion.
- En entornos productivos normalmente se usa Kubernetes, no Compose.
- Compose sigue siendo muy util para validar rapido en tu computador antes de pasar a Kubernetes.
Comparacion rapida:
- Docker Compose: ideal para trabajar en local con varios contenedores, rapido de levantar y facil de depurar.
- Kubernetes: ideal para ambientes mas reales o productivos, con auto-restart, escalado y declaracion de estado.
- Con Kubernetes, no necesitas docker-compose para ejecutar los servicios del cluster.
- En este proyecto, Compose queda como etapa de laboratorio local previa al despliegue Kubernetes.
Desde la raiz microservicios, para borrar lo creado por este proyecto:
docker compose down --rmi local --volumes --remove-orphans
docker rm -f product-service user-service 2>/dev/null || true
docker rmi -f product-service:1.0 user-service:1.0 2>/dev/null || true
docker volume prune -f
docker network prune -fExplicacion de cada linea:
docker compose down --rmi local --volumes --remove-orphans: detiene y borra recursos del compose, elimina imagenes locales generadas por compose, elimina volumenes declarados y contenedores huerfanos.docker rm -f product-service user-service 2>/dev/null || true: fuerza el borrado de esos contenedores por nombre; si no existen, no rompe el flujo.docker rmi -f product-service:1.0 user-service:1.0 2>/dev/null || true: elimina las imagenes etiquetadas del proyecto; si no existen, continua sin error.docker volume prune -f: borra volumenes Docker no usados por contenedores.docker network prune -f: borra redes Docker no usadas.
Para limpieza total de Docker en tu maquina (cuidado: elimina TODO lo no usado globalmente):
docker system prune -a --volumes -fExplicacion:
docker system prune: limpia recursos Docker no usados.-a: incluye imagenes no usadas (no solo dangling).--volumes: incluye volumenes no usados.-f: ejecuta sin pedir confirmacion.
Objetivo: Desplegar los microservicios en Kubernetes (k8s), aprendiendo conceptos fundamentales de orquestación. Contexto: Docker Compose es útil para desarrollo local. Kubernetes es lo que usan empresas para producción. Qué aprenderás: Namespaces, Deployments, Services, PersistentVolumes, Ingress y Traefik.
Archivos de Kubernetes usados en esta guia:
k8s/namespace.yamlk8s/ingress-traefik.yamlproduct-service/k8s/deployment.yamluser-service/k8s/pvc.yamluser-service/k8s/deployment.yaml
Finalidad de cada archivo:
k8s/namespace.yaml: crea el namespacemicroserviciospara aislar todos los recursos del laboratorio.k8s/ingress-traefik.yaml: define reglas HTTP de entrada para publicar ambos microservicios por host/path usando Traefik.product-service/k8s/deployment.yaml: define Deployment + Service deproduct-service.user-service/k8s/pvc.yaml: solicita almacenamiento persistente para los datos de usuarios.user-service/k8s/deployment.yaml: define Deployment + Service deuser-servicey monta el PVC en/app/data.
Esta es una pregunta frecuente:
"Si ambos están en el mismo archivo
deployment.yaml, ¿por qué no son lo mismo?"
Imagina que tienes un restaurante:
-
Deployment: Es la "Cocina"
- Quién: chef + cocineros
- Qué hace: prepara la comida
- Responsabilidades: receta, cantidad, calidad, si alguien se enferma → reemplaza al cocinero
- Mantra: "Tengo 1 chef, si se enferma levanto otro. Total siempre 1 cocinando"
-
Service: Es el "Mostrador"
- Quién: una persona en el mostrador
- Qué hace: toma órdenes + entrega comida
- Responsabilidades: estar en un lugar fijo, tomar pedidos, coordinar con la cocina
- Mantra: "Los clientes siempre encuentran el mostrador en el mismo lugar"
Si el chef se enferma:
- ❌ El chef sale de la cocina (fracaso del Deployment)
- ✅ Kubernetes levanta otro chef INMEDIATAMENTE
- ✅ El mostrador sigue en el mismo lugar (el Service NO cambió)
- ✅ Los clientes NO notaron nada
¿Qué pasa si mezclas Cocina + Mostrador en uno?
Si el chef se enferma → cierra el mostrador también. Los clientes no pueden hacer nada.
flowchart LR
subgraph Responsabilidades["Responsabilidades DIFERENTES"]
direction TB
subgraph DEPLOY["🎯 DEPLOYMENT (Deployment)"]
D1["❓ Cómo crear Pods?"]
D2["❓ Cuántos Pods?"]
D3["❓ Qué imagen usar?"]
D4["❓ Si un Pod muere,<br/>levanto otro?"]
end
subgraph SVC["🌐 SERVICE (Service)"]
S1["❓ A través de qué<br/>nombre me llaman?"]
S2["❓ Qué puerto debo<br/>exponer?"]
S3["❓ Cómo distribuyo<br/>requests entre Pods?"]
S4["❓ Los clients me<br/>encuentran siempre?"]
end
end
style DEPLOY fill:#51cf66,color:#fff
style SVC fill:#4dabf7,color:#fff
Tabla comparativa:
| Aspecto | Deployment | Service |
|---|---|---|
| ¿Qué es? | Receta para crear/mantener Pods | Punto de acceso estable a los Pods |
| Responsabilidad | Gestión de Pods (crear, replicar, actualizar) | Networking (cómo acceder a Pods) |
| Si falla | Levanta nuevo Pod automáticamente | Sigue funcionando, pero sin Pods donde ir |
| Ciclo de vida | Pods vienen y van (IPs cambian) | Name/IP Service permanece estable |
| Cambio en código? | Actualiza/redeploy necesario | Sin cambios |
| Cambio puerto? | Redeploy necesario | Solo actualizar Service |
| Ejemplo | deployment.yaml |
deployment.yaml (mismo archivo) |
sequenceDiagram
participant Admin as Admin<br/>kubectl apply
participant K8s as Kubernetes
participant Deploy as Deployment
participant Pod as Pod<br/>user-service
participant Svc as Service<br/>user-service
participant Client as Client<br/>curl
Admin->>K8s: kubectl apply -f deployment.yaml
K8s->>Deploy: "Crea un Pod con Node.js"
K8s->>Svc: "Crea un Service que apunte al Pod"
Deploy->>Pod: Levanta Pod #1
Svc->>Pod: Se percata: hay un Pod nuevo
Svc->>Svc: Memo: "Pod #1 → IP:4021"
Client->>Svc: curl http://user-service:4021/api/users
Svc->>Pod: Redirige al Pod #1
Pod->>Client: ✅ {"users": [...]}
Note over Pod: Problema: Pod #1 crashea
Pod->>Pod: ❌ Container muere
Deploy->>Pod: "Necesito 1 Pod, tengo 0"
Deploy->>Pod: Levanta Pod #2 (NUEVA IP)
Svc->>Pod: Se percata: Pod #2 reemplazó Pod #1
Svc->>Svc: Actualiza: "Pod #2 → IP:4021"
Client->>Svc: curl http://user-service:4021/api/users
Svc->>Pod: Redirige al Pod #2
Pod->>Client: ✅ {"users": [...]}
Note over Svc,Client: ✅ Cliente NO notó cambio!
¿Por qué son separados?
-
Separation of Concerns (Separación de responsabilidades)
- Deployment = "¿Existe el servicio?" (crear/eliminar Pods)
- Service = "¿Dónde está el servicio?" (encontrarlo consistentemente)
-
Flexibilidad
- Puedes tener 1 Service apuntando a múltiples Deployments
- Puedes cambiar la imagen del Deployment sin que el Service cambie
-
Descubrimiento automático
- Service es DNS interno:
usuarios-nodejssiempre es NAME - Deployment es solo la receta: puede cambiar
- Service es DNS interno:
Considera este escenario:
Escenario: Cambias el código de Node.js, recompiles la imagen (
user-service:2.0), y haceskubectl apply -f deployment.yamlnuevamente. El Deployment levanta un nuevo Pod con la nueva imagen.Pregunta: ¿Necesita el cliente cambiar la URL para conectarse?
- (A) Sí, porque la IP del Pod cambió
- (B) Sí, porque el puerto cambió
- (C) No, porque el Service sigue teniendo el mismo nombre
- (D) No, porque Kubernetes mapea "usuarios-nodejs" automáticamente
Respuesta correcta: (C) y (D) - El Service actúa como intermediario estable.
¿Qué es un PVC y cómo funciona la "portabilidad"?
- PVC =
PersistentVolumeClaim— una solicitud de almacenamiento persistente en Kubernetes. - Por qué importa: Los Pods (contenedores) son efímeros. Cuando se reinician, pierden datos en memoria. El PVC los preserva.
Diferencia crítica: Bind mount vs PVC
flowchart LR
subgraph DM["Bind Mount (Docker) - NO PORTÁTIL"]
MA["Máquina A: /Users/claudio/data/data.json"]
DRun["docker run -v /Users/claudio/... ✅"]
MB["Máquina B: /Users/claudio/... NO EXISTE"]
DErr["docker run ❌ Error"]
MA --> DRun
MB --> DErr
end
subgraph PVC_K8S["PVC (Kubernetes) - PORTÁTIL"]
NK1["Nodo A<br/>Pod v1<br/>/app/data"]
NK2["Nodo B<br/>Pod v1<br/>/app/data"]
PVCN["PVC 'usuarios-data'<br/>(agnóstico del nodo)"]
NK1 --> PVCN
NK2 --> PVCN
PVCN -->|Si Pod muere en A<br/>Kubernetes lo monta en B| NK2
end
style DErr fill:#ff6b6b
style DRun fill:#51cf66
style NK1 fill:#4dabf7
style NK2 fill:#4dabf7
style PVCN fill:#ffd43b
¿Cómo funciona realmente? (El mecanismo)
Cuando creas un PVC, Kubernetes no lo entierra en una máquina. Lo registra en un "registro central":
sequenceDiagram
participant Admin as Admin<br/>kubectl
participant K8s as Kubernetes<br/>Registry
participant NodeA as Nodo A
participant NodeB as Nodo B
participant Storage as Almacenamiento<br/>Persistente
Admin->>K8s: Paso 1: kubectl apply -f pvc.yaml
K8s->>K8s: Crea registro PVC<br/>{name: usuarios-data,<br/>status: Bound}
K8s->>Storage: Asigna espacio 1Gi
Admin->>K8s: Paso 2: kubectl apply -f deployment.yaml
K8s->>NodeA: Levanta Pod user-service
K8s->>Storage: Monta PVC en /app/data
NodeA->>Storage: Pod escribe datos
Storage->>Storage: Serializa datos en /var/lib/pvc-xxx
Admin->>K8s: Paso 3: Pod falla/reinicia
K8s->>K8s: Busca: PVC usuarios-data
alt Recursos en NodeB
K8s->>NodeB: Levanta nuevo Pod
else Sin recursos en A
K8s->>NodeB: Levanta en NodeB
end
K8s->>Storage: Monta el MISMO PVC
Storage->>NodeB: Devuelve datos originales
NodeB->>Storage: Nuevo Pod accede a datos
Analogía final: PVC es como una cuenta bancaria
Bind mount (USB que llevas contigo):
Llevas $100 en una billetera (en tu máquina)
Si cambias de país → dinero en moneda distinta, no puedes usar
Si pierdes la billetera → pierdes todo
Solo funciona EN ESTA MÁQUINA
PVC (Cuenta bancaria en varios bancos):
Tienes $100 en una "cuenta usuarios-data"
El banco (Kubernetes) tiene la referencia:
"nombre_cuenta": "usuarios-data",
"saldo": 100,
"donde_guardar": "/var/lib/..."
Vas a sucursal Nodo A → el banco te da acceso a $100
Sucursal Nodo A cierra (Pod muere)
Vas a sucursal Nodo B → el banco SIGUE ENCONTRANDO tu cuenta
Retiras: todavía hay $100 (los mismos)
❌ El dinero NO se copió
✅ El dinero es el MISMO en cualquier ubicación
✅ El banco decidió EN CUÁL SUCURSAL GUARDAR (no tú)
Después de entender Deployment, Service y PVC, revisa esta pregunta:
Escenario: Tu Pod A (en Node A) escribe datos en
/app/datausando el PVC. Luego, el Pod falla y Kubernetes levanta un Pod B (potencialmente en Node B diferente).Pregunta: ¿Qué ve el Pod B cuando accede a
/app/data?
- (A) Nada, porque Node B es una máquina diferente
- (B) Los datos ORIGINALES que escribió Pod A
- (C) Una copia del archivo de Node A
- (D) Un error porque la ruta
/app/datano existe en Node B
Respuesta correcta: (B) Los datos ORIGINALES
Explicación:
- ❌ No es (A): El PVC NO es un disco local de Node A
- ✅ Es (B): El PVC almacena datos en almacenamiento compartido (agnóstico del nodo)
- ❌ No es (C): Los datos NO se copian; son los MISMOS datos
- ❌ No es (D): El PVC se monta automáticamente en Node B en la misma ruta
Visualización en Docker Desktop (1 solo nodo):
Aunque solo tengas 1 nodo, el mecanismo es idéntico:
graph TD
DOCKER["docker-desktop<br/>(único nodo del cluster)"]
POD["🐳 Pod: 1/1 Running<br/>(usuarios-nodejs)"]
MOUNT["📂 /app/data monta PVC<br/>'usuarios-data'"]
DATA["📄 Lee/escribe en<br/>/var/lib/pvc-usuarios/data.json"]
PVC["💾 PVC 'usuarios-data'<br/>(registrado en Kubernetes)"]
STATUS["📊 Status: Bound"]
SIZE["📏 Size: 1Gi"]
PATH["🔗 Storage Path:<br/>/var/lib/pvc-usuarios/"]
FAILURE["⚠️ Si el Pod falla"]
K8S["🤖 Kubernetes"]
ACTION["🔄 Levanta nuevo Pod"]
RESULT["✅ PVC devuelve:<br/>datos originales persisten"]
DOCKER --> POD
DOCKER --> PVC
POD --> MOUNT
MOUNT --> DATA
PVC --> STATUS
PVC --> SIZE
PVC --> PATH
POD --> FAILURE
FAILURE --> K8S
K8S --> ACTION
ACTION --> RESULT
style DOCKER fill:#ff6b6b,color:#fff
style POD fill:#51cf66,color:#fff
style PVC fill:#ff9999,color:#fff
style FAILURE fill:#ffd43b,color:#333
style K8S fill:#4dabf7,color:#fff
style ACTION fill:#748ffc,color:#fff
style RESULT fill:#51cf66,color:#fff
Antes de empezar, necesitas entender cómo se relacionan los elementos de Kubernetes. No es exactamente una matroshka lineal (uno dentro de otro), sino una estructura anidada con algunos elementos coexistiendo horizontalmente:
flowchart TD
CLUSTER["🏢 CLUSTER<br/>(Todo el sistema Kubernetes)"]
CLUSTER --> NODO1["🖥 NODO 1<br/>(Máquina física/virtual)"]
CLUSTER --> NODO2["🖥 NODO 2<br/>(Máquina física/virtual)"]
CLUSTER --> INGRESS["🌐 INGRESS: micro.local<br/>Traefik - Punto entrada único<br/>/api/products → product-service<br/>/api/users → user-service"]
NODO1 --> NS1["📦 Namespace: microservicios"]
NODO2 --> NS2["📦 Namespace: microservicios"]
NS1 --> POD1["🐳 Pod: product-service-xxxxx"]
NS1 --> POD2["🐳 Pod: user-service-xxxxx"]
NS1 --> SVC1["🔌 Service: product-service<br/>ClusterIP:4020"]
NS1 --> SVC2["🔌 Service: user-service<br/>ClusterIP:4021"]
NS1 --> PVC["💾 PVC: users-data-pvc<br/>1Gi storage"]
POD1 --> CONT1["📝 Spring Boot Container<br/>Puerto: 4020"]
POD2 --> CONT2["📝 Express.js Container<br/>Puerto: 4021<br/>Monta: PVC users-data-pvc"]
SVC1 -.->|expone| POD1
SVC2 -.->|expone| POD2
POD2 -.->|monta| PVC
style CLUSTER fill:#ff6b6b,stroke:#333,color:#fff
style NODO1 fill:#4dabf7,color:#fff
style NODO2 fill:#4dabf7,color:#fff
style NS1 fill:#ffd43b,color:#333
style NS2 fill:#ffd43b,color:#333
style POD1 fill:#51cf66,color:#fff
style POD2 fill:#51cf66,color:#fff
style CONT1 fill:#748ffc,color:#fff
style CONT2 fill:#748ffc,color:#fff
style SVC1 fill:#a8e6cf,color:#333
style SVC2 fill:#a8e6cf,color:#333
style PVC fill:#ff9999,color:#fff
style INGRESS fill:#f0a,color:#fff
La jerarquía explicada de adentro hacia afuera:
1️⃣ Contenedor (nivel más específico)
- Es tu aplicación ejecutándose (Spring Boot, Express.js, etc.)
- Puertos específicos: 4020, 4021
- Imagen Docker:
product-service:1.0,user-service:1.0
2️⃣ Pod
- "Envase" que contiene 1+ contenedores
- En este proyecto: cada Pod tiene un contenedor
- Puede montar PVC (almacenamiento persistente)
- Kubernetes gestiona Pods, no contenedores directamente
3️⃣ Service
- Abstracción de networking
- Expone un Pod (o grupo de Pods) en una dirección dentro del cluster
- Tipo
ClusterIP: solo accesible desde adentro del cluster - Mapea: nombre interno (product-service) → IP virtual (10.0.0.x)
4️⃣ Namespace: un concepto que suele generar confusión al inicio
Tu pregunta anterior es la clave:
"¿El namespace es independiente del nodo pero único por cluster?" "¿Distintos artefactos desplegados en el mismo namespace pero en distintos nodos son parte de lo mismo?"
Respuesta: SÍ. Exactamente eso.
Imagina un edificio (Cluster) con:
- 3 pisos (Nodos)
- Cada piso tiene apartments (Pods)
Namespace = Asignación de espacios por tema:
-
Namespace "microservicios" = "Piso 1 izquierda" (toda esta zona)
- Apartment 1A (Pod user-service en Nodo A)
- Apartment 1B (Pod product-service en Nodo A)
- Apartment 1C (Pod user-service en Nodo B)
- Apartment 1D (Pod product-service en Nodo B)
✅ Todos en el mismo "sector del condominio" aunque estén en pisos diferentes
-
Namespace "otros-servicios" = "Piso 2 izquierda" (zona completamente separada)
- Apartment 2A (Pod de otro proyecto en Nodo A)
- Apartment 2B (Pod de otro proyecto en Nodo C)
❌ Completamente aislada del namespace "microservicios"
Características clave del Namespace:
| Característica | Explicación |
|---|---|
| Scope | Todo el Cluster (no está "atado" a ningún nodo específico) |
| Recursos dentro | Pueden estar distribuidos en MÚLTIPLES nodos |
| Aislamiento | Completo: otro namespace no ve tus Pods, Services, PVCs |
| DNS interno | nombre-servicio.namespace.svc.cluster.local |
| Cuotas | Puedes limitar recursos POR namespace (CPU, memoria, Pods) |
| Seguridad | Controlar acceso por namespace con RBAC |
graph TD
CLUSTER["🏢 Cluster Kubernetes<br/>(Edificio completo)"]
CLUSTER --> NODO_A["🖥 Nodo A<br/>(Piso 1)"]
CLUSTER --> NODO_B["🖥 Nodo B<br/>(Piso 2)"]
CLUSTER --> NODO_C["🖥 Nodo C<br/>(Piso 3)"]
NODO_A --> NS_A1["📦 NS: microservicios"]
NODO_A --> NS_A2["📦 NS: otros-servicios"]
NODO_B --> NS_B1["📦 NS: microservicios"]
NODO_B --> NS_B2["📦 NS: otros-servicios"]
NODO_C --> NS_C1["📦 NS: otro-equipo"]
NS_A1 --> POD_A1["🐳 Pod: user-service<br/>(en Nodo A)"]
NS_A1 --> POD_A2["🐳 Pod: product-service<br/>(en Nodo A)"]
NS_B1 --> POD_B1["🐳 Pod: user-service<br/>(en Nodo B)"]
NS_B1 --> POD_B2["🐳 Pod: product-service<br/>(en Nodo B)"]
NS_A2 --> POD_A3["🐳 Pod: auth<br/>(en Nodo A)"]
NS_B2 --> POD_B3["🐳 Pod: billing<br/>(en Nodo B)"]
NS_C1 --> POD_C1["🐳 Pod: ml-service<br/>(en Nodo C)"]
classDef nsA fill:#51cf66,stroke:#2d5016,color:#fff
classDef nsB fill:#4dabf7,stroke:#1a3a52,color:#fff
classDef nsC fill:#ffd43b,stroke:#664c00,color:#333
classDef nodeStyle fill:#748ffc,stroke:#2d3f70,color:#fff
classDef podStyle fill:#ff8c42,stroke:#663300,color:#fff
class NS_A1,NS_B1 nsA
class NS_A2,NS_B2 nsB
class NS_C1 nsC
class NODO_A,NODO_B,NODO_C nodeStyle
class POD_A1,POD_A2,POD_B1,POD_B2 podStyle
Lo que ves en el diagrama:
✅ El Namespace "microservicios" (verde) abarca múltiples nodos:
user-serviceen Nodo A ✓product-serviceen Nodo A ✓user-serviceen Nodo B ✓product-serviceen Nodo B ✓
Todos son parte de lo MISMO (mismo namespace), pero en diferentes máquinas físicas.
✅ El Namespace "otros-servicios" (azul) es completamente separado:
authen Nodo Abillingen Nodo B- Pero aislados del namespace "microservicios"
Significa que:
-
DNS compartida
- Pod A en Nodo A puede llamar a Pod B en Nodo B usando:
curl http://product-service:4020/api/products - Kubernetes resolve
product-servicea cualquier Pod en el mismo namespace, sin importar el nodo
- Pod A en Nodo A puede llamar a Pod B en Nodo B usando:
-
Reglas de red (Network Policy) aplicadas globalmente
- Una regla "deniega tráfico entre namespaces" se aplica a TODOS los nodos
- Si dices "microservicios no puede hablar con otros-servicios", se cumple en Nodo A, Nodo B, Nodo C, etc.
-
Cuotas y límites compartidos
- Si defines "namespace microservicios = máximo 100Gi de almacenamiento"
- Se suma TODA la memoria usada en Nodo A + Nodo B + Nodo C
-
Mismo contexto lógico
- Un equipo = gestiona 1 namespace
- Otro equipo = gestiona otro namespace
- No importa cuántos nodos ocupen; es una sola unidad conceptual
graph TD
DOCKER["🐳 docker-desktop (1 nodo)"]
DOCKER --> NS_MICRO["📦 Namespace: microservicios"]
DOCKER --> NS_DEFAULT["📦 Namespace: default"]
DOCKER --> NS_KUBE["📦 Namespace: kube-system"]
NS_MICRO --> POD1["🐳 Pod: user-service"]
NS_MICRO --> POD2["🐳 Pod: product-service"]
NS_MICRO --> SVC1["🔌 Service: user-service"]
NS_MICRO --> SVC2["🔌 Service: product-service"]
NS_MICRO --> PVC["💾 PVC: users-data-pvc"]
NS_DEFAULT --> INGRESS["🌐 Ingress: (Traefik)"]
NS_KUBE --> SYSTEM["🤖 Sistema Kubernetes"]
style NS_MICRO fill:#51cf66,color:#fff
style NS_DEFAULT fill:#a8e6cf,color:#333
style NS_KUBE fill:#748ffc,color:#fff
style DOCKER fill:#ff6b6b,color:#fff
| Pregunta | Respuesta |
|---|---|
| ¿Namespace es local a un nodo? | ❌ NO. Es cluster-wide |
| ¿Puedo tener el mismo namespace en múltiples nodos? | ✅ SÍ. De hecho, los namespaces ABARCAN múltiples nodos |
| ¿Pods de diferente namespace pueden comunicarse? | ❌ NO (están aislados) |
| ¿Pods del MISMO namespace en diferentes nodos pueden comunicarse? | ✅ SÍ (Kubernetes DNS los une) |
| ¿Es como una carpeta? | ❌ NO. Es como un sector lógico de infraestructura |
| ¿Cuánto namespace debo usar? | 1 por equipo / por ambiente (dev, staging, prod) |
En este laboratorio NO vas a crear Roles ni RoleBindings. Tú (el admin) tienes acceso total al cluster de Docker Desktop.
Pero es importante que entiendas qué es RBAC para:
- Entender producción (allá SÍ se usa)
- Imaginar: "Si mi compañero usara este cluster, ¿qué permisos le daría?"
- Siguientes módulos de seguridad en Kubernetes
Namespace aisla RECURSOS (Pods, Services, PVCs). Pero ¿quién ACCEDE a los recursos?
Ejemplo real de producción:
Namespace "microservicios" = Donde están los Pods
RBAC = Quién puede acceder/modificarlos
Personas en la empresa:
├── Juan (Developer)
│ └── Permisos: Ver Pods, logs, ejecutar kubectl exec
│ ❌ NO puede eliminar Pods
│ ❌ NO puede editar Deployments
│
├── María (DevOps)
│ └── Permisos: Ver/crear/editar/eliminar TODO
│ ✅ Puede hacer `kubectl apply` de manifiestos
│ ✅ Puede eliminar Pods si es necesario
│
├── Pedro (Intern)
│ └── Permisos: Solo ver (read-only)
│ ✅ Puede hacer `kubectl get pods`
│ ❌ NO puede cambiar nada
│
└── CI/CD (Script automático de despliegue)
└── Permisos: Deploy nuevas versiones
✅ Puede actualizar Deployments
❌ NO puede eliminar infraestructura
$ kubectl delete pod user-service-xxxxx
Error from server (Forbidden): pods "user-service-xxxxx" is forbidden:
User "juan" cannot delete resource "pods" in API group "" in the namespace "microservicios"Traducción: "Juan NO tiene permiso para eliminar Pods aquí"
flowchart LR
subgraph RBAC["RBAC: 3 Componentes"]
direction TB
subgraph USER["👤 USER/SERVICE ACCOUNT<br/>(Quién)"]
U1["Juan (usuario)"]
U2["scheduler (CI/CD)"]
U3["maria (admin)"]
end
subgraph ROLE["🎭 ROLE<br/>(Qué permisos)"]
R1["developer:<br/>ver, logs"]
R2["deployer:<br/>ver, editar"]
R3["admin:<br/>todo"]
end
subgraph BINDING["🔗 ROLEBINDING<br/>(Conectar)"]
B1["Juan → developer"]
B2["scheduler → deployer"]
B3["maria → admin"]
end
USER --> BINDING
ROLE --> BINDING
end
style USER fill:#ffd43b,color:#333
style ROLE fill:#51cf66,color:#fff
style BINDING fill:#4dabf7,color:#fff
En código Kubernetes (producción):
---
# 1. Define permisos (ROLE)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: microservicios
rules:
- apiGroups: [""]
resources: ["pods", "pods/logs"]
verbs: ["get", "list", "watch"] # Solo ver
---
# 2. Asigna permisos a una persona (ROLEBINDING)
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: juan-es-developer
namespace: microservicios
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: developer
subjects:
- kind: User
name: "juan@empresa.com"| Escenario | ¿RBAC? |
|---|---|
| Tu laptop local (Docker Desktop) | ❌ NO (administrador) |
| Proyecto en clase con compañeros | |
| Empresa real con 50 devs | ✅ SÍ (crítico) |
| App en Cloud (AWS EKS, GCP GKE) | ✅ SÍ (estándar) |
En este lab: Ignora RBAC. Tú tienes acceso total. 👑
5️⃣ Nodo
- Máquina física o virtual que forma parte del cluster
- En Docker Desktop: solo hay 1 nodo (
docker-desktop) - En producción: 3, 10, 100+ nodos
- Kubernetes decide EN CUÁL NODO ejecutar cada Pod
6️⃣ Cluster
- La estructura completa de Kubernetes
- Contiene Nodos + todos los recursos dentro
- Control plane (master): gestiona todo
- Data plane (workers): ejecutan Pods
En este laboratorio, tu estructura es:
graph TD
A["🏢 Cluster docker-desktop<br/>(1 solo nodo)"]
B["🖥 Nodo: docker-desktop"]
C["📦 Namespace: microservicios"]
D["🐳 Pod: product-service-xxxxx"]
E["📝 Contenedor: Spring Boot<br/>(puerto 4020)"]
F["🔌 Service: product-service<br/>(expone Pod)"]
G["🐳 Pod: user-service-xxxxx"]
H["📝 Contenedor: Express.js<br/>(puerto 4021)"]
I["💾 Monta: PVC users-data-pvc"]
J["🔌 Service: user-service<br/>(expone Pod)"]
K["🌐 Ingress: micro.local"]
L["/api/products → product-service"]
M["/api/users → user-service"]
N["💾 PVC: users-data-pvc<br/>(1Gi storage)"]
O["🚀 Traefik<br/>(controlador Ingress)"]
A --> B
B --> C
C --> D
C --> G
C --> K
C --> N
C --> O
D --> E
D --> F
G --> H
G --> I
G --> J
K --> L
K --> M
style A fill:#ff6b6b,color:#fff
style B fill:#4dabf7,color:#fff
style C fill:#ffd43b,color:#333
style D fill:#51cf66,color:#fff
style G fill:#51cf66,color:#fff
style E fill:#748ffc,color:#fff
style H fill:#748ffc,color:#fff
style N fill:#ff9999,color:#fff
style O fill:#a8e6cf,color:#333
Diferencia: Microservicio vs Pod vs Contenedor
- Microservicio: concepto de arquitectura. En tu caso: "products" y "users"
- Pod: recurso de Kubernetes. En tu caso: 2 Pods (1 por microservicio)
- Contenedor: software ejecutándose. En tu caso: 2 contenedores (1 por Pod)
En este proyecto, 1 microservicio = 1 Pod = 1 contenedor (relación 1:1:1). Pero en producción común: 1 microservicio = 1 Deployment = múltiples Pods = múltiples contenedores.
Antes de desplegar en Kubernetes, confirma que el cluster de Docker Desktop esta activo:
kubectl config get-contexts
kubectl config current-context
kubectl cluster-infoExplicacion:
kubectl config get-contexts: lista todos los contextos configurados (clusters disponibles).kubectl config current-context: muestra el contexto activo en este momento (debe serdocker-desktop).kubectl cluster-info: muestra informacion adicional del cluster.
Si por alguna razon el contexto no es docker-desktop, actívalo con:
kubectl config use-context docker-desktopExplicacion:
kubectl config use-context docker-desktop: cambia el contexto al cluster local de Docker Desktop.
Para este laboratorio con Docker Desktop local, usa imagenes locales sin registry.
Requisitos para esta seccion:
- Tener
kubectlinstalado. - Docker Desktop con Kubernetes habilitado (ver seccion 0).
Build local de imagenes:
docker build -t product-service:1.0 ./product-service
docker build -t user-service:1.0 ./user-serviceExplicacion:
docker build -t product-service:1.0 ./product-service: construye imagen local de Java.docker build -t user-service:1.0 ./user-service: construye imagen local de Node.js.- Estas imagenes son automaticamente visibles para el cluster de Docker Desktop.
Validacion:
kubectl config current-contextVerificar que la salida sea docker-desktop. Con esto, docker build y kubectl usan el mismo
entorno: no necesitas publicar imagenes en un registry para este laboratorio.
Si el cluster no encuentra imagenes locales (ImagePullBackOff), usa la opcion de registry remoto de esta guia.
Si necesitas trabajar con un registry remoto (opcional), entonces si aplica TU_USUARIO:
docker login
docker build -t TU_USUARIO/product-service:1.0 ./product-service
docker build -t TU_USUARIO/user-service:1.0 ./user-service
docker push TU_USUARIO/product-service:1.0
docker push TU_USUARIO/user-service:1.0Explicacion:
TU_USUARIOes tu usuario/namespace en Docker Hub o registry privado.- Este flujo es solo para despliegue no-local o cuando tu cluster no comparte imagenes locales.
En este proyecto los manifests ya vienen configurados para Docker Desktop (local):
product-service:1.0user-service:1.0
Con Docker Desktop, estas imagenes son automaticamente accesibles para el cluster. Si usas un registry remoto, cambia esos valores en los deployments por tus imagenes publicadas.
¿Tiene Docker Desktop Traefik instalado? Depende de tu versión. Vamos a verificar:
kubectl get pods -n kube-system | grep traefikSi ves una línea con traefik: ✅ Genial, ya está instalado. Continúa con 4.3.
Si NO ves nada:
# 1. Agregar el repositorio de Traefik
helm repo add traefik https://traefik.github.io/charts
helm repo update
# 2. Instalar Traefik
helm install traefik traefik/traefik \
--namespace kube-system \
--set service.type=LoadBalancer
# 3. Esperar a que se inicie (puede tomar 30-60 segundos)
kubectl get pods -n kube-system | grep traefik¿Qué hace este comando?
helm install traefik traefik/traefik: descarga e instala Traefik desde el repositorio oficial--namespace kube-system: lo coloca en el namespace del sistema (donde van los controladores)--set service.type=LoadBalancer: configura el servicio como LoadBalancer para recibir tráfico en localhost:80
Salida esperada después de unos segundos:
traefik-xxxxx 1/1 Running 0 5m
Una vez veas Running, Traefik está listo. Continúa con sección 4.3.
Desde la raiz microservicios:
kubectl apply -f k8s/namespace.yaml
kubectl apply -f user-service/k8s/pvc.yaml
kubectl apply -f product-service/k8s/deployment.yaml
kubectl apply -f user-service/k8s/deployment.yamlExplicacion de cada linea:
kubectl apply: crea o actualiza recursos declarados en un archivo YAML.-f: indica el archivo a aplicar.k8s/namespace.yaml: crea namespacemicroservicios.user-service/k8s/pvc.yaml: crea el PVC para persistencia de usuarios.product-service/k8s/deployment.yaml: crea/actualiza Deployment + Service de products.user-service/k8s/deployment.yaml: crea/actualiza Deployment + Service de users.
Por que este orden es necesario:
k8s/namespace.yamlva primero porque todos los demas recursos se crean dentro demicroservicios; si el namespace no existe, los apply siguientes fallan.user-service/k8s/pvc.yamlva antes del deployment de users porque el Pod necesita ese claim para montar/app/data; si el PVC no existe, el Pod puede quedar en estado pendiente o con error de montaje.product-service/k8s/deployment.yamlcrea el servicio de products y su Pod; se aplica despues del namespace para asegurar aislamiento y nombres consistentes.user-service/k8s/deployment.yamlse aplica al final para que encuentre el PVC ya creado y pueda iniciar con persistencia activa desde el primer arranque.
En resumen, la secuencia evita errores de dependencias: primero contenedor logico (namespace), luego almacenamiento (PVC), y despues workloads (deployments/services).
kubectl get ns
kubectl get pods -n microservicios
kubectl get svc -n microservicios
kubectl get pvc -n microserviciosExplicacion:
kubectl get ns: lista namespaces del cluster.kubectl get pods -n microservicios: lista pods del namespace objetivo.kubectl get svc -n microservicios: lista servicios y puertos publicados dentro del cluster.kubectl get pvc -n microservicios: muestra estado del claim (Bound,Pending, etc.).-n microservicios: limita la consulta al namespace del proyecto.
Estados comunes del PVC y como interpretarlos:
Bound: estado esperado. El claim ya fue enlazado a un volumen real y el Pod puede montar almacenamiento persistente.Pending: Kubernetes aun no encuentra o no puede aprovisionar almacenamiento para ese claim.Lost: el claim estaba enlazado pero perdio su volumen asociado.
Que hacer si aparece Pending:
kubectl get storageclass
kubectl describe pvc users-data-pvc -n microservicios
kubectl get events -n microservicios --sort-by=.metadata.creationTimestampExplicacion rapida:
kubectl get storageclass: confirma si existe una clase de almacenamiento disponible.kubectl describe pvc ...: muestra el motivo exacto delPending(por ejemplo, falta de StorageClass).kubectl get events ...: entrega eventos recientes para detectar errores de aprovisionamiento.
Abre dos terminales:
kubectl port-forward svc/product-service 4020:4020 -n microservicios
kubectl port-forward svc/user-service 4021:4021 -n microserviciosExplicacion:
kubectl port-forward: expone temporalmente un servicio del cluster en tu maquina local.svc/product-service: service de destino de products.svc/user-service: service de destino de users.4020:4020y4021:4021: puerto local:puerto remoto del service.-n microservicios: namespace donde estan los servicios.- Estos comandos quedan en primer plano; usa una terminal por servicio.
Luego prueba:
curl http://localhost:4020/api/products
curl http://localhost:4021/api/usersExplicacion:
curlenvia requests HTTP de prueba.localhost:4020prueba products via port-forward.localhost:4021prueba users via port-forward.
Este paso permite mostrar que puedes tener varias replicas de cada app y, aun asi, seguir teniendo solo dos microservicios logicos (product-service y user-service).
Escalar deployments a 3 replicas:
kubectl scale deployment/product-service --replicas=3 -n microservicios
kubectl scale deployment/user-service --replicas=3 -n microserviciosExplicacion:
kubectl scale: cambia la cantidad de pods de un Deployment.deployment/product-service: deployment que se escala.deployment/user-service: deployment que se escala.--replicas=3: cantidad objetivo de pods.-n microservicios: namespace donde viven los deployments.
Verificar replicas:
kubectl get deployments -n microservicios
kubectl get pods -n microservicios -o wide
kubectl get svc -n microserviciosQue deberias observar:
- En
deployments, cada servicio muestra3/3replicas listas. - En
pods, aparecen 3 pods para products y 3 pods para users. - En
svc, siguen existiendo solo 2 servicios (product-serviceyuser-service). - Conclusion: hay mas instancias (pods), pero la cantidad de microservicios logicos no cambia.
Volver a 1 replica por servicio:
kubectl scale deployment/product-service --replicas=1 -n microservicios
kubectl scale deployment/user-service --replicas=1 -n microservicios¿Qué es un Ingress?
- Ingress es un recurso de Kubernetes que define reglas HTTP/HTTPS para el tráfico de entrada al cluster.
- Problema que resuelve: Sin Ingress, cada microservicio necesitaría su propio puerto expuesto (NodePort), lo que es tedioso y poco escalable.
- Solución: Centraliza toda la lógica de enrutamiento en un único punto de entrada.
¿Qué es Traefik?
Traefik es un Ingress Controller — el software que entiende los objetos Ingress y los ejecuta.
- Lee los archivos
Ingress.yamlque defines en el cluster - Escucha en puerto 80 (HTTP) y 443 (HTTPS) de tu máquina
- Actúa como proxy reverso: es el único punto de entrada al cluster
- Recibe requests externas (navegador, Postman, curl)
- Decide a qué Service interno enviar la request basándose en host y path
- El cliente no sabe dónde están realmente los microservicios \u2014 solo habla con Traefik
Analogía: Traefik es como un recepcionista en la entrada de una empresa:
- Cliente llama: "Quiero hablar con ventas"
- Recepcionista (Traefik): "Déjame dirigirte a la oficina correcta"
- Cliente nunca ve todas las oficinas, solo interactúa con la recepción
¿Por qué es necesario?
- Sin Ingress, cada servicio suele necesitar su propio mecanismo de exposicion.
- Con Ingress, puedes publicar multiples microservicios bajo un mismo punto de entrada.
- Facilita agregar TLS, hostnames y reglas centralizadas. Por "reglas centralizadas" se
entiende que en lugar de configurar cada microservicio por separado para manejar seguridad,
rutas o dominios, todas esas decisiones se definen en un solo lugar (el manifiesto Ingress).
Por ejemplo:
- TLS: en vez de instalar un certificado SSL en cada microservicio, se configura una vez en el Ingress y Traefik termina la conexion HTTPS antes de reenviar al servicio interno.
- Hostnames: se puede enrutar
api.miapp.cla un servicio yadmin.miapp.cla otro, sin tocar el codigo de ninguno. - Redirects, rate limiting, autenticacion basica: se activan como middlewares en Traefik y aplican a todos los servicios que pasen por el, sin duplicar logica en cada uno.
Pasos para configurar Ingress + Traefik:
- Verificar que Traefik este disponible.
kubectl get ingressclass
kubectl get pods -A | grep -i traefikkubectl get ingressclass lista los controladores de Ingress registrados en el cluster.
Docker Desktop instala Traefik automaticamente en su cluster Kubernetes, por lo que no es
necesario instalarlo manualmente. La salida esperada es:
NAME CONTROLLER PARAMETERS AGE
traefik traefik.io/ingress-controller <none> 24h
NAME: el valor que vas a poner eningressClassName: traefikdentro del YAML de Ingress.CONTROLLER: identifica que pod es responsable de leer y aplicar las reglas de enrutamiento.PARAMETERS: sin configuracion extra; usa los valores por defecto de Traefik.AGE: tiempo desde que iniciaste Kubernetes en Docker Desktop.
kubectl get pods -A | grep -i traefik muestra todos los pods relacionados con Traefik en
cualquier namespace. La salida esperada es:
kube-system helm-install-traefik-xxxxx 0/1 Completed 1 24h
kube-system helm-install-traefik-crd-xxxxx 0/1 Completed 0 24h
kube-system svclb-traefik-xxxxx 2/2 Running 2 24h
kube-system traefik-xxxxx 1/1 Running 2 24h
helm-install-traefik: job que instalo Traefik via Helm al crear el cluster. EstadoCompletedes normal: se ejecuta una vez y queda como registro historico.helm-install-traefik-crd: job que instalo los CRDs (tipos de objetos adicionales que Traefik registra en Kubernetes). TambienCompletedes el estado esperado.svclb-traefik: ServiceLB, el balanceador de carga liviano de k3s. Es el que expone el puerto 80 y 443 de tu maquina hacia el pod de Traefik. Sin el, el trafico externo no entraria al cluster.traefik: el pod principal, el proxy reverso en si. Lee los objetos Ingress que defines y decide a que Service derivar cada request segun el path de la URL.
El flujo completo de una request via Ingress es:
flowchart LR
USER["👤 Cliente<br/>curl http://micro.local/api/products"]
PORT["🔌 Puerto 80<br/>(tu Mac)"]
LB["⚖️ svclb-traefik<br/>(entra al cluster)"]
TRAEFIK["🚦 Traefik Pod<br/>(lee Ingress)<br/>decide destino por path"]
SVC["🔌 Service<br/>product-service:4020<br/>(ClusterIP interno)"]
POD["🐳 Pod Spring Boot<br/>puerto 4020<br/>responde"]
USER -->|localhost:80| PORT
PORT -->|solicitud HTTP| LB
LB -->|entra al cluster| TRAEFIK
TRAEFIK -->|path: /api/products| SVC
SVC -->|redirecciona| POD
POD -->|respuesta JSON| USER
style USER fill:#ffd43b,color:#333
style PORT fill:#a8e6cf,color:#333
style LB fill:#748ffc,color:#fff
style TRAEFIK fill:#f0a,color:#fff
style SVC fill:#4dabf7,color:#fff
style POD fill:#51cf66,color:#fff
- Aplicar el manifiesto Ingress.
kubectl apply -f k8s/ingress-traefik.yaml
kubectl get ingress -n microservicioskubectl apply -f k8s/ingress-traefik.yaml registra en el cluster las reglas de enrutamiento
definidas en el YAML: le dice a Traefik que cuando llegue una request con el host micro.local
y el path /api/products, la derive al Service product-service, y lo mismo para /api/users.
Sin aplicar este archivo, Traefik no sabe nada de tus microservicios; solo tiene el controlador
activo pero sin reglas configuradas.
kubectl get ingress -n microservicios verifica que el objeto Ingress fue creado correctamente.
La salida esperada muestra el host micro.local, los paths configurados y el puerto 80. Si la
columna ADDRESS aparece vacia, espera unos segundos y vuelve a ejecutarlo.
- Agregar resolucion local para
micro.local(mantener mientras uses el cluster).
echo "127.0.0.1 micro.local" | sudo tee -a /etc/hosts✅ MANTÉN esta entrada mientras estes usando Kubernetes en el laboratorio. ❌ Solo la eliminara cuando hagas limpieza completa (ver seccion 4.9).
Este paso es necesario aunque el Ingress ya este aplicado. El motivo es que Kubernetes e Ingress
solo resuelven el enrutamiento dentro del cluster: Traefik sabe que micro.local/api/products
va al Service correcto, pero tu Mac no sabe a que IP apunta el nombre micro.local. Sin este
paso, el sistema operativo intenta resolver micro.local via DNS, no encuentra nada, y la
conexion falla antes de llegar al cluster.
El comando agrega una linea al archivo /etc/hosts de macOS, que es el primer lugar donde el
sistema operativo busca nombres de host antes de consultar un servidor DNS:
127.0.0.1 micro.local
Esto le indica a tu Mac: "cuando alguien pida micro.local, dirigelo a localhost (127.0.0.1)".
Desde ahi, svclb-traefik (que escucha en el puerto 80 de localhost) recibe la request y la
pasa al pod de Traefik, que aplica las reglas del Ingress.
Ciclo de vida de esta entrada:
stateDiagram-v2
[*] --> Agregada: echo "127.0.0.1 micro.local" | sudo tee -a /etc/hosts
Agregada --> Mantenida: Ingress configurado\ny cluster corriendo
Mantenida --> Mantenida: Horas, días, semanas\nde uso del laboratorio
Mantenida --> Eliminada: sed -i '/micro.local/d' /etc/hosts\n(limpieza final)
Eliminada --> [*]: Laboratorio terminado
note right of Agregada
Paso 3: Configurar Ingress
Acceso a micro.local
desde navegador/curl
end note
note right of Mantenida
Kernel busca micro.local
en /etc/hosts primero
→ 127.0.0.1 (localhost)
→ Traefik (puerto 80)
→ Microservicios
end note
note right of Eliminada
Sección 4.9: Limpieza
micro.local eliminado
/etc/hosts vuelve a original
end note
Desglose del comando:
echo "127.0.0.1 micro.local": genera el texto de la linea a agregar.|: pasa ese texto como entrada al siguiente comando.sudo tee -a /etc/hosts: escribe el texto al final del archivo/etc/hostscon permisos de administrador (-asignifica append, agrega sin borrar lo que ya existe).
En Windows (PowerShell como Administrador), el equivalente es:
Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "127.0.0.1 micro.local"O manualmente: clic derecho en Notepad → "Ejecutar como administrador" → Archivo → Abrir
→ C:\Windows\System32\drivers\etc\hosts → agregar 127.0.0.1 micro.local al final → guardar.
No existe sudo en Windows; la elevacion de privilegios se obtiene ejecutando la aplicacion
o terminal como Administrador.
- Probar requests via Ingress.
curl http://micro.local/api/products
curl http://micro.local/api/usersNota:
- Si
micro.localno responde, prueba con header Host:
curl -H "Host: micro.local" http://127.0.0.1/api/products
curl -H "Host: micro.local" http://127.0.0.1/api/usersIngress vs API Gateway:
- Ingress y API Gateway no son exactamente lo mismo.
- Ingress (con Traefik) resuelve principalmente enrutamiento HTTP/HTTPS y terminacion TLS.
- Un API Gateway completo suele agregar politicas avanzadas: autenticacion centralizada, quotas, rate limit por consumidor, versionado, analitica y monetizacion.
- En proyectos pequenos/medianos, Traefik + middlewares puede cubrir parte del rol de API Gateway.
En esta seccion agregamos un ejemplo minimo para explicar secretos de forma didactica.
- Un
Secretevita hardcodear valores sensibles en codigo o manifests. - En este laboratorio usaremos un valor demo (no productivo).
- Nunca subas secretos reales a Git.
Para valores no sensibles (por ejemplo APP_MODE=dev) puedes definir una variable normal en el Deployment:
env:
- name: APP_MODE
value: "dev"Esto es util para configuracion comun, pero no para tokens, passwords o API keys.
- Crear el Secret desde linea de comandos.
kubectl create secret generic users-api-secret \
--from-literal=API_TOKEN=token-demo-fullstack3 \
-n microserviciosExplicacion:
create secret generic: crea un objetoSecretde tipo clave/valor.--from-literal=API_TOKEN=...: define la claveAPI_TOKEN.-n microservicios: lo crea en el mismo namespace de la app.
- Verificar que el Secret exista (sin imprimir el valor).
kubectl get secret users-api-secret -n microservicios
kubectl describe secret users-api-secret -n microservicios- Inyectar el Secret como variable de entorno en
user-service.
Agrega este bloque dentro del contenedor en user-service/k8s/deployment.yaml:
env:
- name: APP_MODE
value: "dev"
- name: API_TOKEN
valueFrom:
secretKeyRef:
name: users-api-secret
key: API_TOKENLuego reaplica el deployment:
kubectl apply -f user-service/k8s/deployment.yaml- Probar que ambas variables existen en el pod.
kubectl get pods -n microservicios
kubectl exec -it <NOMBRE_POD_USERS> -n microservicios -- printenv APP_MODE
kubectl exec -it <NOMBRE_POD_USERS> -n microservicios -- printenv API_TOKENSalida esperada:
dev
token-demo-fullstack3
- Limpieza del ejemplo (opcional).
kubectl delete secret users-api-secret -n microserviciosRespuesta corta:
- Viven como objetos
Secretdentro de la API de Kubernetes (en el namespace correspondiente). - El cluster los persiste en
etcd. - Por defecto van codificados en base64 (esto no es cifrado fuerte por si solo).
Buenas practicas de plataforma:
- Habilitar encryption at rest para Secrets en
etcd. - Limitar acceso con RBAC.
- Evitar imprimir secretos en logs o
kubectl describecon valores expuestos.
No. kubectl create secret ... es util para demos o pruebas rapidas, pero en produccion normalmente se usa alguno de estos enfoques:
- GitOps + cifrado de manifiestos
- Ejemplo:
SealedSecretsoSOPS. - El repositorio guarda solo la version cifrada.
- Gestor externo de secretos
- Ejemplo: Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault.
- Kubernetes sincroniza secretos mediante
External Secrets Operatoro CSI drivers.
- Pipeline CI/CD
- El pipeline inyecta secretos en deploy sin dejarlos en texto plano en el repo.
Resumen pedagógico:
ConfigMap: configuracion no sensible.Secret: datos sensibles.- Una variable de entorno puede venir de
value(simple) osecretKeyRef(sensible). - En produccion, no depender solo de CLI manual: usar automatizacion + cifrado + control de acceso.
flowchart TB
subgraph Client[Cliente]
U[Browser / Postman / curl]
end
subgraph Entry[Entrada al cluster]
T[Traefik Ingress Controller]
I[Ingress
host: micro.local
paths: /api/products, /api/users]
end
subgraph K8s[Namespace: microservicios]
SP[Service product-service ClusterIP:4020]
SU[Service user-service ClusterIP:4021]
DP[Deployment product-service]
DU[Deployment user-service]
PP1[Pod product-service #1]
PP2[Pod product-service #2 optional scale]
PU1[Pod user-service #1]
PU2[Pod user-service #2 optional scale]
PVC[PVC users-data-pvc]
PV[(Persistent Volume)]
end
subgraph Build[Build local]
DJ[Dockerfile product-service]
DN[Dockerfile user-service]
IMJ[Imagen product-service:1.0]
IMN[Imagen user-service:1.0]
end
U --> T
T --> I
I -->|/api/products| SP
I -->|/api/users| SU
SP --> DP
SU --> DU
DP --> PP1
DP --> PP2
DU --> PU1
DU --> PU2
PU1 --> PVC
PU2 --> PVC
PVC --> PV
DJ --> IMJ
DN --> IMN
IMJ --> DP
IMN --> DU
Desde la raiz microservicios:
kubectl delete -f k8s/ingress-traefik.yaml --ignore-not-found
kubectl delete -f user-service/k8s/deployment.yaml --ignore-not-found
kubectl delete -f product-service/k8s/deployment.yaml --ignore-not-found
kubectl delete -f user-service/k8s/pvc.yaml --ignore-not-found
kubectl delete -f k8s/namespace.yaml --ignore-not-foundExplicacion de cada linea:
kubectl delete: elimina recursos definidos en archivos YAML.-f: indica el archivo fuente.--ignore-not-found: evita error si el recurso ya no existe.k8s/ingress-traefik.yaml: elimina las reglas de enrutamiento del Ingress de Traefik.user-service/k8s/deployment.yaml: elimina Deployment + Service de users y sus Pods.product-service/k8s/deployment.yaml: elimina Deployment + Service de products y sus Pods.user-service/k8s/pvc.yaml: elimina el PVC de users. Los datos del volumen dependen de la reclaim policy del StorageClass (local-pathlos elimina junto con el PVC).k8s/namespace.yaml: elimina el namespacemicroserviciosy todo lo que quede dentro en cascada (Services, ConfigMaps, Pods remanentes, etc.).
Nota sobre Pods y Services: no es necesario eliminarlos individualmente. Al eliminar un Deployment, Kubernetes termina y borra los Pods que gestionaba. Al eliminar el namespace, todo su contenido cae en cascada automaticamente.
Eliminar la entrada micro.local del archivo hosts al FINALIZAR (limpieza completa):
127.0.0.1 micro.local de forma permanente.
No ejecutes esto mientras estes usando Kubernetes; solo al terminar el laboratorio completamente.
macOS/Linux:
sudo sed -i '' '/micro\.local/d' /etc/hostsExplicacion:
sudo sed -i '': edita el archivo en su lugar sin backup (opcion-i ''requerida en macOS).'/micro\.local/d': patrón de busqueda (/micro\.local/) + accion borrado (d).- Ejemplo: si tu archivo hosts contiene:
Despues del comando solo quedara:
127.0.0.1 localhost 127.0.0.1 micro.local127.0.0.1 localhost
Windows (PowerShell como Administrador):
(Get-Content "C:\Windows\System32\drivers\etc\hosts") |
Where-Object { $_ -notmatch 'micro\.local' } |
Set-Content "C:\Windows\System32\drivers\etc\hosts"Explicacion:
Get-Content: lee el archivo hosts linea por linea.Where-Object { $_ -notmatch 'micro\.local' }: conserva SOLO las lineas que NO contienenmicro.local.Set-Content: reescribe el archivo con el resultado filtrado.- Resultado final: la linea
127.0.0.1 micro.localdesaparece permanentemente.
Opcional: limpiar imagenes locales Docker despues del laboratorio:
docker rmi -f product-service:1.0 user-service:1.0 2>/dev/null || true
docker image prune -fExplicacion:
docker rmi -f ...: elimina las imagenes etiquetadas del proyecto.2>/dev/null || true: oculta errores si alguna imagen no existe; en Git Bash (Windows) este redireccionamiento funciona igual que en macOS/Linux.docker image prune -f: elimina imagenes sin tag no usadas por ningun contenedor.
Pregunta frecuente:
"Si
user-serviceguarda datos endata.json, ¿puedeproduct-serviceleer ese mismo archivo?"
Respuesta corta:
- Sí, técnicamente es posible en Docker run, Docker Compose y Kubernetes.
- Pero no es lo recomendado para microservicios en producción.
- Recomendación arquitectónica:
product-servicedebería consumir datos por API deuser-service, no por archivo compartido.
Con la configuración actual:
user-servicesí monta el archivo/persistencia de usuarios.product-serviceno monta ese volumen.- Por lo tanto, hoy
product-serviceno puede leerdata.jsonpor filesystem.
Puedes montar el mismo archivo host también en el contenedor Java:
docker run -d --name product-service -p 4020:4020 \
-v $(pwd)/user-service/data/data.json:/app/shared/users.json:ro \
product-service:1.0Notas:
:romonta en solo lectura (recomendado para evitar escrituras accidentales).- Esto solo da acceso al archivo; para usarlo, la app Java debe tener código que lea
/app/shared/users.json.
En docker-compose.yml, monta el mismo volumen en ambos servicios:
services:
user-service:
volumes:
- ./user-service/data/data.json:/app/data/data.json
product-service:
volumes:
- ./user-service/data/data.json:/app/shared/users.json:roNotas:
- Funciona bien para laboratorio local.
- En producción con microservicios, compartir archivos entre servicios suele ser antipatrón.
Puedes montar el mismo PVC en product-service.
Ejemplo en product-service/k8s/deployment.yaml:
spec:
template:
spec:
containers:
- name: product-service
volumeMounts:
- name: users-shared-data
mountPath: /app/shared
readOnly: true
volumes:
- name: users-shared-data
persistentVolumeClaim:
claimName: users-data-pvcConsideraciones reales:
- Si el PVC es
ReadWriteOnce, el comportamiento depende de StorageClass/nodo. - En Docker Desktop (1 nodo) normalmente se puede probar sin problema.
- En clusters multinodo, para acceso concurrente multi-pod/multi-nodo suele requerirse
ReadWriteMany+ backend compatible (NFS/EFS/CephFS, etc.).
¿Qué significa exactamente "depende de StorageClass/nodo"?
- Depende del StorageClass porque ese recurso define el tipo de almacenamiento real (disco local, NFS, EBS/EFS, Ceph, etc.) y sus capacidades.
- Depende del nodo porque algunos volúmenes solo pueden montarse en un nodo a la vez (por ejemplo, muchos casos con
ReadWriteOnce).
Ejemplo práctico:
- Si
user-serviceyproduct-serviceestan en el mismo nodo:
- Con
ReadWriteOncenormalmente ambos pods pueden montar el volumen sin problema.
- Si Kubernetes agenda un pod en otro nodo distinto:
- Con
ReadWriteOnce, puede fallar el montaje o quedar el pod enPending, porque el volumen ya esta "atado" a otro nodo.
- Si quieres acceso simultaneo desde varios nodos:
- Necesitas
ReadWriteManyy un backend compatible (NFS/EFS/CephFS, etc.).
Comandos de verificacion utiles:
kubectl get pvc users-data-pvc -n microservicios -o yaml
kubectl get storageclass
kubectl get pods -n microservicios -o wide
kubectl describe pod <NOMBRE_POD> -n microserviciosQue mirar:
accessModesdel PVC (ReadWriteOnceoReadWriteMany).storageClassNamedel PVC y su tipo de provisionamiento.- En que nodo quedo cada pod (
NODEenkubectl get pods -o wide). - Eventos de error de montaje en
kubectl describe pod.
Conviene en escenarios didácticos cuando se quiere mostrar:
- Montaje de volúmenes.
- Diferencia entre acceso por red (API) vs acceso por filesystem.
Evítalo en diseño productivo de microservicios cuando:
- Quieres bajo acoplamiento entre servicios.
- Necesitas independencia de despliegue y versionado.
- Quieres que cada servicio sea dueño de sus datos.
Regla práctica:
- Didáctico: compartir volumen puede servir para entender persistencia.
- Producción: preferir comunicación por API/eventos, no por archivo compartido.
Objetivo: Agregar un API Gateway (KrakenD) entre Traefik y los microservicios, y un frontend React que consuma las APIs. Con esto el flujo end-to-end queda completo: navegador → Traefik → KrakenD → microservicios.
Qué aprenderás: Qué es un API Gateway y por qué existe, build multi-stage de Docker para frontend estático, ConfigMap como mecanismo de configuración de KrakenD.
Traefik ya enruta tráfico — ¿entonces para qué agregar KrakenD?
| Responsabilidad | Traefik (ya tenés) | KrakenD (nuevo) |
|---|---|---|
| Enrutamiento por host/path | ✅ | ✅ |
| TLS termination (HTTPS) | ✅ | ❌ (no es su rol) |
| Rate limiting por consumidor | Básico | ✅ por API key/JWT |
| Transformación de requests | Limitado | ✅ |
| Agregación de múltiples backends | ❌ | ✅ |
| Validación JWT / API Keys | ❌ | ✅ |
| Analytics por endpoint/consumidor | ❌ | ✅ |
Regla simple: Traefik es la puerta del edificio (quién entra al cluster). KrakenD es la recepcionista (a qué piso y con qué permisos).
Ambos coexisten sin solaparse:
Browser
└─► Traefik (edge del cluster, TLS, entrada)
└─► KrakenD (API Gateway: auth, rate limit, agregación)
├─► product-service:4020
└─► user-service:4021
KrakenD es stateless: no tiene base de datos. Toda su configuración vive en un archivo
krakend.json que se monta en el pod como un ConfigMap de Kubernetes. Esto lo hace
ideal para IaC: la configuración es código, versionada en git.
El build de Vite (React) produce HTML + JS + CSS puros — no hay lógica de servidor. nginx sirve esos archivos estáticos de forma mucho más eficiente que Node.js:
- Sin runtime de JavaScript en producción
- Sin Garbage Collector
- Cache HTTP nativo (
Cache-Control,ETag) - Imagen Docker final: ~25MB (vs ~400MB con node_modules)
El truco está en el Dockerfile multi-stage:
Stage 1: node:20-alpine Stage 2: nginx:alpine
├── npm ci ├── COPY --from=builder /app/dist
├── npm run build │ /usr/share/nginx/html
└── genera /app/dist/ └── imagen final sin node_modules
(HTML/JS/CSS puro)
El stage 2 solo copia el resultado compilado del stage 1. node_modules nunca entra
a la imagen final.
microservicios/
├── frontend-service/
│ ├── Dockerfile ← multi-stage: node build → nginx serve
│ ├── nginx.conf ← fallback a index.html para React Router
│ ├── package.json
│ ├── vite.config.js ← proxy /api en desarrollo local
│ ├── index.html
│ ├── src/
│ │ ├── main.jsx
│ │ └── App.jsx ← consume /api/products y /api/users
│ └── k8s/
│ └── deployment.yaml ← Deployment nginx + Service ClusterIP
├── krakend/
│ └── k8s/
│ ├── krakend-config.yaml ← ConfigMap con krakend.json
│ └── deployment.yaml ← Deployment KrakenD + Service ClusterIP
└── k8s/
├── ingress-traefik.yaml ← Fase 4 (sin cambios)
└── ingress-traefik-v2-gateway.yaml ← Fase 5 (reemplaza al anterior)
flowchart TB
subgraph Client[Cliente]
U[Browser / Postman / curl]
end
subgraph Entry[Entrada al cluster]
T[Traefik Ingress Controller]
I[Ingress v2
host: micro.local
/api → krakend
/ → frontend-service]
end
subgraph K8s[Namespace: microservicios]
FE[Service frontend-service ClusterIP:80]
DFE[Deployment frontend-service]
PFE[Pod nginx sirviendo dist/]
GW[Service krakend ClusterIP:8080]
DGW[Deployment KrakenD]
PGW[Pod KrakenD]
CM[ConfigMap krakend-config]
SP[Service product-service ClusterIP:4020]
DP[Deployment product-service]
PP[Pod product-service]
SU[Service user-service ClusterIP:4021]
DU[Deployment user-service]
PU[Pod user-service]
PVC[PVC users-data-pvc]
end
U --> T
T --> I
I -->|/| FE
I -->|/api| GW
FE --> DFE --> PFE
GW --> DGW --> PGW
CM -.->|montado como volumen| PGW
PGW -->|/api/products| SP --> DP --> PP
PGW -->|/api/users| SU --> DU --> PU
PU --> PVC
docker build -t frontend-service:1.0 ./frontend-serviceKrakenD usa la imagen oficial devopsfaith/krakend:2.7 — no necesita build local.
kubectl apply -f krakend/k8s/krakend-config.yaml
kubectl apply -f krakend/k8s/deployment.yamlExplicacion:
krakend-config.yaml: crea el ConfigMap conkrakend.json(rutas, backends, timeouts).deployment.yaml: crea el Deployment que usa la imagen oficial y monta el ConfigMap, más el Service ClusterIP que Traefik usará para enrutar/api.
kubectl apply -f frontend-service/k8s/deployment.yamlExplicacion:
- Crea el Deployment de nginx con la imagen
frontend-service:1.0. - Crea el Service ClusterIP que Traefik usará para enrutar
/.
kubectl apply -f k8s/ingress-traefik-v2-gateway.yamlComo el nuevo Ingress tiene el mismo name: microservicios-ingress, el apply lo
sobreescribe en el cluster sin necesidad de borrar nada manual. Traefik detecta el cambio
y actualiza sus rutas automáticamente.
Diferencia entre v1 y v2:
| Path | Ingress v1 (Fase 4) | Ingress v2 (Fase 5) |
|---|---|---|
/api/products |
→ product-service:4020 | — |
/api/users |
→ user-service:4021 | — |
/api (todo) |
— | → krakend:8080 |
/health |
→ user-service:4021 | → krakend:8080 |
/ |
— | → frontend-service:80 |
kubectl get pods -n microserviciosDeberías ver estos 4 pods:
NAME READY STATUS RESTARTS
frontend-service-xxxxx 1/1 Running 0
krakend-xxxxx 1/1 Running 0
product-service-xxxxx 1/1 Running 0
user-service-xxxxx 1/1 Running 0
# Frontend React (navegador o curl)
curl http://micro.local
# API a través de KrakenD
curl http://micro.local/api/products
curl http://micro.local/api/users
# Health check
curl http://micro.local/healthOo abrir directamente http://micro.local en el navegador — verás el frontend React
cargando los datos de los dos microservicios.
Para iterar en el frontend sin necesitar Kubernetes:
# Terminal 1: levantar los microservicios
docker compose up -d
# Terminal 2: frontend en modo dev
cd frontend-service
npm install
npm run dev
# → http://localhost:3000El vite.config.js ya tiene configurado el proxy para que /api en local apunte
a http://localhost:8080. Si no tenés KrakenD local corriendo, podés ajustarlo
tranquilamente para que apunte directo a http://localhost:4020 y http://localhost:4021
durante el desarrollo.
Desde esta fase el frontend ya incluye los formularios de Iniciar sesión y Crear cuenta integrados directamente en la página principal.
Importante:
- Los formularios llaman a
POST /auth/loginyPOST /auth/registerdel auth-service. - Si el auth-service aún no está desplegado (Fase 5), los formularios se muestran pero las peticiones darán error 503 — esto es esperado.
- Al completar Fase 6, el login y registro funcionan completamente.
- El JWT resultante se guarda en
localStoragedel navegador.
Si necesitás agregar o modificar endpoints en KrakenD:
- Editar
krakend/k8s/krakend-config.yaml(la secciónkrakend.jsondel ConfigMap). - Aplicar el cambio:
kubectl apply -f krakend/k8s/krakend-config.yaml- Reiniciar el pod para que tome la nueva configuración:
kubectl rollout restart deployment/krakend -n microservicios- Verificar que el rollout terminó:
kubectl rollout status deployment/krakend -n microserviciosPara volver al estado de la Fase 4 (solo los dos microservicios):
# Revertir el Ingress a la version original
kubectl apply -f k8s/ingress-traefik.yaml
# Eliminar KrakenD
kubectl delete -f krakend/k8s/deployment.yaml --ignore-not-found
kubectl delete -f krakend/k8s/krakend-config.yaml --ignore-not-found
# Eliminar frontend
kubectl delete -f frontend-service/k8s/deployment.yaml --ignore-not-foundPara desmantelar todo (Fases 4 y 5 juntas):
kubectl delete -f k8s/ingress-traefik-v2-gateway.yaml --ignore-not-found
kubectl delete -f krakend/k8s/deployment.yaml --ignore-not-found
kubectl delete -f krakend/k8s/krakend-config.yaml --ignore-not-found
kubectl delete -f frontend-service/k8s/deployment.yaml --ignore-not-found
kubectl delete -f user-service/k8s/deployment.yaml --ignore-not-found
kubectl delete -f product-service/k8s/deployment.yaml --ignore-not-found
kubectl delete -f user-service/k8s/pvc.yaml --ignore-not-found
kubectl delete -f k8s/namespace.yaml --ignore-not-foundLimpiar imágenes locales:
docker rmi -f frontend-service:1.0 product-service:1.0 user-service:1.0 2>/dev/null || trueObjetivo: agregar un microservicio propio de autenticacion con JWT y persistencia en PostgreSQL.
En esta fase se integra:
- auth-service (Node.js/Express): endpoints
POST /auth/registeryPOST /auth/login - PostgreSQL (persistencia de usuarios con password encriptado con bcrypt)
- JWT (JSON Web Token) para sesiones sin estado
auth-service/k8s/secrets.yaml— credenciales de PostgreSQL y JWT secretauth-service/k8s/postgres.yaml— PostgreSQL Deployment + PVC + Serviceauth-service/k8s/deployment.yaml— auth-service Deployment + Servicek8s/ingress-traefik-v3-gateway-auth.yaml— Ingress actualizado con ruta/auth
| Campo | Tipo | Restriccion |
|---|---|---|
name |
VARCHAR | requerido |
last_name |
VARCHAR | requerido |
email |
VARCHAR | requerido, único |
username |
VARCHAR | requerido, único |
password_hash |
VARCHAR | bcrypt 10 rounds |
phone |
VARCHAR | opcional |
address |
TEXT | opcional |
docker build -t auth-service:1.0 ./auth-servicekubectl apply -f auth-service/k8s/secrets.yaml
kubectl apply -f auth-service/k8s/postgres.yaml
kubectl apply -f auth-service/k8s/deployment.yaml
kubectl apply -f k8s/ingress-traefik-v3-gateway-auth.yamlkubectl get pods -n microserviciosDeberias ver en Running:
auth-postgres-xxxxxauth-service-xxxxx
| Método | Ruta | Descripción |
|---|---|---|
POST |
/auth/register |
Registra usuario, devuelve JWT |
POST |
/auth/login |
Valida credenciales, devuelve JWT |
GET |
/auth/me |
Devuelve datos del usuario (requiere Authorization: Bearer <token>) |
GET |
/auth/health |
Health check |
# Registrar usuario
curl -X POST http://micro.local/auth/register \
-H 'Content-Type: application/json' \
-d '{"name":"Ana","lastName":"Gomez","email":"ana@duoc.cl","username":"ana","password":"123456"}'
# Login
curl -X POST http://micro.local/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"ana","password":"123456"}'
# Ver perfil (reemplaza <TOKEN> con el JWT recibido)
curl http://micro.local/auth/me \
-H 'Authorization: Bearer <TOKEN>'kubectl apply -f k8s/ingress-traefik-v2-gateway.yaml
kubectl delete -f auth-service/k8s/deployment.yaml --ignore-not-found
kubectl delete -f auth-service/k8s/postgres.yaml --ignore-not-found
kubectl delete -f auth-service/k8s/secrets.yaml --ignore-not-foundbash scripts/deploy-all.shbash scripts/teardown.shNotas importantes del script teardown.sh:
- Por defecto elimina despliegues/ingress y conserva datos de Kubernetes (PVC/namespace) para acelerar el siguiente despliegue.
- Para borrar imagenes:
bash scripts/teardown.sh --delete-images. - Para borrar volumenes:
bash scripts/teardown.sh --delete-volumes. - Para borrar datos persistentes de Kubernetes:
bash scripts/teardown.sh --delete-k8s-data. - Para limpieza total:
bash scripts/teardown.sh --full-clean.
Objetivo: demostrar cómo un microservicio llama a otro dentro del cluster de Kubernetes usando el DNS interno, sin pasar por el API Gateway.
La cadena completa es:
Browser → Traefik → KrakenD → cart-service:4022 → product-service:4020
| Componente | Tecnología |
|---|---|
| API | FastAPI 0.111 + uvicorn |
| Base de datos | asyncpg (PostgreSQL no bloqueante) |
| JWT | python-jose (HS256, mismo secreto que auth-service) |
| HTTP interno | httpx async |
cart-service/src/main.py— app FastAPI, lifespan, CORScart-service/src/db.py— pool asyncpg, creación de tablacart_itemscart-service/src/auth.py— dependenciaget_current_user(), valida JWTcart-service/src/routes/cart.py— endpoints GET/POST/PATCH/DELETEcart-service/k8s/secrets.yaml— credenciales de cart-postgrescart-service/k8s/postgres.yaml— PostgreSQL dedicado para el carritocart-service/k8s/deployment.yaml— Deployment + Service (ClusterIP, port 4022)krakend/k8s/krakend-config-v2.yaml— ConfigMap v2 con endpoints/api/cart/*
| Campo | Tipo | Restricción |
|---|---|---|
user_id |
INTEGER | FK implícita al sub del JWT |
product_id |
INTEGER | ID del producto en product-service |
quantity |
INTEGER | > 0 |
(user_id, product_id) |
UNIQUE | evita duplicados |
docker build -t cart-service:1.0 ./cart-servicekubectl apply -f cart-service/k8s/secrets.yaml
kubectl apply -f cart-service/k8s/postgres.yaml
kubectl apply -f cart-service/k8s/deployment.yaml
kubectl apply -f krakend/k8s/krakend-config-v2.yaml
kubectl rollout restart deployment krakend -n microservicios| Método | Ruta | Auth | Descripción |
|---|---|---|---|
GET |
/api/cart/health |
No | Health check |
GET |
/api/cart |
JWT | Carrito completo con datos de producto |
POST |
/api/cart/items |
JWT | Agrega producto (body: {product_id, quantity}) |
PATCH |
/api/cart/items/{product_id} |
JWT | Actualiza cantidad |
DELETE |
/api/cart/items/{product_id} |
JWT | Elimina un ítem |
DELETE |
/api/cart |
JWT | Vacía todo el carrito |
# Health (sin JWT)
curl http://micro.local/api/cart/health
# Login para obtener token
TOKEN=$(curl -s -X POST http://micro.local/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"ana","password":"123456"}' | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")
# Ver carrito (vacío)
curl -H "Authorization: Bearer $TOKEN" http://micro.local/api/cart
# Agregar producto
curl -X POST http://micro.local/api/cart/items \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"product_id": 1, "quantity": 2}'
# Ver carrito enriquecido con datos del producto
curl -H "Authorization: Bearer $TOKEN" http://micro.local/api/cart
# Vaciar carrito
curl -X DELETE -H "Authorization: Bearer $TOKEN" http://micro.local/api/cart{
"user": { "id": 1, "username": "ana", "email": "ana@duoc.cl" },
"items": [
{
"id": 3,
"product_id": 1,
"quantity": 2,
"product": { "id": 1, "name": "Laptop", "price": 999.99 },
"subtotal": 1999.98
}
],
"total": 1999.98
}Dentro de cart-service/src/routes/cart.py, la función fetch_product() hace una llamada HTTP directa usando el DNS interno de Kubernetes:
async with httpx.AsyncClient(timeout=3.0) as client:
r = await client.get(f"http://product-service:4020/api/products/{product_id}")product-service resuelve al Service de Kubernetes del mismo nombre, en el namespace microservicios. Esta comunicación ocurre dentro del cluster, sin pasar por Traefik ni KrakenD.
kubectl apply -f krakend/k8s/krakend-config.yaml
kubectl rollout restart deployment krakend -n microservicios
kubectl delete -f cart-service/k8s/deployment.yaml --ignore-not-found
kubectl delete -f cart-service/k8s/postgres.yaml --ignore-not-found
kubectl delete -f cart-service/k8s/secrets.yaml --ignore-not-foundbash scripts/deploy-all.shbash scripts/teardown.shNotas importantes del script teardown.sh:
- Por defecto elimina despliegues/ingress y conserva datos de Kubernetes (PVC/namespace) para acelerar el siguiente despliegue.
- Para borrar imagenes:
bash scripts/teardown.sh --delete-images. - Para borrar volumenes:
bash scripts/teardown.sh --delete-volumes. - Para borrar datos persistentes de Kubernetes:
bash scripts/teardown.sh --delete-k8s-data. - Para limpieza total:
bash scripts/teardown.sh --full-clean.
Visión de todos los servicios desplegados en Kubernetes y sus relaciones.
graph TB
subgraph EXT["🌐 Exterior (Browser)"]
BROWSER["Browser\nhttp://micro.local"]
end
subgraph K8S["☸️ Namespace: microservicios"]
TRAEFIK["Traefik Ingress\nmicro.local:80"]
subgraph GW["API Gateway"]
KRAKEND["KrakenD\n:8080"]
end
subgraph FRONT["Frontend"]
NGINX["frontend-service\nnginx :80"]
end
subgraph AUTH["Auth Service (Fase 6)"]
AUTH_SVC["auth-service\nNode.js :3002"]
AUTH_PG["auth-postgres\nPostgreSQL :5432"]
AUTH_SECRET["Secret: auth-secrets\nJWT_SECRET · DB_*"]
end
subgraph CART["Cart Service (Fase 7)"]
CART_SVC["cart-service\nFastAPI :4022"]
CART_PG["cart-postgres\nPostgreSQL :5432"]
CART_SECRET["Secret: cart-secrets\nDB_*"]
end
subgraph MS["Microservicios"]
JAVA["product-service\nSpring Boot :4020"]
NODE["user-service\nExpress :4021"]
PVC["PVC: users-data\n1Gi"]
end
end
BROWSER -->|"HTTP :80"| TRAEFIK
TRAEFIK -->|"/api /health"| KRAKEND
TRAEFIK -->|"/auth"| AUTH_SVC
TRAEFIK -->|"/"| NGINX
KRAKEND -->|"/api/products*"| JAVA
KRAKEND -->|"/api/users* /health"| NODE
KRAKEND -->|"/api/cart* Authorization→"| CART_SVC
CART_SVC -->|"HTTP interno\nDNS del cluster"| JAVA
CART_SVC --- CART_PG
CART_SVC -.-|"JWT_SECRET"| AUTH_SECRET
CART_SVC --- CART_SECRET
AUTH_SVC --- AUTH_PG
AUTH_SVC --- AUTH_SECRET
NODE --- PVC
style EXT fill:#f0f4ff,stroke:#aab
style K8S fill:#f9f9f9,stroke:#ccc
style GW fill:#fff3cd,stroke:#f0ad4e
style FRONT fill:#d4edda,stroke:#28a745
style AUTH fill:#cce5ff,stroke:#0d6efd
style CART fill:#ffe5d0,stroke:#fd7e14
style MS fill:#e8d5f5,stroke:#6f42c1
Muestra cómo fluye GET /api/cart desde el browser hasta enriquecer la respuesta con datos de product-service.
sequenceDiagram
actor U as 👤 Usuario<br/>(Browser)
participant T as Traefik<br/>Ingress
participant K as KrakenD<br/>:8080
participant C as cart-service<br/>FastAPI :4022
participant P as product-service<br/>Spring Boot :4020
participant DB as cart-postgres<br/>PostgreSQL
U->>T: GET /api/cart<br/>Authorization: Bearer JWT
T->>K: /api/cart (forwarded)
Note over K: input_headers: [Authorization]<br/>pasa el JWT al backend
K->>C: GET /cart<br/>Authorization: Bearer JWT
C->>C: get_current_user()<br/>valida JWT con JWT_SECRET
Note over C: Decodifica: sub=1, username="ana"
C->>DB: SELECT * FROM cart_items<br/>WHERE user_id = 1
DB-->>C: [{product_id:2, qty:3}, ...]
loop Por cada ítem del carrito
C->>P: GET /api/products/2<br/>(DNS interno: product-service:4020)
P-->>C: {id:2, name:"Laptop", price:999.99}
end
C->>C: Calcula subtotales y total
C-->>K: {"user":…, "items":[…], "total":2999.97}
K-->>T: Response (no-op, sin transformar)
T-->>U: 200 OK + JSON enriquecido
Muestra cómo auth-service emite el token y cómo cart-service lo valida sin llamar al auth-service (secreto compartido).
sequenceDiagram
actor U as 👤 Usuario
participant F as Frontend<br/>React (nginx)
participant AS as auth-service<br/>Node.js :3002
participant PG as auth-postgres
participant CS as cart-service<br/>FastAPI :4022
Note over U,CS: 1️⃣ REGISTRO / LOGIN
U->>F: Click "Iniciar sesión"<br/>{username, password}
F->>AS: POST /auth/login
AS->>PG: SELECT usuario WHERE username=?
PG-->>AS: {id:1, password_hash:"$2b$..."}
AS->>AS: bcrypt.compare(password, hash)
AS-->>F: {token: "eyJhbGci...", user: {...}}
F->>F: localStorage.setItem("token", ...)
Note over F: AuthContext actualiza isLoggedIn=true
Note over U,CS: 2️⃣ USO DEL CARRITO (sin contactar auth-service)
U->>F: Click "🛒 Agregar"
F->>CS: POST /api/cart/items<br/>Authorization: Bearer eyJhbGci...
Note over CS: jose.jwt.decode(token, JWT_SECRET)<br/>Mismo secreto → sin HTTP call al auth-service
CS->>CS: user_id = payload["sub"]
CS-->>F: {message: "Producto agregado", item: {...}}
Note over U,CS: 🔑 El JWT_SECRET vive en k8s Secret "auth-secrets"<br/>Tanto auth-service como cart-service lo leen al arrancar
Muestra qué objeto Kubernetes se despliega en cada fase del laboratorio.
graph LR
subgraph F4["Fase 4 — Kubernetes base"]
NS["Namespace\nmicroservicios"]
D1["Deployment\nproduct-service"]
S1["Service\nproduct-service :4020"]
D2["Deployment\nuser-service"]
S2["Service\nuser-service :4021"]
PVC1["PVC\nusers-data 1Gi"]
I1["Ingress\nmicro.local → svcs"]
end
subgraph F5["Fase 5 — API Gateway + Frontend"]
CM["ConfigMap\nkrakend.json"]
D3["Deployment\nkrakend"]
S3["Service\nkrakend :8080"]
D4["Deployment\nfrontend-service"]
S4["Service\nfrontend-service :80"]
I2["Ingress v2\n/api→krakend /→frontend"]
end
subgraph F6["Fase 6 — Auth Service"]
SEC1["Secret\nauth-secrets"]
D5["Deployment\nauth-postgres"]
S5["Service\nauth-postgres :5432"]
PVC2["PVC\nauth-postgres 1Gi"]
D6["Deployment\nauth-service"]
S6["Service\nauth-service :3002"]
I3["Ingress v3\n/auth→auth-service"]
end
subgraph F7["Fase 7 — Cart Service"]
SEC2["Secret\ncart-secrets"]
D7["Deployment\ncart-postgres"]
S7["Service\ncart-postgres :5432"]
PVC3["PVC\ncart-postgres 1Gi"]
D8["Deployment\ncart-service"]
S8["Service\ncart-service :4022"]
CM2["ConfigMap v2\nkrakend + /api/cart*"]
end
F4 --> F5 --> F6 --> F7
style F4 fill:#e8d5f5,stroke:#6f42c1,color:#000
style F5 fill:#fff3cd,stroke:#f0ad4e,color:#000
style F6 fill:#cce5ff,stroke:#0d6efd,color:#000
style F7 fill:#ffe5d0,stroke:#fd7e14,color:#000
Todas las rutas expuestas públicamente (a través de Traefik → KrakenD) y las internas (dentro del cluster).
graph TD
ML["micro.local :80"]
ML -->|"GET /api/products"| KR["KrakenD :8080"]
ML -->|"GET /api/products/{id}"| KR
ML -->|"GET /api/users"| KR
ML -->|"GET /api/users/{id}"| KR
ML -->|"POST /api/users"| KR
ML -->|"GET /health"| KR
ML -->|"GET /api/cart/health"| KR
ML -->|"GET /api/cart 🔐"| KR
ML -->|"POST /api/cart/items 🔐"| KR
ML -->|"PATCH /api/cart/items/{id} 🔐"| KR
ML -->|"DELETE /api/cart/items/{id} 🔐"| KR
ML -->|"DELETE /api/cart 🔐"| KR
ML -->|"POST /auth/register"| AU["auth-service :3002"]
ML -->|"POST /auth/login"| AU
ML -->|"GET /auth/me 🔐"| AU
ML -->|"GET /auth/health"| AU
ML -->|"GET /"| FE["frontend-service :80"]
KR -->|"productos"| PJ["product-service :4020"]
KR -->|"usuarios"| UN["user-service :4021"]
KR -->|"carrito"| CS["cart-service :4022"]
CS -.->|"HTTP interno\nfetch_product()"| PJ
style ML fill:#343a40,color:#fff
style KR fill:#f0ad4e,color:#000
style AU fill:#0d6efd,color:#fff
style FE fill:#28a745,color:#fff
style PJ fill:#6f42c1,color:#fff
style UN fill:#20c997,color:#fff
style CS fill:#fd7e14,color:#fff
🔐 = endpoint protegido, requiere
Authorization: Bearer <JWT>
Este laboratorio entrega una base muy sólida, pero en industria todavía faltan competencias clave para pasar de "funciona" a "operable y seguro en producción".
- Gestión de sesión robusta: refresh tokens, revocación y rotación de secretos.
- Protección de login: rate limiting, lockout progresivo y auditoría de intentos.
- Hardening del frontend: reducir riesgo de XSS y manejo seguro de credenciales.
- Políticas de seguridad en Kubernetes: RBAC real, NetworkPolicies y principio de mínimo privilegio.
- Pruebas unitarias y de integración automatizadas por servicio.
- Pruebas end-to-end del flujo completo (auth + cart + products).
- Criterios de calidad en CI (fallar pipeline si hay tests o linting en rojo).
- Logging estructurado y correlación por request-id.
- Métricas y alertas (latencia, errores 5xx, tasa de login fallido).
- Dashboards operativos para detectar degradación antes de caída.
- Pipeline CI/CD con build, test, escaneo de vulnerabilidades y despliegue controlado.
- Estrategias de despliegue (rolling, blue/green o canary) con rollback automático.
- Gestión de configuración por ambiente (dev/stage/prod) sin duplicar manifiestos.
- Versionado de APIs y compatibilidad hacia atrás.
- Migraciones de base de datos (schema evolution) con trazabilidad.
- Manejo de errores estandarizado (códigos, mensajes y contratos de respuesta).
- Timeouts, reintentos y circuit breaker en llamadas internas.
- Patrones de tolerancia a fallos para que una caída parcial no derribe todo el sistema.
- Pruebas de carga y de caos para validar comportamiento bajo estrés.
Después de completar este laboratorio, deberías entender:
- Este proyecto muestra cuatro servicios principales (
product-service,user-service,auth-service,cart-service) que podrían ser un solo monolito. - Ventaja: Equipos separados pueden trabajar en paralelo, escalar servicios individuales, usar diferentes lenguajes.
- Desventaja: Más complejidad operacional, necesitas orquestar la comunicación entre servicios.
- Aprendiste a empaquetar aplicaciones en imágenes Docker reutilizables para Java, Node.js y Python.
- Las imágenes garantizan: "funciona en mi computador" = "funciona en producción" (reproducibilidad).
- Concepto clave: capas de imagen (cada
RUN,COPYcrea una capa; se cachean para build rápido).
- Docker Compose: Orquestador local para desarrollo. Define servicios + redes en
docker-compose.yml. - Kubernetes: Orquestador de producción. Maneja auto-escalado, auto-recuperación, actualización sin downtime.
- En este lab, Compose fue el "trampolín" a Kubernetes y luego se extendió con API Gateway, auth y carrito.
- En Docker: bind mount = vincula archivo host ↔ contenedor (temporal, no es portátil).
- En Kubernetes: PVC = solicita almacenamiento persistente (agnóstico del nodo, portable).
- Aprendiste que los datos en memoria de un contenedor se pierden si reinicia → necesitas persistencia; en este laboratorio se aplicó en
user-service,auth-serviceycart-service.
- Fase 1-2: Accedías servicios en
localhost:4020(desarrollo local). - Fase 3: Docker Compose las exponía automáticamente.
- Fase 4: Kubernetes + Ingress + Traefik = punto único de entrada + routing inteligente.
- Fase 5: KrakenD como API Gateway centraliza auth, rate limiting y transformacion. nginx sirve el frontend React estático. El e2e queda completo: browser → Traefik → KrakenD → microservicios.
- Fase 7: cart-service llama a product-service internamente (DNS de cluster). Esta es la comunicación inter-microservicio real, directa, sin pasar por el API Gateway.
- Implementaste
auth-serviceconregister,loginyme, usando bcrypt + JWT. - Integraste el frontend para iniciar/cerrar sesión y consumir rutas protegidas.
- Aplicaste propagación de
Authorizationa través de KrakenD haciacart-service.
- Los archivos
deployment.yaml,service.yaml,ingress.yamldeclaran el estado deseado. - Kubernetes mantiene ese estado: si un Pod muere, Kubernetes lo crea de nuevo automáticamente.
- Concepto: Declarativo (decimos qué queremos) vs Imperativo (decimos cómo hacerlo).
- También aplicaste gestión por fases (
ingress/krakend-configv1→v2) y rollback por manifiestos.
- Automatización de despliegue (base CI/CD): Se implementaron scripts reproducibles de ciclo completo (
deploy-all.shyteardown.sh), versionado en Git y release por tags. Lo que falta para CI/CD completo es pipeline automático (GitHub Actions u otro) con build+test+deploy. - Operación y troubleshooting: Se trabajó con diagnósticos reales de pods, logs, rollouts y rollback por fases. Lo que falta para observabilidad completa es métricas, alertas y dashboards centralizados.
- Seguridad aplicada (nivel laboratorio): Se implementó JWT, bcrypt, Secrets en Kubernetes y endpoints protegidos en auth/cart. Lo que falta para nivel productivo es TLS, RBAC estricto, NetworkPolicies, rotación de secretos y hardening adicional.
- Docker oficial: https://docs.docker.com/
- Kubernetes oficial: https://kubernetes.io/docs/
- Traefik Ingress: https://doc.traefik.io/traefik/providers/kubernetes-ingress/
- Spring Boot + Gradle: https://spring.io/guides
- Express.js: https://expressjs.com/
- Completar persistencia en todos los dominios:
auth-serviceycart-serviceya usan PostgreSQL; como siguiente paso, migrar tambiénproduct-servicey/ouser-servicea base de datos real. - CI/CD real: Agregar GitHub Actions para build, test, escaneo y deploy automático por rama/tag.
- Observabilidad completa: Integrar métricas y logs centralizados (Prometheus/Grafana + stack de logs).
- Validación JWT en KrakenD: Mover parte de la validación al gateway para simplificar microservicios.
- Rate limiting en KrakenD: Definir límites por IP/API key y políticas de protección en login.
- Pruebas de carga y resiliencia: Incorporar
k6oApache JMeterpara validar rendimiento y estabilidad.
Última actualización: Junio 2026 | Para: Estudiantes DUOC Fullstack 3 - Ingeniería de Software