Fundamentos De Docker y Kubernetes — De Cero a Capitán
Introduction
📋 Programa Detallado del Entrenamiento: De Cero a Capitán en Kubernetes
Workshop Oficial — Cloud Native Medellín 🏔️☕
- Repositorio Oficial en GitHub: https://github.com/edwinbermudez91/Fundamentos-Kubernetes
- Comunidad CNCF Medellín: https://community.cncf.io/cloud-native-medellin/
- Sitio Web: https://cloudnativemedellin.com/
- Duración: 6 Semanas (Módulos teórico-prácticos con retos interactivos)
- Nivel: Principiante a Intermedio (No se requiere experiencia previa en Kubernetes)
🎯 Objetivos de Aprendizaje
Al finalizar este entrenamiento serás capaz de:
- Comprender a fondo qué es un contenedor y cómo el kernel de Linux (
namespacesycgroups) aísla procesos sin duplicar sistemas operativos. - Escribir
Dockerfilessiguiendo las mejores prácticas de la industria (imágenes ligeras, multi-stage builds, usuarios sin privilegiosroot). - Dominar la arquitectura interna de Kubernetes: Control Plane (
kube-apiserver,etcd,kube-scheduler,kube-controller-manager) y Worker Nodes (kubelet,kube-proxy, container runtime). - Diseñar, desplegar y depurar manifiestos declarativos en YAML para Pods, Deployments, Services, ConfigMaps, Secrets y Volúmenes.
- Gestionar el ciclo de vida de aplicaciones en producción: despliegues continuos sin caídas (Rolling Updates), autorreparación (Self-healing) y reversiones (Rollbacks).
- Configurar enrutamiento avanzado con Services (
ClusterIP,NodePort) eIngress. - Operar con resiliencia y seguridad: sondas de salud (
liveness/readiness), límites de consumo de CPU/RAM para evitarOOMKilled, y control de acceso basado en roles (RBAC). - Empaquetar aplicaciones complejas con Helm y desplegar una arquitectura multinivel lista para producción.
🗓️ Calendario Curricular por Módulos
🐳 Módulo 00: Fundamentos de Docker — De Código a Contenedor para Kubernetes
Descripción:
Antes de que Kubernetes pueda orquestar aplicaciones, necesitamos empaquetarlas de manera estándar e inmutable. En esta semana desmitificamos qué es un contenedor frente a una máquina virtual pesada y construimos nuestras primeras imágenes OCI, gestionamos redes, volúmenes, registros empresariales con Harbor y corremos modelos de IA en contenedores con Ollama.
- Conceptos Teóricos:
- La metáfora del tinto colombiano: café (código), receta (
Dockerfile), bolsa sellada (imagen), greca activa (contenedor). - Máquinas Virtuales vs. Contenedores: hipervisores vs. Kernel compartido de Linux.
- Tecnologías base del contenedor:
Namespaces(aislamiento de red, procesos PID y filesystem) yCgroups(límites de CPU y memoria). - Anatomía del
Dockerfile:FROM,WORKDIR,COPY,RUN,EXPOSE,ENV,CMDvsENTRYPOINT. - Buenas prácticas de seguridad y optimización: imágenes Alpine, usuario no-root (
USER),COPY --chown,dockerignorey gestión de señales PID 1 (SIGTERM). - Auditoría de vulnerabilidades con Trivy (CVEs en SO, dependencias y malas prácticas de Dockerfile).
- Registros OCI empresariales con Harbor: control de acceso (RBAC), Cuentas Robot y políticas de prevención.
- Inferencia de Inteligencia Artificial local en contenedores con Ollama (CPU y GPU).
- La metáfora del tinto colombiano: café (código), receta (
- Laboratorios Prácticos (6 Laboratorios Oficiales):
01-servidor-web-html: Servidor Nginx estático sobre Alpine Linux (~25 MB) y proceso en primer plano.02-app-python-api: API REST en Python con usuario sin privilegios (appuser), variablesENV,.dockerignorey UI web interactiva.03-redes-comunicacion-contenedores: Comunicación entre microservicios (Greca Backend y Carrito Cliente) en red bridge personalizada con resolución DNS interna por nombre.04-almacenamiento-y-volumenes: Persistencia real con Named Volumes (docker volume) vs Bind Mounts (-v) frente al almacenamiento efímero.05-registro-imagenes-harbor: Ciclo de distribución OCI completo: autenticación con Cuentas Robot,docker tag,docker pushy escaneo con Trivy en el portal.06-ia-local-ollama: Inferencia de LLM (Qwen2.5) en CPU con límites de memoria y cores,Modelfilepara asistente DevOps y API REST en127.0.0.1:11434.- Escaneo de seguridad obligatorio con Trivy CLI en cada imagen construida.
- Puente local a Kubernetes: cómo cargar imágenes locales en clústeres de Kind con
kind load docker-image.
- Los 5 Retos Rotos de la Semana 00:
Reto 01: Proceso en segundo plano apaga el contenedor (PID 1).Reto 02: Contenedor crashea por falta de variable de entorno (ENV).Reto 03: Contenedor vulnerable ejecutándose comoroot(Seguridad).Reto 04: Fuga de contraseñas y secretos por ausencia de.dockerignore.Reto 05: Conexión rechazada por enlazar a127.0.0.1y puerto desalineado.
🌱 Módulo 01: El Motor por Dentro y tu Primer Pod
Descripción:
Entendemos qué hace Kubernetes por debajo, cómo interactúan sus componentes internos y desplegamos nuestra primera unidad de cómputo: el Pod.
- Conceptos Teóricos:
- ¿Por qué necesitamos un orquestador? Caídas de servidores, balanceo de carga y escalabilidad automática.
- Arquitectura del Control Plane (El Cerebro):
API Server: La puerta de entrada parakubectly el único que habla conetcd.etcd: La base de datos clave-valor distribuida con el estado deseado.Scheduler: Asigna Pods a nodos con recursos disponibles.Controller Manager: El guardián que detecta desviaciones y reconcilia el estado.
- Arquitectura del Worker Node (La Estación del Metro):
Kubelet: El agente o capataz que ejecuta los contenedores en el nodo.Kube-proxy: Maneja las reglas de red local (iptables/IPVS).- Container Runtime:
containerdoCRI-O.
- Anatomía de un Pod: ¿Por qué Kubernetes no corre contenedores sueltos? Red compartida (
localhost) y volúmenes compartidos. - Patrón Sidecar: contenedores de apoyo en el mismo Pod.
- Laboratorios Prácticos:
- Creación y gestión de un clúster local con Kind (
kind create cluster --name cnm-lab). - Despliegue de un Pod simple con
kubectl apply -f mi-primer-pod.yaml. - Inspección y solución de problemas con
kubectl describe podykubectl logs. - Acceso directo desde el navegador local mediante
kubectl port-forward. - Despliegue de un Pod multi-contenedor con patrón Sidecar (servidor web + actualizador de contenido).
- Instalación y uso de Headlamp: consola visual open source para Kubernetes.
- Creación y gestión de un clúster local con Kind (
- Reto Roto 01:
- Depuración de un Pod rebelde que nunca llega a estado
Runningdebido aImagePullBackOffy comandos mal configurados.
- Depuración de un Pod rebelde que nunca llega a estado
🔄 Módulo 02: Despliegues Sin Caídas y Autorreparación
Descripción:
En producción los Pods son efímeros y nunca se despliegan de forma solitaria. En este módulo implementamos controladores de alto nivel para garantizar alta disponibilidad y actualizaciones continuas sin tiempo de inactividad.
- Conceptos Teóricos:
- De Pods aislados a controladores declarativos.
ReplicaSet: Cómo funciona el bucle de reconciliación para garantizarNréplicas activas.Deployment: Gestión declarativa de versiones y estrategias de actualización.- Estrategias de actualización: Recreate vs. RollingUpdate (
maxSurge,maxUnavailable). - Mecanismo de autorreparación (Self-healing): qué pasa si un contenedor se cae o un nodo muere.
- Reversión instantánea (Rollback) ante despliegues fallidos.
- Laboratorios Prácticos:
- Despliegue de una aplicación con 3 réplicas balanceadas.
- Simulación de fallos: eliminar Pods a mano y observar la regeneración en segundos.
- Actualización en vivo de versión v1 a v2 observando el reemplazo progresivo de Pods.
- Ejecución de
kubectl rollout undopara revertir una versión defectuosa.
- Reto Roto 02:
- Reparación de un Deployment atascado con
CrashLoopBackOffy selectores de etiquetas (matchLabels) inconsistentes.
- Reparación de un Deployment atascado con
🌐 Módulo 03: Conectando Todo: Redes en Kubernetes
Descripción:
Dado que los Pods nacen y mueren recibiendo IPs dinámicas impredecibles, necesitamos una capa de abstracción estable para la comunicación interna y externa.
- Conceptos Teóricos:
- El modelo de red de Kubernetes: "Cada Pod tiene su propia IP y todos los Pods se comunican entre sí sin NAT".
- Tipos de Services:
ClusterIP: IP interna virtual accesible únicamente dentro del clúster (ideal para bases de datos y microservicios internos).NodePort: Abre un puerto estático (rango 30000-32767) en cada nodo del clúster.LoadBalancer: Se integra con proveedores de nube para aprovisionar un balanceador externo.
- Service Discovery y CoreDNS: Resolución de nombres internos (
<servicio>.<namespace>.svc.cluster.local). - Ingress y Controladores de Ingress: Enrutamiento capa 7 (HTTP/HTTPS) basado en rutas (
/api,/web) y nombres de dominio con un solo punto de entrada.
- Laboratorios Prácticos:
- Exponer una aplicación interna con un Service
ClusterIP. - Conectar un Frontend con un Backend usando únicamente el nombre DNS del servicio.
- Configurar Nginx Ingress Controller en Kind y enrutar tráfico HTTP hacia múltiples servicios.
- Exponer una aplicación interna con un Service
- Reto Roto 03:
- Resolver un Service que no envía tráfico a sus Pods debido a un error en
targetPorty selector de etiquetas (labels).
- Resolver un Service que no envía tráfico a sus Pods debido a un error en
💾 Módulo 04: Configuración y Datos Persistentes
Descripción:
Las aplicaciones modernas deben desacoplar su código de su configuración (The Twelve-Factor App) y manejar datos persistentes que sobrevivan al ciclo de vida del contenedor.
- Conceptos Teóricos:
- Configuración desacoplada: Variables de entorno vs. archivos de configuración inyectados.
ConfigMaps: Almacenamiento de configuración en texto plano (puertos, endpoints, temas).Secrets: Almacenamiento seguro de credenciales, tokens y llaves (codificados en Base64 / integrables con KMS).- Almacenamiento efímero (
emptyDir) vs. almacenamiento persistente. - La tríada del almacenamiento en K8s:
PersistentVolume(PV): El disco físico aprovisionado en la infraestructura.PersistentVolumeClaim(PVC): El ticket de solicitud de almacenamiento que hace el Pod.StorageClass: Aprovisionamiento dinámico de discos bajo demanda.
- Laboratorios Prácticos:
- Inyección de variables de entorno y archivos de configuración con
ConfigMap. - Montaje seguro de credenciales con
Secret. - Despliegue de una base de datos con persistencia real usando
PVCy verificación de datos tras reiniciar los Pods.
- Inyección de variables de entorno y archivos de configuración con
- Reto Roto 04:
- Diagnóstico de un Pod con
CreateContainerConfigErrorpor unConfigMapKeyRefinexistente y un PVC que se queda en estadoPending.
- Diagnóstico de un Pod con
🛡️ Módulo 05: Salud, Límites y Seguridad Básica
Descripción:
Aprende a operar aplicaciones en entornos de producción reales con resiliencia, control de recursos y control de acceso.
- Conceptos Teóricos:
- Sondas de salud (Probes):
StartupProbe: Para aplicaciones lentas al arrancar.LivenessProbe: Detecta bloqueos o deadlocks y reinicia el contenedor.ReadinessProbe: Detiene el envío de tráfico a Pods que aún no están listos para responder.
- Tipos de verificación:
httpGet,tcpSocketyexec. - Gestión de recursos:
requests: Recursos mínimos garantizados para el agendamiento del Pod.limits: Techo máximo de CPU (estrangulamiento / throttling) y Memoria (OOMKilled).
- Aislamiento lógico con
Namespaces. - Seguridad básica con
RBAC:ServiceAccount,Role,ClusterRole,RoleBinding.
- Sondas de salud (Probes):
- Laboratorios Prácticos:
- Configuración de sondas
livenessyreadinessen una aplicación web. - Prueba de estrés de memoria para observar cómo actúa el kernel ante límites excedidos.
- Creación de un usuario con permisos de solo lectura (
read-only) en un namespace específico mediante RBAC.
- Configuración de sondas
- Reto Roto 05:
- Reparar un Pod que entra en bucle continuo de reinicios porque su
livenessProbeapunta a una ruta errónea.
- Reparar un Pod que entra en bucle continuo de reinicios porque su
🚀 Módulo 06: Empaquetado con Helm y Proyecto Final
Descripción:
En el mundo profesional no aplicamos decenas de archivos YAML sueltos. Utilizamos Helm, el gestor de paquetes de Kubernetes, para parametrizar, versionar y desplegar soluciones completas.
- Conceptos Teóricos:
- ¿Por qué Helm? El dolor de mantener múltiples YAMLs para desarrollo, staging y producción.
- Anatomía de un Helm Chart:
Chart.yaml: Metadatos del paquete.values.yaml: Valores por defecto configurables.templates/: Plantillas Go template que generan los manifiestos dinámicos.
- Comandos esenciales:
helm create,helm lint,helm install,helm upgrade,helm rollback,helm uninstall.
- Laboratorios Prácticos:
- Creación de un Chart personalizado desde cero.
- Parametrización de réplicas, imágenes, puertos y variables de entorno.
- Despliegue de dependencias externas (ej. Redis o PostgreSQL) usando repositorios de Charts comunitarios.
- Proyecto Final de Graduación:
- Despliegue de una arquitectura de 3 capas completa (Frontend web, API backend y Base de datos con volumen persistente, sondas de salud, Ingress y Helm).
- ¡Graduación oficial y obtención del rango de Capitán K8s en la comunidad! ⚓
🧭 Metodología de los "Retos Rotos"
Cada módulo semanal cuenta con su respectivo reto roto en el repositorio:
- Navega a la carpeta semanal correspondiente en el repositorio oficial.
- Identifica el error aplicando el archivo en tu clúster de laboratorio.
- Investiga con comandos de diagnóstico:
kubectl get,kubectl describe,kubectl logs. - Corrige el archivo en tu rama de trabajo y agrégalo en
aportes-comunidad/soluciones-retos/<tu-usuario>/. - Envía tu Pull Request para que sea revisado por los mentores de Cloud Native Medellín.
Módulo 00: Fundamentos y Conceptos de Docker
🐳 Módulo 00: Fundamentos de Docker — De Código a Contenedor
Workshop De Cero a Capitán en Kubernetes — Cloud Native Medellín 🏔️☕
- Repositorio del Módulo: Fundamentos-Kubernetes / semana-00-fundamentos-docker
- Comunidad: Cloud Native Medellín (CNCF Chapter)
🎯 ¿Qué vas a dominar en esta unidad?
- Qué es realmente un contenedor y en qué se diferencia de una Máquina Virtual pesada.
- Cómo el Kernel de Linux aísla procesos usando Namespaces y limita recursos con Cgroups.
- Cómo escribir un
Dockerfiledesde cero con buenas prácticas (Alpine, usuario no-root,.dockerignore,exec form). - Construir redes bridge personalizadas y entender la resolución DNS interna por nombre entre contenedores.
- Almacenamiento efímero vs. Named Volumes (
docker volume) vs. Bind Mounts (-v). - Auditoría y escaneo de vulnerabilidades con Trivy (
HIGH,CRITICAL). - Ciclo de distribución OCI con Harbor: autenticación (
docker login), etiquetado (docker tag), publicación (docker push) y descarga (docker pull). - Inferencia de modelos de IA locales en CPU con Ollama, límites de recursos y consumo vía API REST.
- El truco fundamental para cargar imágenes locales en clústeres de Kind con
kind load docker-image.
☕ La Analogía del Tinto Colombiano
Para no enredarnos con términos extraños, piensa en cómo funciona un buen tinto en Medellín:
[ Granos de Café ] ➔ [ Receta: Dockerfile ] ➔ [ Bolsa Sellada: Imagen ] ➔ [ Greca Encendida: Contenedor ]
(Tu Código) (Instrucciones) (Plantilla Inmutable) (Proceso en Ejecución)
- Los Granos de café: Es tu código fuente (
app.py,index.html, etc.). La materia prima sin procesar. - El
Dockerfile(La Receta): Indica cómo tostar, moler y preparar el café paso a paso. - La Imagen de Docker (La Bolsa de café sellada): La bolsa de café molido sellada al vacío que compras en la tienda. Es inmutable: la construyes una vez y la puedes llevar a Medellín, Bogotá o Tokio; sabe exactamente igual en todas partes.
- El Contenedor (La Greca preparando el tinto): La greca encendida pasando agua caliente por el café molido. La aplicación viva y corriendo, lista para servir peticiones.
🧠 Máquinas Virtuales vs. Contenedores
- Máquina Virtual (VM): Incluye un sistema operativo completo (Guest OS) con su propio kernel encima de un hipervisor (VirtualBox, VMware). Pesa gigabytes y tarda minutos en arrancar.
- Contenedor: No duplica el sistema operativo. Comparte el Kernel de Linux del anfitrión y aísla los procesos con:
- Namespaces: Dan la ilusión de tener red propia, tabla de procesos propia (PID 1) y sistema de archivos propio.
- Cgroups (Control Groups): Establecen techos de consumo de CPU y memoria RAM para proteger al host.
🛠️ Comandos Esenciales de Docker
| Acción | Comando | ¿Qué hace? |
|---|---|---|
| Construir imagen | docker build -t mi-app:v1.0 . | Lee el Dockerfile en el directorio actual y crea la imagen con etiqueta mi-app:v1.0. |
| Listar imágenes | docker images | Muestra las imágenes almacenadas localmente. |
| Correr contenedor | docker run -d -p 8080:80 --name mi-servidor mi-app:v1.0 | -d: segundo plano, -p: mapeo de puerto (host:contenedor), --name: nombre amigable. |
| Ver contenedores activos | docker ps | Muestra qué contenedores están corriendo actualmente. |
| Ver todos (incluidos caídos) | docker ps -a | Permite revisar contenedores detenidos o con error. |
| Ver logs en vivo | docker logs -f mi-servidor | Sigue en tiempo real la salida estándar y errores de la aplicación. |
| Entrar a la terminal | docker exec -it mi-servidor sh | Abre una sesión interactiva dentro del contenedor. |
| Detener contenedor | docker stop mi-servidor | Envía una señal limpia SIGTERM para apagarlo. |
| Eliminar contenedor | docker rm mi-servidor | Borra el contenedor detenido del disco. |
| Crear red bridge | docker network create mi-red | Crea una red privada con resolución DNS automática por nombre. |
| Crear volumen | docker volume create mi-volumen | Crea un volumen administrado que persiste datos fuera del ciclo de vida del contenedor. |
| Etiquetar imagen | docker tag mi-app:v1.0 mi-registro.io/equipo/mi-app:v1.0 | Asigna un nuevo tag con la URL del registro de destino. |
| Iniciar sesión en registro | docker login mi-registro.io | Autentica contra un registro OCI como Harbor o Docker Hub. |
| Publicar imagen | docker push mi-registro.io/equipo/mi-app:v1.0 | Sube la imagen empaquetada al registro remoto. |
| Descargar imagen | docker pull mi-registro.io/equipo/mi-app:v1.0 | Descarga la imagen remota a tu máquina o nodo. |
| Escanear seguridad | trivy image --severity HIGH,CRITICAL mi-app:v1.0 | Analiza vulnerabilidades conocidas (CVEs) en la imagen. |
🚀 Preparación del Entorno: Clonar el Repositorio
Para ejecutar las prácticas de este módulo con total seguridad, evitar errores de contexto (context not found) o rutas relativas incorrectas, clona el repositorio oficial en tu terminal y sitúate en la raíz del proyecto:
# 1. Clonar el repositorio oficial del workshop
git clone https://github.com/edwinbermudez91/Fundamentos-Kubernetes.git
# 2. Entrar a la raíz del repositorio
cd Fundamentos-Kubernetes
!IMPORTANT 📁 Regla de Oro: Rutas Absolutas con
$PWD:
A lo largo de toda esta unidad, todos los comandos utilizan la variable de entorno$PWD(ej.$PWD/semana-00-fundamentos-docker/...) para construir la ruta absoluta exacta en tu sistema o en la terminal web de Labs.iximiuz.
Esto garantiza que puedes ejecutar cualquier comando desde la raíz del repositorio sin necesidad de cambiar de carpeta (cd) entre laboratorios, eliminando por completo los errores de archivos no encontrados.
🧭 Laboratorios Prácticos de este Módulo
Completa los laboratorios prácticos en las siguientes unidades del curso:
| Unidad | Laboratorio | Descripción |
|---|---|---|
| Lab 01 | Servidor Web Estático con Nginx | Nginx Alpine (~25 MB), proceso en primer plano y HTML. |
| Lab 02 | API en Python con Variables de Entorno | Usuario no-root (appuser), ENV, .dockerignore y UI interactiva. |
| Lab 03 | Redes en Docker y Microservicios | Red bridge custom (red-cafetera), DNS interno y comunicación Backend-Frontend. |
| Lab 04 | Almacenamiento y Volúmenes | Datos efímeros vs Named Volumes y Bind Mounts con persistencia real. |
| Lab 05 | Registro y Publicación con Harbor | Autenticación con Cuentas Robot, docker tag, push y auditoría Trivy. |
| Lab 06 | IA Local en Docker con Ollama | Inferencia de LLM (Qwen2.5) en CPU con límites cgroups y Modelfile DevOps. |
| Retos Rotos | Los 5 Retos Rotos de la Semana 00 | Depuración de fallos reales típicos de contenedores y puente a Kind. |
Módulo 00 — Lab 1: Servidor Web Nginx
🧪 Laboratorio 1: Servidor Web Estático con Nginx
- Repositorio del Taller: Fundamentos-Kubernetes / semana-00-fundamentos-docker
- Comunidad: Cloud Native Medellín (CNCF Chapter)
Aprende a empaquetar una página web estática dentro de un contenedor Nginx ultra ligero. Los archivos de este laboratorio se encuentran en:
📂 semana-00-fundamentos-docker/1.ejemplos/01-servidor-web-html/
1. Archivo HTML (index.html):
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mi Primer Contenedor Docker — Cloud Native Medellín</title>
<style>
:root {
--bg: #0d1117;
--card-bg: #161b22;
--border: #30363d;
--text: #c9d1d9;
--heading: #58a6ff;
--accent: #00c7b7;
--code-bg: #21262d;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg);
color: var(--text);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 1.5rem;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 12px;
max-width: 600px;
width: 100%;
padding: 2.5rem;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4);
text-align: center;
}
.badge {
display: inline-block;
background: rgba(0, 199, 183, 0.15);
color: var(--accent);
border: 1px solid var(--accent);
padding: 0.35rem 0.85rem;
border-radius: 9999px;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 1.25rem;
}
h1 {
color: var(--heading);
font-size: 1.85rem;
margin-bottom: 0.75rem;
}
p {
line-height: 1.6;
margin-bottom: 1.5rem;
}
.steps {
text-align: left;
background: var(--code-bg);
border-radius: 8px;
padding: 1.25rem 1.5rem;
margin-bottom: 1.5rem;
font-size: 0.95rem;
}
.steps ol {
padding-left: 1.2rem;
}
.steps li {
margin-bottom: 0.5rem;
}
.footer {
font-size: 0.85rem;
color: #8b949e;
border-top: 1px solid var(--border);
padding-top: 1.25rem;
}
</style>
</head>
<body>
<div class="card">
<span class="badge">🐳 Docker + ☸️ Kubernetes</span>
<h1>¡Hola desde tu Contenedor!</h1>
<p>
Si estás viendo esta página web servida por <strong>Nginx</strong>, significa que tu imagen de Docker fue empaquetada correctamente y está corriendo con éxito.
</p>
<div class="steps">
<strong>¿Qué acaba de ocurrir por debajo?</strong>
<ol>
<li>Docker leyó el <code>Dockerfile</code>.</li>
<li>Descargó la capa base oficial de <code>nginx:alpine</code>.</li>
<li>Copió este archivo <code>index.html</code> dentro de <code>/usr/share/nginx/html/</code>.</li>
<li>Expuso el puerto 80 y dejó el proceso escuchando en primer plano.</li>
</ol>
</div>
<div class="footer">
🏔️ Comunidad Cloud Native Medellín • De Cero a Capitán en Kubernetes ☕
</div>
</div>
</body>
</html>
2. Archivo Dockerfile:
# Usamos una imagen base oficial ultra ligera basada en Alpine Linux
FROM nginx:alpine
# Copiamos nuestro HTML al directorio público por defecto de Nginx
COPY index.html /usr/share/nginx/html/index.html
# Indicamos que el contenedor escucha en el puerto 80
EXPOSE 80
# Nginx arranca automáticamente en primer plano
CMD ["nginx", "-g", "daemon off;"]
3. Construye y corre el contenedor con ruta absoluta:
# 1. Construir la imagen referenciando la ruta absoluta del laboratorio
docker build -t mi-web-estatica:v1.0 $PWD/semana-00-fundamentos-docker/1.ejemplos/01-servidor-web-html
# 2. Correr el contenedor en segundo plano mapeando el puerto 8080:80
docker run -d -p 8080:80 --name mi-servidor-web mi-web-estatica:v1.0
# 3. Probar en terminal o abrir http://localhost:8080
curl -I http://localhost:8080
# 4. Inspeccionar los logs en vivo
docker logs -f mi-servidor-web
# 5. Entrar al contenedor con docker exec
docker exec -it mi-servidor-web sh
# 6. Auditar la imagen y la configuración con Trivy usando ruta absoluta
trivy image --severity HIGH,CRITICAL mi-web-estatica:v1.0
trivy config $PWD/semana-00-fundamentos-docker/1.ejemplos/01-servidor-web-html
# 7. Limpieza
docker rm -f mi-servidor-web
!TIP 🛡️ Requisito para el paso #6 (Instalación de Trivy):
Para instalar la herramienta Trivy en tu máquina (Homebrew en macOS, Linux, WSL) o correrla directamente sin instalar nada utilizando el contenedor efímero oficial (docker run aquasec/trivy), consulta la Guía Completa de Seguridad y Escaneo de Contenedores con Trivy.
Módulo 00 — Lab 2: API REST en Python
🧪 Laboratorio 2: API en Python con Variables de Entorno
- Repositorio del Taller: Fundamentos-Kubernetes / semana-00-fundamentos-docker
- Comunidad: Cloud Native Medellín (CNCF Chapter)
Aprende a crear y empaquetar un microservicio en Python parametrizado mediante variables de entorno que ofrece una experiencia interactiva dual:
- 🌐 En el Navegador: Interfaz web visual y animada con la taza de tinto paisa humeante, sonido con Web Audio API, simulador de variables de entorno y telemetría de Kubernetes.
- 📡 En Terminal o Kubernetes: Respuesta limpia en JSON para
curl, scripts y probes de salud (/health).
Los archivos de este laboratorio se encuentran en:
📂 $PWD/semana-00-fundamentos-docker/1.ejemplos/02-app-python-api/
1. Código de la Aplicación (app.py):
El microservicio utiliza únicamente la librería estándar de Python (http.server, sin frameworks pesados ni dependencias externas), lo que permite crear imágenes mínimas y ultra seguras.
"""
Servidor HTTP en Python sin dependencias externas (Standard Library).
Ideal para entender cómo empaquetar una aplicación con variables de entorno
y probarla primero en Docker y luego en Kubernetes.
Características:
- 🌐 Interfaz Web interactiva, animada y moderna para navegadores con la taza de tinto paisa humeante.
- 📡 Endpoint REST JSON para llamadas con curl, scripts y Kubernetes.
- 🩺 Endpoint de salud /health para probes de Kubernetes (Liveness y Readiness).
- 🛡️ Inspección de seguridad en tiempo de ejecución (UID / usuario no-root).
"""
import http.server
import json
import os
import socket
import sys
import time
import urllib.parse
# 1. Variables de entorno configurables
PORT = int(os.environ.get("PORT", 8080))
MENSAJE_DEFAULT = os.environ.get("MENSAJE", "¡Tinto recién colado! ☕")
COMUNIDAD = "Cloud Native Medellín 🏔️☕"
WORKSHOP = "De Cero a Capitán en Kubernetes"
START_TIME = time.time()
TINTOS_TOTALES = 1
# 2. Plantilla HTML interactiva con diseño moderno, animaciones CSS y Web Audio API
HTML_PAGE = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>☕ Cloud Native Medellín — Tinto Recién Colado</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0e17;
--card-bg: rgba(18, 24, 38, 0.88);
--card-border: rgba(245, 158, 11, 0.25);
--coffee-gold: #f59e0b;
--coffee-warm: #d97706;
--coffee-dark: #78350f;
--accent-cyan: #06b6d4;
--accent-green: #10b981;
--accent-red: #ef4444;
--text-main: #f9fafb;
--text-muted: #9ca3af;
--code-bg: #030712;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: radial-gradient(circle at 50% 15%, rgba(217, 119, 6, 0.18), transparent 45%),
radial-gradient(circle at 85% 85%, rgba(6, 182, 212, 0.1), transparent 40%),
var(--bg);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
overflow-x: hidden;
}
.container {
width: 100%;
max-width: 840px;
background: var(--card-bg);
border: 1px solid var(--card-border);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-radius: 28px;
padding: 2.8rem 2.2rem;
box-shadow: 0 30px 70px -15px rgba(0, 0, 0, 0.75),
0 0 40px -5px rgba(245, 158, 11, 0.18);
position: relative;
animation: fadeIn 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(24px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* Badges Bar */
.badge-bar {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
justify-content: center;
margin-bottom: 1.5rem;
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.4rem 0.95rem;
border-radius: 9999px;
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.3px;
border: 1px solid transparent;
transition: all 0.25s ease;
}
.badge-coffee {
background: rgba(245, 158, 11, 0.15);
color: var(--coffee-gold);
border-color: rgba(245, 158, 11, 0.4);
}
.badge-cyan {
background: rgba(6, 182, 212, 0.15);
color: var(--accent-cyan);
border-color: rgba(6, 182, 212, 0.4);
}
.badge-green {
background: rgba(16, 185, 129, 0.15);
color: var(--accent-green);
border-color: rgba(16, 185, 129, 0.4);
}
.pulse-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-green);
box-shadow: 0 0 10px var(--accent-green);
animation: pulse 1.8s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.75); }
}
/* Header */
header {
text-align: center;
margin-bottom: 2rem;
}
h1 {
font-size: 2.3rem;
font-weight: 800;
line-height: 1.2;
background: linear-gradient(135deg, #ffffff 40%, var(--coffee-gold) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.05rem;
color: var(--text-muted);
}
/* Coffee Animation Showcase */
.coffee-showcase {
display: flex;
flex-direction: column;
align-items: center;
margin: 1.8rem 0;
position: relative;
}
.cup-wrapper {
position: relative;
width: 140px;
height: 125px;
display: flex;
justify-content: center;
align-items: flex-end;
cursor: pointer;
}
/* Steam animation */
.steam-container {
position: absolute;
top: -10px;
display: flex;
gap: 16px;
z-index: 10;
}
.steam-wisp {
width: 7px;
height: 52px;
background: linear-gradient(to top, rgba(255, 255, 255, 0.8), rgba(245, 158, 11, 0.35), transparent);
border-radius: 50% 50% 20% 20%;
filter: blur(3px);
animation: steamRise 2.5s infinite ease-out;
opacity: 0;
}
.steam-wisp:nth-child(1) { animation-delay: 0s; }
.steam-wisp:nth-child(2) { animation-delay: 0.8s; height: 60px; }
.steam-wisp:nth-child(3) { animation-delay: 1.6s; }
@keyframes steamRise {
0% {
transform: translateY(20px) scaleX(0.7) skewX(0deg);
opacity: 0;
}
30% {
opacity: 0.85;
}
70% {
transform: translateY(-28px) scaleX(1.5) skewX(10deg);
opacity: 0.35;
}
100% {
transform: translateY(-55px) scaleX(2.2) skewX(-12deg);
opacity: 0;
}
}
/* Coffee Cup CSS */
.coffee-cup {
position: relative;
width: 112px;
height: 74px;
background: linear-gradient(135deg, #f9fafb 0%, #e5e7eb 65%, #9ca3af 100%);
border-radius: 0 0 46px 46px;
box-shadow: inset 0 -6px 12px rgba(0, 0, 0, 0.25),
0 10px 24px rgba(0, 0, 0, 0.45);
z-index: 2;
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.coffee-cup:hover {
transform: scale(1.04);
}
.cup-handle {
position: absolute;
right: -24px;
top: 10px;
width: 32px;
height: 42px;
border: 8px solid #f3f4f6;
border-left: none;
border-radius: 0 22px 22px 0;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.25);
}
.cup-saucer {
position: absolute;
bottom: -7px;
width: 142px;
height: 14px;
background: linear-gradient(to bottom, #f3f4f6, #9ca3af);
border-radius: 50%;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.55);
z-index: 1;
}
.cup-liquid {
position: absolute;
top: 5px;
left: 10px;
right: 10px;
height: 20px;
background: radial-gradient(ellipse at center, #78350f 0%, #451a03 70%, #1f0d05 100%);
border-radius: 50%;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.9);
overflow: hidden;
}
.cup-crema {
position: absolute;
top: 3px;
left: 20%;
width: 32px;
height: 11px;
background: rgba(245, 158, 11, 0.4);
border-radius: 50%;
filter: blur(2px);
animation: cremaDrift 4s ease-in-out infinite alternate;
}
@keyframes cremaDrift {
0% { transform: scale(1) translateX(0); }
100% { transform: scale(1.2) translateX(14px); }
}
/* Message Callout */
.message-hero {
margin-top: 1.8rem;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(245, 158, 11, 0.35);
padding: 1.25rem 2rem;
border-radius: 18px;
text-align: center;
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.5);
max-width: 90%;
width: 100%;
}
.message-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--coffee-gold);
font-weight: 700;
margin-bottom: 0.45rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
}
.message-text {
font-size: 1.55rem;
font-weight: 700;
color: #fff;
text-shadow: 0 0 24px rgba(245, 158, 11, 0.55);
transition: all 0.3s ease;
}
/* Counter Badge */
.counter-pill {
font-size: 0.88rem;
color: var(--coffee-gold);
background: rgba(245, 158, 11, 0.12);
border: 1px dashed rgba(245, 158, 11, 0.35);
padding: 0.45rem 1.2rem;
border-radius: 9999px;
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.8rem;
}
/* Action Buttons */
.actions-row {
display: flex;
flex-wrap: wrap;
gap: 0.9rem;
justify-content: center;
margin: 1.8rem 0;
}
.btn {
appearance: none;
border: none;
cursor: pointer;
font-family: inherit;
font-size: 0.95rem;
font-weight: 600;
padding: 0.85rem 1.6rem;
border-radius: 14px;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.btn-primary {
background: linear-gradient(135deg, var(--coffee-gold), var(--coffee-warm));
color: #0b0f19;
box-shadow: 0 4px 18px rgba(245, 158, 11, 0.45);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(245, 158, 11, 0.65);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.07);
color: var(--text-main);
border: 1px solid rgba(255, 255, 255, 0.14);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.14);
transform: translateY(-2px);
}
/* Presets Section */
.presets-section {
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 18px;
padding: 1.35rem;
margin: 1.6rem 0;
}
.presets-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--coffee-gold);
margin-bottom: 0.85rem;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
}
.preset-chips {
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
}
.chip {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-main);
padding: 0.45rem 0.9rem;
border-radius: 10px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.chip:hover {
background: rgba(245, 158, 11, 0.18);
border-color: var(--coffee-gold);
transform: translateY(-1px);
}
/* Telemetry Grid */
.telemetry-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
margin: 1.8rem 0;
}
.telemetry-card {
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 16px;
padding: 1.1rem 1.25rem;
transition: all 0.25s ease;
}
.telemetry-card:hover {
border-color: rgba(245, 158, 11, 0.35);
transform: translateY(-2px);
}
.telemetry-title {
font-size: 0.74rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.8px;
margin-bottom: 0.4rem;
display: flex;
align-items: center;
gap: 0.35rem;
}
.telemetry-value {
font-family: 'JetBrains Mono', monospace;
font-size: 1.05rem;
font-weight: 600;
color: #fff;
word-break: break-all;
}
.safe-badge {
color: var(--accent-green);
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.9rem;
}
.danger-badge {
color: var(--accent-red);
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.9rem;
}
/* JSON Box */
.json-box {
margin-top: 1.6rem;
background: var(--code-bg);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
overflow: hidden;
}
.json-header {
background: rgba(255, 255, 255, 0.035);
padding: 0.7rem 1.2rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.json-header span {
font-size: 0.82rem;
font-family: 'JetBrains Mono', monospace;
color: var(--text-muted);
}
.btn-copy {
background: transparent;
border: none;
color: var(--coffee-gold);
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.65rem;
border-radius: 8px;
transition: background 0.2s;
}
.btn-copy:hover {
background: rgba(245, 158, 11, 0.15);
}
pre {
padding: 1.25rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.86rem;
line-height: 1.65;
overflow-x: auto;
}
pre .json-key { color: #f472b6; }
pre .json-str { color: #34d399; }
pre .json-num { color: #f59e0b; }
pre .json-bool { color: #a78bfa; }
footer {
text-align: center;
margin-top: 2rem;
font-size: 0.88rem;
color: var(--text-muted);
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-top: 1.35rem;
}
/* Animation when pouring coffee */
@keyframes coffeePour {
0% { transform: scale(0.95); opacity: 0.8; }
50% { transform: scale(1.08); opacity: 1; filter: drop-shadow(0 0 20px rgba(245, 158, 11, 0.85)); }
100% { transform: scale(1); opacity: 1; }
}
.pouring {
animation: coffeePour 0.65s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
</style>
</head>
<body>
<div class="container">
<div class="badge-bar">
<span class="badge badge-coffee">☕ Semana 00 • Docker</span>
<span class="badge badge-cyan">🏔️ Cloud Native Medellín</span>
<span class="badge badge-green"><span class="pulse-dot"></span> Pod Activo</span>
</div>
<header>
<h1>¡Tinto Paisa Containerizado!</h1>
<p class="subtitle">Workshop: De Cero a Capitán en Kubernetes</p>
</header>
<div class="coffee-showcase">
<div class="cup-wrapper" id="cupWrapper" onclick="colarTinto()" title="Haz clic para colar un tinto">
<div class="steam-container">
<div class="steam-wisp"></div>
<div class="steam-wisp"></div>
<div class="steam-wisp"></div>
</div>
<div class="cup-saucer"></div>
<div class="coffee-cup" id="coffeeCup">
<div class="cup-handle"></div>
<div class="cup-liquid">
<div class="cup-crema"></div>
</div>
</div>
</div>
<div class="message-hero">
<div class="message-label">☕ Variable de Entorno [ENV MENSAJE]</div>
<div class="message-text" id="mensajeDisplay">__MENSAJE__</div>
</div>
<div class="counter-pill">
<span>Tintos servidos por esta greca:</span>
<strong id="tintosCounter" style="font-size: 1.15rem; color: #fff;">__TINTOS__</strong>
</div>
</div>
<div class="actions-row">
<button class="btn btn-primary" id="btnColar" onclick="colarTinto()">
☕ ¡Colar otro Tinto!
</button>
<button class="btn btn-secondary" onclick="probarHealth()">
🩺 Probar /health
</button>
<button class="btn btn-secondary" onclick="verApiJson()">
📡 Ver Endpoint /api
</button>
</div>
<!-- Presets -->
<div class="presets-section">
<div class="presets-title">
<span>🎛️ Simular Variación de Variable de Entorno:</span>
<span style="font-size: 0.78rem; color: var(--text-muted); font-weight: 400;">Pruébalo en vivo</span>
</div>
<div class="preset-chips">
<button class="chip" onclick="cambiarMensaje('¡Tinto recién colado! ☕')">☕ ¡Tinto recién colado! ☕</button>
<button class="chip" onclick="cambiarMensaje('¡Tinto campesino con panela y canela! 🍯')">🍯 Tinto campesino con panela</button>
<button class="chip" onclick="cambiarMensaje('¡Perico bien cargado pal frío! 🥛')">🥛 Perico bien caliente</button>
<button class="chip" onclick="cambiarMensaje('¡Tinto caliente en el Metro San Antonio! 🚊')">🚊 Estación San Antonio</button>
<button class="chip" onclick="cambiarMensaje('¡De Cero a Capitán en Kubernetes! ☸️🚀')">☸️ De Cero a Capitán</button>
</div>
</div>
<!-- Telemetry -->
<div class="telemetry-grid">
<div class="telemetry-card">
<div class="telemetry-title">📦 Hostname / Pod</div>
<div class="telemetry-value">__HOSTNAME__</div>
</div>
<div class="telemetry-card">
<div class="telemetry-title">🔌 Puerto Activo</div>
<div class="telemetry-value">:__PUERTO__</div>
</div>
<div class="telemetry-card">
<div class="telemetry-title">🛡️ Proceso Contenedor</div>
<div class="telemetry-value" style="font-size: 0.95rem;">
__USUARIO_BADGE__
</div>
</div>
<div class="telemetry-card">
<div class="telemetry-title">⏱️ Uptime Contenedor</div>
<div class="telemetry-value" id="uptimeVal">0s</div>
</div>
</div>
<!-- Live JSON Box -->
<div class="json-box">
<div class="json-header">
<span>Respuesta Cruda de la API (JSON)</span>
<button class="btn-copy" onclick="copiarJson()">
<span id="copyIcon">📋</span> <span id="copyText">Copiar JSON</span>
</button>
</div>
<pre><code id="jsonCode"></code></pre>
</div>
<footer>
🏔️ <strong>Cloud Native Medellín</strong> • De Cero a Capitán en Kubernetes • Semana 00: Fundamentos de Docker ☕
</footer>
</div>
<script>
let tintosCount = __TINTOS__;
let startTime = Date.now();
let audioCtx = null;
let payload = {
mensaje: "__MENSAJE__",
comunidad: "Cloud Native Medellín 🏔️☕",
workshop: "De Cero a Capitán en Kubernetes",
hostname_o_pod: "__HOSTNAME__",
puerto: __PUERTO__,
usuario_proceso: "__USUARIO__",
es_root: __ES_ROOT__,
tintos_servidos: __TINTOS__,
orquestador: "Listo para correr en Kubernetes"
};
function renderJson() {
const codeEl = document.getElementById("jsonCode");
const jsonStr = JSON.stringify(payload, null, 2);
codeEl.innerHTML = jsonStr
.replace(/"(.*?)":/g, '<span class="json-key">"$1"</span>:')
.replace(/:\s*"(.*?)"/g, ': <span class="json-str">"$1"</span>')
.replace(/:\s*([0-9]+)/g, ': <span class="json-num">$1</span>')
.replace(/:\s*(true|false)/g, ': <span class="json-bool">$1</span>');
}
function playCoffeeSound() {
try {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(587.33, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(880, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.35);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.35);
} catch (e) {}
}
function colarTinto() {
tintosCount++;
document.getElementById("tintosCounter").innerText = tintosCount;
payload.tintos_servidos = tintosCount;
renderJson();
const cup = document.getElementById("coffeeCup");
cup.classList.remove("pouring");
void cup.offsetWidth;
cup.classList.add("pouring");
playCoffeeSound();
const btn = document.getElementById("btnColar");
const originalText = btn.innerText;
btn.innerText = "✨ ¡Tinto Servido! ☕";
setTimeout(() => { btn.innerText = originalText; }, 700);
}
function cambiarMensaje(nuevo) {
payload.mensaje = nuevo;
document.getElementById("mensajeDisplay").innerText = nuevo;
renderJson();
colarTinto();
}
function copiarJson() {
navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => {
const copyText = document.getElementById("copyText");
copyText.innerText = "¡Copiado!";
setTimeout(() => { copyText.innerText = "Copiar JSON"; }, 2000);
});
}
function probarHealth() {
window.open('/health', '_blank');
}
function verApiJson() {
window.open('/api', '_blank');
}
// Cronómetro de Uptime en vivo
setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const m = Math.floor(elapsed / 60);
const s = elapsed % 60;
document.getElementById("uptimeVal").innerText = m > 0 ? `${m}m ${s}s` : `${s}s`;
}, 1000);
renderJson();
</script>
</body>
</html>
"""
class SimpleHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
global TINTOS_TOTALES
# 1. Endpoint de salud para Kubernetes (Liveness & Readiness probe)
if self.path == "/health":
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
uptime_seg = int(time.time() - START_TIME)
self.wfile.write(json.dumps({
"status": "ok",
"saludable": True,
"uptime_segundos": uptime_seg
}, indent=2).encode("utf-8"))
return
# 2. Información del proceso y usuario para verificar buenas prácticas
usuario = "desconocido"
uid = -1
try:
import pwd
uid = os.getuid()
usuario = pwd.getpwuid(uid).pw_name
except Exception:
usuario = os.environ.get("USER", "appuser")
es_root = (uid == 0)
hostname = socket.gethostname()
# Objeto de respuesta estándar
respuesta = {
"mensaje": MENSAJE_DEFAULT,
"comunidad": COMUNIDAD,
"workshop": WORKSHOP,
"hostname_o_pod": hostname,
"puerto": PORT,
"usuario_proceso": f"{usuario} (UID: {uid})",
"es_root": es_root,
"tintos_servidos": TINTOS_TOTALES,
"orquestador": "Listo para correr en Kubernetes"
}
# 3. Detección de cliente: Navegador Web vs. curl / API
accept = self.headers.get("Accept", "")
user_agent = self.headers.get("User-Agent", "")
url_parsed = urllib.parse.urlparse(self.path)
# Si el usuario explícitamente pide /api o ?format=json o viene de curl/wget sin pedir html:
quiere_json = (
url_parsed.path == "/api"
or "format=json" in url_parsed.query
or ("application/json" in accept and "text/html" not in accept)
or ("curl" in user_agent.lower() and "text/html" not in accept)
)
if not quiere_json:
# Respuesta interactiva y visual para navegadores
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
if es_root:
badge_usuario = '<span class="danger-badge">⚠️ root (UID 0) — Inseguro</span>'
else:
badge_usuario = f'<span class="safe-badge">🛡️ {usuario} (UID {uid}) — Seguro</span>'
html_renderizado = HTML_PAGE
html_renderizado = html_renderizado.replace("__MENSAJE__", MENSAJE_DEFAULT)
html_renderizado = html_renderizado.replace("__HOSTNAME__", hostname)
html_renderizado = html_renderizado.replace("__PUERTO__", str(PORT))
html_renderizado = html_renderizado.replace("__USUARIO__", f"{usuario} (UID: {uid})")
html_renderizado = html_renderizado.replace("__USUARIO_BADGE__", badge_usuario)
html_renderizado = html_renderizado.replace("__ES_ROOT__", "true" if es_root else "false")
html_renderizado = html_renderizado.replace("__TINTOS__", str(TINTOS_TOTALES))
self.wfile.write(html_renderizado.encode("utf-8"))
else:
# Respuesta en formato JSON para terminal, curl y APIs
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(json.dumps(respuesta, indent=2, ensure_ascii=False).encode("utf-8"))
def log_message(self, formato, *args):
# Logs formateados para docker logs y kubectl logs
sys.stdout.write(f"[{self.log_date_time_string()}] {formato % args}\n")
sys.stdout.flush()
if __name__ == "__main__":
servidor = http.server.HTTPServer(("0.0.0.0", PORT), SimpleHandler)
try:
import pwd
u_actual = pwd.getpwuid(os.getuid()).pw_name
uid_actual = os.getuid()
except Exception:
u_actual, uid_actual = "appuser", 10001
print(f"🚀 Servidor escuchando en http://0.0.0.0:{PORT} como '{u_actual}' (UID: {uid_actual})...")
print(f"☕ Mensaje configurado: '{MENSAJE_DEFAULT}'")
try:
servidor.serve_forever()
except KeyboardInterrupt:
print("\n🛑 Servidor detenido limpiamente.")
servidor.server_close()
!TIP Puedes revisar el código completo con la interfaz gráfica, animaciones y audio en
semana-00-fundamentos-docker/1.ejemplos/02-app-python-api/app.py.
2. Archivo Dockerfile (Buenas Prácticas de Seguridad):
# 1. Usamos una imagen base oficial con versión fijada (pinned tag) sobre Alpine Linux (~50MB)
FROM python:3.12-alpine
# 2. 🛡️ Buenas Prácticas de Seguridad: Crear usuario y grupo sin privilegios (Least Privilege)
RUN addgroup -g 10001 -S appgroup && \
adduser -u 10001 -S appuser -G appgroup
# 3. Carpeta de trabajo dentro del contenedor
WORKDIR /app
# 4. Copiamos el código asignando la propiedad directamente al usuario no-root (--chown)
COPY --chown=appuser:appgroup app.py .
# 5. Definimos variables de entorno por defecto
ENV PORT=8080
ENV MENSAJE="¡Hola desde mi contenedor en Cloud Native Medellín! 🏔️☕"
# 6. 🔒 Cambiamos al usuario sin privilegios antes del comando de inicio
USER appuser
# 7. Documentamos el puerto que escucha la aplicación
EXPOSE 8080
# 8. 🩺 Comprobación de salud (Health Check) para monitorear el estado del contenedor
# Mitiga la desconfiguración de seguridad/calidad al permitir que Docker detecte si la app responde
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q -O /dev/null http://localhost:8080/health || exit 1
# 9. Ejecutamos la aplicación en primer plano (PID 1) usando la forma exec (corchetes)
# Esto asegura que reciba señales del sistema operativo (SIGTERM) para apagado limpio
CMD ["python", "app.py"]
3. Archivo .dockerignore:
Evita que se empaqueten archivos innecesarios o secretos dentro de las capas de la imagen:
.env
*.log
.git/
__pycache__/
*.pyc
4. Construir y Probar el Contenedor con Rutas Absolutas:
# 1. Construir la imagen referenciando la ruta absoluta del microservicio
docker build -t mi-api-cnm:v1.0 $PWD/semana-00-fundamentos-docker/1.ejemplos/02-app-python-api
# 2. Correr el contenedor en puerto 8080
docker run -d -p 8080:8080 --name test-docker mi-api-cnm:v1.0
# 3. Probar en terminal (Respuesta JSON) o abrir http://localhost:8080 en el navegador
curl http://localhost:8080
# 4. Probar probe de salud de Kubernetes
curl http://localhost:8080/health
# 5. Comprobar que corre como usuario no-root (Seguridad)
docker exec -it test-docker whoami
# Salida: appuser (UID: 10001)
# 6. Sobrescribir variable de entorno en tiempo de ejecución con -e
docker run -d -p 8081:8080 -e MENSAJE="¡Saludos desde el Poblado en Medellín! ☕" --name test-docker-env mi-api-cnm:v1.0
curl http://localhost:8081
# 7. Auditar vulnerabilidades y configuración del Dockerfile con Trivy (ruta absoluta)
trivy image --severity HIGH,CRITICAL mi-api-cnm:v1.0
trivy config $PWD/semana-00-fundamentos-docker/1.ejemplos/02-app-python-api
# 8. Limpieza
docker rm -f test-docker test-docker-env
Módulo 00 — Lab 3: Redes en Docker
🧪 Laboratorio 3: Redes en Docker y Comunicación entre Microservicios
- Repositorio del Taller: Fundamentos-Kubernetes / semana-00-fundamentos-docker
- Comunidad: Cloud Native Medellín (CNCF Chapter)
¿Cómo se comunican dos contenedores entre sí? Es muy común intentar conectar el Contenedor A con el Contenedor B usando http://localhost:8080, pero esto no funciona debido al aislamiento de red de cada contenedor.
☕ La Analogía Paisa: El Walkie-Talkie del Metro
Cada contenedor vive en su propio mundo aislado (Network Namespace): su propio localhost (127.0.0.1) solo se refiere a sí mismo.
Para que el carrito del Metro (carrito-san-antonio) le pida café a la greca central del sótano (greca-central), ambos deben sintonizar la misma frecuencia de radio (Docker Custom Bridge Network) y llamarse por su nombre:
Arquitectura de los Microservicios:
- Backend (
greca-backend:v1.0): Microservicio interno en puerto8080que mantiene el inventario de tintos. No necesita exponer puertos al host (-p). - Cliente (
carrito-cliente:v1.0): Microservicio en puerto8081que recibe peticiones externas y consulta agreca-central:8080utilizando el DNS interno de Docker.
Paso a paso práctico con Rutas Absolutas:
# 1. Construir las imágenes de ambos microservicios usando rutas absolutas
docker build -t greca-backend:v1.0 $PWD/semana-00-fundamentos-docker/1.ejemplos/03-redes-comunicacion-contenedores/backend-api
docker build -t carrito-cliente:v1.0 $PWD/semana-00-fundamentos-docker/1.ejemplos/03-redes-comunicacion-contenedores/cliente-app
# 2. Crear una red bridge personalizada (aislamiento y DNS automático por nombre)
docker network create red-cafetera
# 3. Iniciar el Backend (Greca Central) dentro de la red (puerto interno 8080)
docker run -d \
--name greca-central \
--network red-cafetera \
greca-backend:v1.0
# 4. Iniciar el Cliente (Carrito San Antonio) en la misma red conectando con el backend por nombre
docker run -d \
--name carrito-san-antonio \
--network red-cafetera \
-p 8081:8081 \
-e BACKEND_HOST="greca-central" \
carrito-cliente:v1.0
# 5. Probar la comunicación (Navegador en http://localhost:8081 o con curl)
curl http://localhost:8081
# 6. Inspeccionar la red para ver las IPs privadas asignadas a cada contenedor
docker network inspect red-cafetera
# 7. Escaneo de seguridad y configuración con Trivy usando rutas absolutas
trivy image --severity HIGH,CRITICAL greca-backend:v1.0
trivy image --severity HIGH,CRITICAL carrito-cliente:v1.0
trivy config $PWD/semana-00-fundamentos-docker/1.ejemplos/03-redes-comunicacion-contenedores/backend-api
trivy config $PWD/semana-00-fundamentos-docker/1.ejemplos/03-redes-comunicacion-contenedores/cliente-app
# 8. Limpieza
docker rm -f greca-central carrito-san-antonio
docker network rm red-cafetera
!NOTE ☸️ El Puente a Kubernetes:
En Kubernetes, las redes bridge de Docker son reemplazadas por el CNI (Container Network Interface) y los contenedores se descubren mediante Services (ClusterIP) gracias al servicio interno de CoreDNS.
Módulo 00 — Lab 4: Almacenamiento y Volúmenes
🧪 Laboratorio 4: Almacenamiento y Volúmenes (Datos Efímeros vs. Persistentes)
- Repositorio del Taller: Fundamentos-Kubernetes / semana-00-fundamentos-docker
- Comunidad: Cloud Native Medellín (CNCF Chapter)
Por defecto, los contenedores son efímeros e inmutables. Poseen una capa de lectura y escritura temporal (Copy-on-Write). Si un contenedor se elimina (docker rm), todo lo que escribió dentro de él se destruye.
☕ La Analogía Paisa: El Libro Contable del Carrito
Un carrito de tintos se puede dañar o cambiar, pero la libreta de cuentas (ventas.json) donde anotas las ventas no puede quemarse con el carrito. Necesitas guardarla en un casillero seguro: un volumen de Docker.
En este laboratorio utilizamos la aplicación de ventas app-ventas:v1.0 ubicada en 📂 $PWD/semana-00-fundamentos-docker/1.ejemplos/04-almacenamiento-y-volumenes/, la cual persiste registros en /app/datos/ventas.json.
🔨 Paso Previo: Construir la imagen con ruta absoluta
docker build -t app-ventas:v1.0 $PWD/semana-00-fundamentos-docker/1.ejemplos/04-almacenamiento-y-volumenes
🧪 Experimento 1: Sin Volumen (La Tragedia de los Datos Efímeros)
# 1. Iniciar contenedor sin montar volúmenes
docker run -d -p 8080:8080 --name test-efimero app-ventas:v1.0
# 2. Registrar ventas por HTTP
curl "http://localhost:8080/vender?tipo=Tinto+Campesino&cliente=Carlos"
curl "http://localhost:8080/vender?tipo=Perico&cliente=Mariana"
curl http://localhost:8080/api
# 3. Simular caída y destruir el contenedor
docker rm -f test-efimero
# 4. Volver a crear el contenedor
docker run -d -p 8080:8080 --name test-efimero app-ventas:v1.0
curl http://localhost:8080/api
# 💥 RESULTADO: "total_registros": 0. ¡Todos los datos se perdieron para siempre!
docker rm -f test-efimero
🧪 Experimento 2: Con Named Volume (Persistencia Real con docker volume)
# 1. Crear un volumen administrado por Docker
docker volume create datos-tintos
# 2. Iniciar el primer contenedor montando el volumen en /app/datos
docker run -d -p 8080:8080 --name carrito-1 -v datos-tintos:/app/datos app-ventas:v1.0
# 3. Registrar ventas
curl "http://localhost:8080/vender?tipo=Tinto+Especial&cliente=Don+Guillermo"
curl "http://localhost:8080/vender?tipo=Cafe+Origen&cliente=Sofia"
curl http://localhost:8080/api
# 4. ¡Destruir el contenedor sin piedad!
docker rm -f carrito-1
# 5. Iniciar un segundo contenedor completamente nuevo usando el MISMO volumen
docker run -d -p 8080:8080 --name carrito-2 -v datos-tintos:/app/datos app-ventas:v1.0
# 6. Consultar los datos en el nuevo contenedor
curl http://localhost:8080/api
# 🎉 ¡Todas las ventas de Don Guillermo y Sofía siguen intactas!
docker rm -f carrito-2
🧪 Experimento 3: Sincronización en vivo con Bind Mount (-v host:cont)
# 1. Crear carpeta local en tu máquina y darle permisos de escritura para el usuario no-root (UID 10001)
mkdir -p $PWD/mis-datos-locales
chmod 777 $PWD/mis-datos-locales
# 2. Montar la carpeta local en el contenedor usando ruta absoluta $PWD
docker run -d -p 8080:8080 --name carrito-bind -v "$PWD/mis-datos-locales:/app/datos" app-ventas:v1.0
# 3. Registrar una venta
curl "http://localhost:8080/vender?tipo=Tinto+con+Panela&cliente=Estudiante+Cloud+Native"
# 4. Comprobar que el archivo existe y se actualizó en tu máquina física
cat $PWD/mis-datos-locales/ventas.json
# 5. Limpieza
docker rm -f carrito-bind
docker volume rm datos-tintos
rm -rf $PWD/mis-datos-locales
!IMPORTANT 🛡️ Permisos en Bind Mounts con usuario no-root (UID 10001):
Como nuestroDockerfilesigue las mejores prácticas de seguridad ejecutándose conappuser(UID 10001), la carpeta montada del host requiere permisos de escritura (chmod 777) para permitirle al contenedor crear el archivoventas.json. Si no tiene permisos, el servidor responderá un error 500 explicativo indicando la solución.
🧪 Experimento 4: Escaneo de Seguridad y Configuración con Trivy
trivy image --severity HIGH,CRITICAL app-ventas:v1.0
trivy config $PWD/semana-00-fundamentos-docker/1.ejemplos/04-almacenamiento-y-volumenes
Módulo 00 — Lab 5: Registro Harbor
🧪 Laboratorio 5: Registro y Publicación de Imágenes con Harbor
- Repositorio del Taller: Fundamentos-Kubernetes / semana-00-fundamentos-docker
- Comunidad: Cloud Native Medellín (CNCF Chapter)
Una vez construidas tus imágenes locales, necesitas distribuirlas a servidores de prueba, producción o a los nodos de tu clúster. Para ello usamos Harbor, el registro OCI empresarial de código abierto graduado de la CNCF.
📖 Guía Teórica y Despliegue Local:
Los manifiestos de este laboratorio se encuentran en📂 $PWD/semana-00-fundamentos-docker/1.ejemplos/05-registro-imagenes-harbor/.
Para detalles arquitectónicos de Harbor empresarial y Helm, consulta la Guía Empresarial de Harbor.
📦 Manifiesto de Despliegue Local (docker-compose.yml)
Este laboratorio utiliza un registro OCI local compatible v2 y una interfaz web para explorar los repositorios:
services:
# 📦 Registro OCI Local (Compatible con Docker Distribution Registry v2)
registry:
image: registry:2
container_name: registro-local
restart: unless-stopped
ports:
- "5001:5000"
environment:
REGISTRY_STORAGE_DELETE_ENABLED: "true"
REGISTRY_HTTP_SECRET: "secreto-registro-local-cnm"
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: "['*']"
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: "['HEAD', 'GET', 'OPTIONS', 'DELETE']"
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: "['Authorization', 'Accept', 'Cache-Control']"
REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers: "['Docker-Content-Digest']"
volumes:
- datos-registro:/var/lib/registry
networks:
- red-registro
# 🌐 Interfaz Gráfica Web para explorar repositorios y tags
registry-ui:
image: joxit/docker-registry-ui:2
container_name: registro-ui
restart: unless-stopped
ports:
- "8082:80"
environment:
- SINGLE_REGISTRY=true
- REGISTRY_TITLE=Registro Local Cloud Native Medellín
- DELETE_IMAGES=true
- SHOW_CONTENT_DIGEST=true
- NGINX_PROXY_PASS_URL=http://registry:5000
depends_on:
- registry
networks:
- red-registro
volumes:
datos-registro:
name: datos-registro-local
networks:
red-registro:
name: red-registro-local
Para levantar el entorno:
docker compose -f $PWD/semana-00-fundamentos-docker/1.ejemplos/05-registro-imagenes-harbor/docker-compose.yml up -d
Esto iniciará el registro OCI local en el puerto 5001 y la interfaz gráfica web en http://localhost:8082.
1. Iniciar Sesión en el Registro (docker login)
💡 Nota para la Opción B (Registro Local con Compose): Si estás usando el registro local (
localhost:5001), no necesitas autenticarte (no requiere contraseña). Puedes pasar directamente al Paso 2.
# Autenticación con credenciales o token de Cuenta Robot en Harbor (Opción A):
docker login harbor.cloudnative.local:8080 -u "robot$ci-deployer" -p "tu-token-secreto"
2. Etiquetar las Imágenes con Nomenclatura OCI (docker tag)
Docker necesita conocer el registro de destino mediante el prefijo del tag:
🏷️ Nomenclatura Obligatoria:
<host-registro>/<proyecto>/<nombre-imagen>:<tag>
# Si estás usando el registro local con Docker Compose (Opción B):
REGISTRO="localhost:5001"
PROYECTO="cafeteria"
# Si estás usando un servidor Harbor empresarial / remoto (Opción A):
# REGISTRO="harbor.cloudnative.local:8080"
# PROYECTO="cafeteria"
# Etiquetamos las imágenes construidas en los laboratorios anteriores:
docker tag mi-web-estatica:v1.0 ${REGISTRO}/${PROYECTO}/mi-web-estatica:v1.0
docker tag mi-api-cnm:v1.0 ${REGISTRO}/${PROYECTO}/mi-api-cnm:v1.0
docker tag greca-backend:v1.0 ${REGISTRO}/${PROYECTO}/greca-backend:v1.0
docker tag app-ventas:v1.0 ${REGISTRO}/${PROYECTO}/app-ventas:v1.0
# Verificar los alias creados (no duplican espacio en disco)
docker images | grep ${PROYECTO}
3. Publicar las Imágenes (docker push)
docker push ${REGISTRO}/${PROYECTO}/mi-api-cnm:v1.0
⚡ Reutilización de Capas: Si luego publicas
greca-backend:v1.0(que también está basada enpython:3.12-alpine), verás que Docker indicaLayer already exists, ahorrando ancho de banda y tiempo.
4. Inspección de Artefactos y Escaneo de Seguridad
Dependiendo del entorno que estés utilizando:
- En la UI del Registro Local (Opción B - http://localhost:8082):
Verás el namespacecafeteria/, tags, digest SHA-256 inmutable (sha256:b9b4ef6d...), tamaño y capas. Al ser un visor ultraligero de desarrollo, no incluye escáner integrado. - Escaneo de Vulnerabilidades con Trivy (CLI Local):
Para auditar la imagen directamente en tu máquina:(O condocker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:latest image mi-api-cnm:v1.0docker scout cves mi-api-cnm:v1.0). Gracias apython:3.12-alpine, obtendrás 0 vulnerabilidades de severidad alta o crítica. - En el Portal Web de Harbor (Opción A):
Verás el badge interactivo con el reporte de Trivy generado automáticamente al hacer push y podrás configurar la política "Prevent vulnerable images from running" para bloquear imágenes con fallos críticos.
5. Descargar y Ejecutar en Entorno Limpio (docker pull)
# 1. Eliminar la copia local para forzar la descarga de red
docker rmi ${REGISTRO}/${PROYECTO}/mi-api-cnm:v1.0
# 2. Descargar imagen fresca desde Harbor
docker pull ${REGISTRO}/${PROYECTO}/mi-api-cnm:v1.0
# 3. Correr el contenedor desde la imagen del registro
docker run -d -p 8080:8080 --name test-harbor ${REGISTRO}/${PROYECTO}/mi-api-cnm:v1.0
# 4. Probar respuesta y limpiar
curl http://localhost:8080
docker rm -f test-harbor
Módulo 00 — Lab 6: IA Local con Ollama
🧪 Laboratorio 6: IA Local en Docker con Ollama (Asistente DevOps)
- Repositorio del Taller: Fundamentos-Kubernetes / semana-00-fundamentos-docker
- Comunidad: Cloud Native Medellín (CNCF Chapter)
Aprende a ejecutar un Modelo de Lenguaje (LLM) dentro de un contenedor Docker en modo exclusivamente CPU, sin requerir GPUs ni controladores CUDA. Crearemos un asistente especializado llamado devops-mini y lo consultaremos por terminal y mediante su API REST.
📖 Guía Teórica de Referencia:
Para comprender la arquitectura de Ollama, aceleración por hardware, comparativas y dimensionamiento de modelos (1.5B, 3B, 7B), consulta la Guía de IA Local con Ollama.
1. Crear el Volumen y Levantar Ollama con Límites de Recursos
# 1. Crear volumen para persistir los modelos descargados
docker volume create ollama
# 2. Ejecutar Ollama con techo de memoria y núcleos de CPU
docker run -d \
--name ollama \
--restart unless-stopped \
--memory=1400m \
--cpus=3 \
-p 127.0.0.1:11434:11434 \
-v ollama:/root/.ollama \
ollama/ollama
| Parámetro | Propósito Técnico |
|---|---|
--memory=1400m | Límite estricto de memoria RAM vía cgroups para evitar congelar el host. |
--cpus=3 | Techo de núcleos de CPU asignados para inferencia. |
-p 127.0.0.1:11434:11434 | Publica la API únicamente en loopback local por seguridad. |
-v ollama:/root/.ollama | Persistencia de modelos ante eliminación del contenedor. |
2. Descargar y Probar el Modelo Base (qwen2.5:0.5b)
# Descargar el modelo ligero (~400 MB)
docker exec -it ollama ollama pull qwen2.5:0.5b
# Probar conversación interactiva en terminal (escribe /bye para salir)
docker exec -it ollama ollama run qwen2.5:0.5b
3. Crear el Asistente DevOps con un Modelfile
Un Modelfile permite personalizar el comportamiento del modelo igual que un Dockerfile personaliza una imagen:
FROM qwen2.5:0.5b
PARAMETER num_ctx 1024
PARAMETER num_predict 256
PARAMETER temperature 0.2
SYSTEM """
Eres un asistente DevOps conciso y experto en Linux, Docker y Kubernetes.
Respondes en español con comandos precisos y explicaciones breves sin rodeos.
"""
Crea el modelo personalizado utilizando el Modelfile del repositorio con ruta absoluta (o enviándolo por entrada estándar):
# Opción 1: Directamente desde el archivo del repositorio con ruta absoluta
docker exec -i ollama ollama create devops-mini -f /dev/stdin < $PWD/semana-00-fundamentos-docker/1.ejemplos/06-ia-local-ollama/Modelfile
# Opción 2: O crearlo inline en la terminal mediante heredoc
docker exec -i ollama ollama create devops-mini -f /dev/stdin << 'EOF'
FROM qwen2.5:0.5b
PARAMETER num_ctx 1024
PARAMETER num_predict 256
PARAMETER temperature 0.2
SYSTEM """
Eres un asistente DevOps conciso y experto en Linux, Docker y Kubernetes.
Respondes en español con comandos precisos y explicaciones breves.
"""
EOF
Verifica la lista de modelos:
docker exec -it ollama ollama list
4. Probar el Asistente devops-mini en la Terminal
docker exec -it ollama ollama run devops-mini "Dame los comandos para ver los logs de un contenedor caído en Docker."
5. Consumir la API REST de Ollama
En otra terminal o script de automatización, envía una consulta en formato JSON:
curl http://127.0.0.1:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "devops-mini",
"messages": [
{ "role": "user", "content": "¿Qué diferencia hay entre CMD y ENTRYPOINT?" }
],
"stream": false
}'
6. Persistencia Real y Monitoreo de Recursos
# 1. Eliminar el contenedor
docker rm -f ollama
# 2. Recrearlo usando el mismo volumen 'ollama'
docker run -d --name ollama --restart unless-stopped \
--memory=1400m --cpus=3 \
-p 127.0.0.1:11434:11434 \
-v ollama:/root/.ollama \
ollama/ollama
# 3. Comprobar que los modelos no se perdieron
docker exec -it ollama ollama list
# 4. Monitorear consumo de RAM/CPU en tiempo real
docker stats ollama
# 5. Auditar si fue terminado por memoria (OOMKilled)
docker inspect ollama --format '{{.State.OOMKilled}}'
7. Auditoría de Seguridad con Trivy
trivy image --severity HIGH,CRITICAL ollama/ollama
8. Limpieza del Laboratorio
docker rm -f ollama
# (Opcional) Liberar espacio del volumen de modelos:
docker volume rm ollama
Módulo 00 — Retos Rotos de la Semana 00
🌉 El Puente a Kubernetes: Cargar Imágenes Locales en Kind
- Repositorio del Taller: Fundamentos-Kubernetes / semana-00-fundamentos-docker
- Comunidad: Cloud Native Medellín (CNCF Chapter)
Cuando uses un clúster local con Kind, Kubernetes por defecto intentará buscar las imágenes en Docker Hub o en registros remotos. Para usar imágenes construidas en tu computador sin publicarlas en internet:
# Carga la imagen local directamente dentro de los nodos del clúster Kind
kind load docker-image mi-api-cnm:v1.0 --name cnm-lab
¡Y en tus manifiestos de Kubernetes solo debes configurar imagePullPolicy: IfNotPresent!
🛠️ Guías y Recursos de Referencia (GitHub)
Para profundizar en los aspectos teóricos y buenas prácticas de producción, consulta las guías oficiales en el repositorio:
- 🛡️ Guía de Seguridad y Escaneo de Vulnerabilidades con Trivy: Auditoría de CVEs, mejores prácticas de Dockerfiles y comparación de distribuciones base.
- ⚓ Guía Empresarial de Harbor Registry: Arquitectura de microservicios, Cuentas Robot, políticas de retención y despliegue con Helm.
- 🤖 Guía de IA Local con Ollama: Aceleración por hardware, matriz de memoria de modelos LLM y directivas de seguridad corporativa.
- ☕ De un Tinto Colombiano a Kubernetes: La historia completa que conecta la venta de tinto en el Metro de Medellín con el Control Plane, Pods, Deployments y Services.
💥 Los 5 Retos Rotos de la Semana 00
En el repositorio oficial:
🔗 semana-00-fundamentos-docker/2.reto-roto/
Te enfrentarás a los 5 errores reales más frecuentes en contenedores. Cada reto incluye código que falla a propósito para que entrenes tus habilidades de diagnóstico:
| Reto | Tipo de Falla / Tema | Diagnóstico Clave | Enlace al Reto en GitHub |
|---|---|---|---|
| 01 | Proceso en segundo plano apaga el contenedor (PID 1) | docker ps -a muestra terminación inmediata. Nginx no corre con daemon off;. | 🔗 Ver Reto 01: Dockerfile Roto |
| 02 | Contenedor crashea por falta de variable de entorno (ENV) | docker logs revela excepción en Python por variable APP_SECRET_TOKEN ausente. | 🔗 Ver Reto 02: Variable Faltante |
| 03 | Contenedor vulnerable ejecutándose como root (Seguridad) | docker run whoami responde root (UID 0), violando las directivas de seguridad. | 🔗 Ver Reto 03: Contenedor Root |
| 04 | Fuga de contraseñas y secretos por falta de .dockerignore | COPY . . empaqueta archivos .env privados dentro de las capas de la imagen. | 🔗 Ver Reto 04: Archivos Secretos |
| 05 | Conexión rechazada por enlace a 127.0.0.1 y puerto desalineado | La app enlaza a localhost interno y rechaza conexiones externas del host. | 🔗 Ver Reto 05: Host Desalineado |
🧪 Comandos para probar los 5 Retos Rotos con Rutas Absolutas:
# Reto 01: Dockerfile Roto (Proceso en segundo plano apaga el contenedor)
docker build -t reto-docker-01 $PWD/semana-00-fundamentos-docker/2.reto-roto/01-dockerfile-roto
docker run -d --name test-reto-01 -p 8080:80 reto-docker-01
docker ps -a # Observa que se apaga inmediatamente (Exited 0)
docker rm -f test-reto-01
# Reto 02: Variable de Entorno Faltante
docker build -t reto-docker-02 $PWD/semana-00-fundamentos-docker/2.reto-roto/02-variable-entorno-faltante
docker run -d --name test-reto-02 reto-docker-02
docker logs test-reto-02 # Observa KeyError: 'APP_SECRET_TOKEN'
docker rm -f test-reto-02
# Reto 03: Contenedor Ejecutándose como Root
docker build -t reto-docker-03 $PWD/semana-00-fundamentos-docker/2.reto-roto/03-contenedor-como-root
docker run --rm reto-docker-03 whoami # Responde root en lugar de appuser
trivy image --severity HIGH,CRITICAL reto-docker-03
# Reto 04: Dockerignore Ausente y Fuga de Secretos
docker build -t reto-docker-04 $PWD/semana-00-fundamentos-docker/2.reto-roto/04-dockerignore-y-archivos-secretos
docker run --rm reto-docker-04 ls -la /app # Revela el archivo .env privado empaquetado
# Reto 05: Puerto y Host Desalineados
docker build -t reto-docker-05 $PWD/semana-00-fundamentos-docker/2.reto-roto/05-puerto-y-host-desalineado
docker run -d --name test-reto-05 -p 8080:8080 reto-docker-05
curl http://localhost:8080 # Fallo de conexión por enlazar a 127.0.0.1 y puerto 3000
docker rm -f test-reto-05
🏆 ¿Cómo enviar tus soluciones?
- Realiza un Fork de Fundamentos-Kubernetes.
- Clónalo y soluciona cada reto en tu máquina.
- Copia tus soluciones a
aportes-comunidad/soluciones-retos/<tu-usuario>/semana-00/. - Abre tu Pull Request para subir de rango en
ASISTENTES.md.
Módulo 01: El Motor por Dentro y tu Primer Pod
🌱 Módulo 01: El Motor por Dentro y tu Primer Pod
Workshop De Cero a Capitán en Kubernetes — Cloud Native Medellín 🏔️☕
- Repositorio del Módulo: Fundamentos-Kubernetes / semana-01-arquitectura
- Comunidad: Cloud Native Medellín (CNCF Chapter)
🎯 ¿Qué vas a dominar en esta unidad?
- La arquitectura interna de Kubernetes: Control Plane (Cerebro) vs. Worker Nodes (Estaciones físicas).
- Qué es un Pod y por qué es la unidad atómica de ejecución en Kubernetes.
- Cómo escribir y entender la estructura fundamental de un manifiesto YAML en Kubernetes.
- Crear y verificar tu clúster local usando Kind.
- Desplegar un Pod básico e interactuar con él usando la CLI
kubectl. - Implementar el patrón multi-contenedor Sidecar.
- Acceder a tu aplicación desde el navegador mediante
port-forward. - Conectar una consola visual moderna con Headlamp.
- Diagnosticar problemas comunes de Pods (
ImagePullBackOff,CrashLoopBackOff,Pending).
☕ La Analogía del Metro de Medellín y el Carrito de Tintos
Imagina el sistema de transporte del Metro de Medellín:
- El Contenedor (Docker): La greca encendida preparando café (tu aplicación corriendo en un proceso).
- El Pod: El carrito de tintos completo, donde montas la greca junto con los vasitos desechables, servilletas y azúcar. En Kubernetes nunca pones grecas sueltas en el piso: siempre van dentro de un Pod que les proporciona dirección IP única (
10.244.x.x), espacio de red compartido (localhost) y almacenamiento compartido. - El Worker Node: La estación física (ej. Estación San Antonio o Parque Berrío) que cuenta con espacio, electricidad y red para alojar múltiples carritos.
- El Control Plane (Cerebro): La administración central del Metro que supervisa toda la red, decide en qué estación colocar cada carrito y envía reemplazos inmediatamente si alguno falla.
👨💻 Tú (kubectl)
│
▼ 1. "Quiero un Pod con Nginx"
🧠 Control Plane (API Server)
│
▼ 2. Asigna el servidor con recursos libres
💪 Worker Node (Estación San Antonio)
│
▼ 3. Kubelet prepara el carrito
🛒 Pod (IP: 10.244.0.5)
└── 🔥 Contenedor Nginx (Puerto 80)
🛠️ Paso 1: Levanta tu Clúster Local con Kind
Si aún no tienes un clúster corriendo en tu entorno local:
# Crear un clúster local ligero con Kind
kind create cluster --name cnm-lab
# Verificar que los nodos estén listos (Ready)
kubectl get nodes
📄 Anatomía de un Manifiesto YAML
En Kubernetes todo se define de forma declarativa con 4 campos obligatorios:
apiVersion: v1 # Versión de la API de Kubernetes para este recurso
kind: Pod # Tipo de objeto que queremos crear (Pod, Deployment, Service, etc.)
metadata:
name: mi-primer-pod # Nombre único del recurso
labels:
app: web-medellin # Etiquetas clave-valor para identificarlo
spec: # Estado deseado del recurso
containers:
- name: servidor-web
image: nginx:alpine
ports:
- containerPort: 80
🧪 Laboratorio 1: Despliega tu Primer Pod
1. Guarda el manifiesto en mi-primer-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: hola-medellin
labels:
app: bienvenida
comunidad: cloud-native-medellin
spec:
containers:
- name: web
image: nginx:alpine
ports:
- containerPort: 80
2. Aplica el Pod en el clúster:
kubectl apply -f mi-primer-pod.yaml
3. Inspecciona el estado:
# Ver lista de Pods
kubectl get pods
# Ver detalles a fondo, IP asignada y eventos del ciclo de vida
kubectl describe pod hola-medellin
4. Conéctate desde tu navegador con Port-Forward:
kubectl port-forward pod/hola-medellin 8080:80
Abre en tu navegador: http://localhost:8080. ¡Verás a Nginx respondiendo desde tu Pod!
🧪 Laboratorio 2: Pod Multi-Contenedor (Patrón Sidecar)
Dos contenedores dentro del mismo Pod comparten almacenamiento (volumes) y pueden comunicarse mediante localhost.
pod-sidecar.yaml:
apiVersion: v1
kind: Pod
metadata:
name: pod-con-sidecar
spec:
volumes:
- name: html-compartido
emptyDir: {}
containers:
# Contenedor 1: Servidor Web Principal
- name: servidor-nginx
image: nginx:alpine
volumeMounts:
- name: html-compartido
mountPath: /usr/share/nginx/html
ports:
- containerPort: 80
# Contenedor 2: Sidecar de contenido (actualiza el HTML periódicamente)
- name: actualizador-sidecar
image: busybox:latest
volumeMounts:
- name: html-compartido
mountPath: /datos
command: ["sh", "-c"]
args:
- |
while true; do
echo "<h1>🏔️ Cloud Native Medellín</h1><p>Hora actual en el Pod: $(date)</p>" > /datos/index.html
sleep 5
done
Aplica el manifiesto y compruébalo:
kubectl apply -f pod-sidecar.yaml
kubectl port-forward pod/pod-con-sidecar 8081:80
Visita http://localhost:8081 y refresca cada pocos segundos para ver la hora actualizarse dinámicamente gracias al sidecar.
🖥️ Consola Visual con Headlamp
Para supervisar Nodos, Pods, eventos y logs con una interfaz web moderna y amigable:
# Puedes ejecutar Headlamp como contenedor local en segundos:
docker run -d -p 9090:9090 -v ~/.kube/config:/.kube/config:ro headlamp-k8s/headlamp:latest
Abre http://localhost:9090 y explora visualmente tu clúster.
👉 Guía completa: semana-01-arquitectura/headlamp.md
🕵️♂️ Diccionario Rápido: ¿Qué le duele a mi Pod?
| Estado | ¿Qué significa? | ¿Cómo solucionarlo? |
|---|---|---|
Pending | El Pod está en espera. Ningún nodo tiene suficiente CPU/RAM libre o no cumple los selectores. | kubectl describe pod <nombre> y revisar la sección Events al final. |
ImagePullBackOff | No se pudo descargar la imagen de Docker (nombre erróneo, tag inexistente o imagen privada). | Revisa el nombre de la imagen con kubectl describe pod <nombre>. Si es local en Kind, recuerda hacer kind load docker-image <imagen>. |
CrashLoopBackOff | El contenedor arranca pero su proceso principal termina con código de error (exit code != 0). | Ejecuta kubectl logs <nombre> y kubectl logs <nombre> --previous para ver qué excepción detuvo la app. |
💥 El Reto Roto de la Semana 01
En el repositorio:
🔗 semana-01-arquitectura/2.reto-roto/pod-con-errores.yaml
Un compañero intentó desplegar un Pod con Nginx pero cometió 2 errores muy comunes y el Pod nunca pasa a estado Running:
- Aplícalo en tu clúster y observa qué falla.
- Investiga con
kubectl describe pod pod-rebelde. - Corrige el archivo y sube tu solución a
aportes-comunidad/soluciones-retos/<tu-usuario>/semana-01/. - ¡Envía tu Pull Request a la tripulación!
Módulo 02: Despliegues Sin Caídas y Autorreparación
🔄 Módulo 02: Despliegues Sin Caídas y Autorreparación
Workshop De Cero a Capitán en Kubernetes — Cloud Native Medellín 🏔️☕
- Repositorio Oficial: Fundamentos-Kubernetes en GitHub
- Comunidad: Cloud Native Medellín (CNCF Chapter)
🎯 ¿Qué vas a dominar en esta unidad?
- Por qué en producción nunca desplegamos Pods sueltos.
- Cómo el bucle de control de reconciliación de ReplicaSet garantiza resiliencia.
- Crear y gestionar Deployments de manera declarativa.
- Estrategia de actualización continua (Rolling Updates) sin tiempo de inactividad (zero downtime).
- Simulación de fallos y prueba del mecanismo de autorreparación (Self-healing).
- Reversión instantánea (Rollback) con
kubectl rollout undo.
☕ La Analogía del Tinto: La Flota de Carritos
Si un carrito de tintos en la Estación San Antonio se queda sin gas o se apaga la greca, los usuarios se quedan sin café.
Con un Deployment, la administración del Metro define: "Siempre debe haber 3 carritos activos en San Antonio". Si uno se avería, el ReplicaSet detecta que solo hay 2 y automáticamente enciende un tercer carrito nuevo de inmediato.
📄 Ejemplo Práctico: Despliegue de Alta Disponibilidad
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-paisa-deployment
labels:
app: web-paisa
spec:
replicas: 3
selector:
matchLabels:
app: web-paisa
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: web-paisa
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
🛠️ Comandos Esenciales de Deployments
# Aplicar el Deployment
kubectl apply -f deployment.yaml
# Ver estado de réplicas
kubectl get deployments
kubectl get pods -l app=web-paisa
# Simular falla: elimina un Pod y observa cómo nace otro de inmediato
kubectl delete pod <nombre-de-un-pod>
kubectl get pods
# Actualizar la versión de la imagen en caliente
kubectl set image deployment/web-paisa-deployment nginx=nginx:1.25-alpine
# Ver el progreso del Rolling Update
kubectl rollout status deployment/web-paisa-deployment
# Revertir a la versión anterior si algo salió mal
kubectl rollout undo deployment/web-paisa-deployment
💥 Reto Roto de la Semana
Revisa el reto semanal en el repositorio oficial:
🔗 Fundamentos-Kubernetes en GitHub
Módulo 03: Conectando Todo: Redes en Kubernetes
🌐 Módulo 03: Conectando Todo: Redes en Kubernetes
Workshop De Cero a Capitán en Kubernetes — Cloud Native Medellín 🏔️☕
- Repositorio Oficial: Fundamentos-Kubernetes en GitHub
- Comunidad: Cloud Native Medellín (CNCF Chapter)
🎯 ¿Qué vas a dominar en esta unidad?
- El problema de las IPs dinámicas de los Pods y cómo lo resuelve la abstracción de Service.
- Tipos de servicios:
ClusterIP,NodePortyLoadBalancer. - Descubrimiento de servicios y resolución interna con CoreDNS.
- Enrutamiento capa 7 mediante Ingress Controllers.
- Diagnóstico de conectividad y balanceo interno de tráfico.
☕ La Analogía del Tinto: La Fila Única y el Torniquete
- Service (Punto de información y fila única): Si hay 3 carritos de tintos rotando e intercambiando puestos, los clientes no persiguen a los carritos individuales: hacen fila en un punto de atención fijo (la IP virtual del Service) que despacha automáticamente al carrito libre.
- Ingress (Los torniquetes y señalización): Quien llega de la calle por la entrada principal ve carteles: "Si va a la cafetería siga al frente, si va a las taquillas gire a la izquierda". Ingress analiza la URL (
/api,/web) y deriva el tráfico hacia el Service adecuado.
📄 Ejemplo Práctico: Service ClusterIP y Service NodePort
apiVersion: v1
kind: Service
metadata:
name: servicio-web-interno
spec:
type: ClusterIP
selector:
app: web-paisa
ports:
- port: 80 # Puerto del servicio expuesto internamente en el clúster
targetPort: 80 # Puerto del contenedor en el Pod
---
apiVersion: v1
kind: Service
metadata:
name: servicio-web-externo
spec:
type: NodePort
selector:
app: web-paisa
ports:
- port: 80
targetPort: 80
nodePort: 30080 # Puerto estático en todos los nodos (30000-32767)
📄 Ejemplo Práctico: Ingress HTTP
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-principal
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: workshop.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: servicio-web-interno
port:
number: 80
💥 Reto Roto de la Semana
Revisa el reto semanal en el repositorio oficial:
🔗 Fundamentos-Kubernetes en GitHub
Módulo 04: Configuración y Datos Persistentes
💾 Módulo 04: Configuración y Datos Persistentes
Workshop De Cero a Capitán en Kubernetes — Cloud Native Medellín 🏔️☕
- Repositorio Oficial: Fundamentos-Kubernetes en GitHub
- Comunidad: Cloud Native Medellín (CNCF Chapter)
🎯 ¿Qué vas a dominar en esta unidad?
- El principio de las 12 aplicaciones de factores: desacoplar código de configuración.
- Inyectar variables de entorno y archivos usando ConfigMaps.
- Almacenar credenciales, llaves y contraseñas de forma segura con Secrets.
- Diferenciar almacenamiento efímero de almacenamiento persistente.
- Gestionar la tríada del almacenamiento:
PersistentVolume(PV),PersistentVolumeClaim(PVC) yStorageClass.
📄 Ejemplo Práctico: ConfigMap y Secret
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_ENV: "production"
MENSAJE_BIENVENIDA: "¡Bienvenidos a Cloud Native Medellín! 🏔️☕"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
DB_PASSWORD: "SuperClaveSecretaK8s2026"
📄 Ejemplo Práctico: Solicitud de Volumen Persistente (PVC)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-datos-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
💥 Reto Roto de la Semana
Revisa el reto semanal en el repositorio oficial:
🔗 Fundamentos-Kubernetes en GitHub
Módulo 05: Salud, Límites y Seguridad Básica
🛡️ Módulo 05: Salud, Límites y Seguridad Básica
Workshop De Cero a Capitán en Kubernetes — Cloud Native Medellín 🏔️☕
- Repositorio Oficial: Fundamentos-Kubernetes en GitHub
- Comunidad: Cloud Native Medellín (CNCF Chapter)
🎯 ¿Qué vas a dominar en esta unidad?
- Configuración de sondas de salud:
LivenessProbe,ReadinessProbeyStartupProbe. - Asignación de
requestsylimitsde CPU y Memoria (prevención deOOMKilled). - Aislamiento lógico multi-tenant con Namespaces.
- Principios de menor privilegio con RBAC:
ServiceAccount,RoleyRoleBinding.
📄 Ejemplo Práctico: Sondas de Salud y Límites de Recursos
apiVersion: v1
kind: Pod
metadata:
name: app-resiliente
spec:
containers:
- name: web
image: nginx:alpine
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "250m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 2
periodSeconds: 5
💥 Reto Roto de la Semana
Revisa el reto semanal en el repositorio oficial:
🔗 Fundamentos-Kubernetes en GitHub
Módulo 06: Empaquetado con Helm y Proyecto Final
🚀 Módulo 06: Empaquetado con Helm y Proyecto Final
Workshop De Cero a Capitán en Kubernetes — Cloud Native Medellín 🏔️☕
- Repositorio Oficial: Fundamentos-Kubernetes en GitHub
- Comunidad: Cloud Native Medellín (CNCF Chapter)
🎯 ¿Qué vas a dominar en esta unidad?
- Por qué Helm es el estándar de la industria para empaquetar aplicaciones en Kubernetes.
- La estructura de un Helm Chart:
Chart.yaml,values.yamlytemplates/. - Gestión de plantillas Go (
{{ .Values.xyz }}). - Despliegue, actualización y desinstalación con la CLI de Helm.
- Proyecto Final: Despliegue de una aplicación moderna completa (Frontend, Backend y Base de datos con almacenamiento persistente).
- ¡Graduación oficial y obtención del rango de Capitán K8s en la tripulación! ⚓
🛠️ Comandos Esenciales de Helm
# Crear la estructura de un Chart nuevo
helm create mi-app-chart
# Validar la sintaxis del Chart
helm lint mi-app-chart
# Simular el renderizado de los manifiestos sin aplicarlos
helm template mi-app-chart
# Instalar el Chart en el clúster
helm install mi-app-release ./mi-app-chart
# Actualizar con nuevos valores
helm upgrade mi-app-release ./mi-app-chart -f custom-values.yaml
# Revertir a una revisión previa si hubo fallos
helm rollback mi-app-release 1
⚓ Graduación: Capitán K8s
Completa el proyecto final, sube tu solución a aportes-comunidad/ en tu fork y abre tu Pull Request al repositorio oficial:
🔗 https://github.com/edwinbermudez91/Fundamentos-Kubernetes
¡Felicitaciones por llegar al final de la travesía con Cloud Native Medellín! 🏔️☕