Un contenedor es una unidad de software que empaqueta una aplicación junto con todas sus dependencias (librerías, runtime, ficheros de configuración) de forma que se ejecuta igual en cualquier entorno: el portátil del desarrollador, un servidor de pruebas o la nube en producción. Resuelve el clásico "en mi máquina funciona". Docker es la herramienta más popular para construir y ejecutar contenedores, y Kubernetes es el orquestador estándar de la industria para gestionar miles de contenedores en producción: los despliega, los escala, los reinicia si fallan y los conecta entre sí. Dominar ambos es imprescindible en arquitectura cloud-native, porque casi toda aplicación moderna se entrega en contenedores. Esta lección te lleva desde el concepto hasta un despliegue real en Kubernetes.
Contenido
- Contenedores frente a máquinas virtuales.
- Anatomía de Docker: imágenes, contenedores y registros.
- Construir una imagen con un Dockerfile.
- Por qué necesitamos un orquestador.
- Conceptos clave de Kubernetes: Pod, Deployment y Service.
- Un manifiesto de Kubernetes paso a paso.
- Errores comunes y consejos.
- Ejercicios y soluciones.
- Contenedores frente a máquinas virtuales
Ambos aíslan aplicaciones, pero de forma muy distinta. Una máquina virtual (VM) virtualiza el hardware completo e incluye un sistema operativo invitado entero. Un contenedor virtualiza solo el sistema operativo: comparte el kernel del anfitrión y empaqueta únicamente la aplicación y sus dependencias.
| Aspecto | Máquina Virtual | Contenedor |
|---|---|---|
| Aislamiento | Hardware completo (hipervisor) | Proceso (namespaces del kernel) |
| Sistema operativo | Uno completo por VM | Comparte el kernel del anfitrión |
| Tamaño | Gigabytes | Megabytes |
| Tiempo de arranque | Minutos | Segundos o menos |
| Densidad (cuántos por host) | Pocos | Muchos |
| Portabilidad | Media | Muy alta |
graph TB
subgraph "Maquinas Virtuales"
HW1[Hardware] --> HYP[Hipervisor]
HYP --> VM1[SO Invitado + App A]
HYP --> VM2[SO Invitado + App B]
end
subgraph "Contenedores"
HW2[Hardware] --> OS[SO Anfitrion]
OS --> DOCK[Docker Engine]
DOCK --> C1[App A]
DOCK --> C2[App B]
endExplicación del diagrama: en las VMs cada aplicación arrastra su propio sistema operativo completo sobre un hipervisor, lo que consume muchos recursos. En los contenedores, el motor (Docker) comparte un único sistema operativo anfitrión, así que los contenedores son ligeros y arrancan en segundos. No son enemigos: a menudo se ejecutan contenedores dentro de VMs.
- Anatomía de Docker
Tres conceptos que no debes confundir:
- Imagen (image): plantilla inmutable de solo lectura que contiene tu aplicación y dependencias. Es como una "foto" o molde.
- Contenedor (container): una instancia en ejecución de una imagen. De una imagen puedes arrancar muchos contenedores.
- Registro (registry): almacén de imágenes (Docker Hub, GitHub Container Registry, AWS ECR). Subes (
push) y descargas (pull) imágenes desde ahí.
# Construir una imagen a partir del Dockerfile del directorio actual docker build -t miapp:1.0 . # Ejecutar un contenedor a partir de la imagen, publicando el puerto 8080 docker run -d -p 8080:8080 --name miapp miapp:1.0 # Ver los contenedores en ejecucion docker ps # Subir la imagen a un registro docker push miregistro.io/miapp:1.0
Explicación de los comandos:
docker build -t miapp:1.0 .: construye la imagen.-tla etiqueta con nombremiappy versión1.0; el.indica que el Dockerfile está en el directorio actual.docker run -d -p 8080:8080: arranca un contenedor.-dlo ejecuta en segundo plano (detached);-p 8080:8080mapea el puerto del anfitrión al del contenedor para poder acceder desde fuera.docker ps: lista los contenedores activos, su estado y puertos.docker push: publica la imagen en un registro para que otros (o un clúster) la descarguen.
- Construir una imagen con un Dockerfile
Un Dockerfile es un fichero de texto con instrucciones para construir la imagen, paso a paso. Cada instrucción crea una "capa" cacheable. Veamos un ejemplo realista con multi-stage build (compilación en varias etapas) para una aplicación Java:
# --- Etapa 1: compilacion --- FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # --- Etapa 2: imagen final, ligera --- FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=build /app/target/miapp.jar app.jar EXPOSE 8080 USER 1000 ENTRYPOINT ["java", "-jar", "app.jar"]
Explicación instrucción a instrucción:
FROM maven:... AS build: parte de una imagen base con Maven y Java para compilar. La nombramosbuildpara referirnos a ella después.WORKDIR /app: establece el directorio de trabajo dentro de la imagen.COPY pom.xml .+RUN mvn dependency:go-offline: copiamos primero solo elpom.xmly descargamos dependencias. Así, si el código cambia pero las dependencias no, Docker reutiliza esta capa de la caché y la construcción es más rápida.COPY src ./src+RUN mvn package: copia el código fuente y genera el.jar.- Segundo
FROM eclipse-temurin:17-jre-alpine: empieza una imagen final mucho más pequeña (solo el runtime de Java, sobre Alpine Linux). Esta técnica deja fuera Maven y el código fuente, reduciendo el tamaño y la superficie de ataque. COPY --from=build ...: copia solo el artefacto compilado desde la etapa anterior.EXPOSE 8080: documenta el puerto que usa la app.USER 1000: ejecuta el proceso con un usuario sin privilegios (no root), buena práctica de seguridad.ENTRYPOINT [...]: comando que se ejecuta al arrancar el contenedor.
- Por qué necesitamos un orquestador
Ejecutar uno o dos contenedores con docker run es fácil. ¿Pero qué pasa con cientos de contenedores en decenas de servidores? Necesitas resolver: ¿en qué servidor coloco cada contenedor? ¿qué hago si un contenedor se cae? ¿cómo escalo de 3 a 30 réplicas en un pico de tráfico? ¿cómo actualizo sin tiempo de inactividad? ¿cómo se encuentran entre sí? Hacerlo a mano es inviable. Un orquestador como Kubernetes automatiza todo esto.
- Conceptos clave de Kubernetes
Kubernetes (abreviado K8s) organiza los contenedores con varias abstracciones. Las tres fundamentales:
| Objeto | Qué es | Analogía |
|---|---|---|
| Pod | Unidad mínima desplegable: uno o varios contenedores que comparten red y almacenamiento | Una "cápsula" con tu app |
| Deployment | Gestiona réplicas de Pods y actualizaciones controladas | El "gestor" que mantiene N copias vivas |
| Service | Punto de acceso de red estable hacia un conjunto de Pods | La "recepción" con dirección fija |
- Pod: lo más pequeño que despliega Kubernetes. Normalmente un contenedor por Pod. Los Pods son efímeros: pueden morir y recrearse con otra IP.
- Deployment: declaras "quiero 3 réplicas de esta imagen" y Kubernetes las mantiene. Si un Pod muere, crea otro. Permite rolling updates (actualizaciones sin caída).
- Service: como los Pods cambian de IP, el Service da una dirección estable y reparte el tráfico entre los Pods sanos (balanceo de carga interno).
graph LR
USER[Usuario] --> SVC[Service: IP estable]
SVC --> P1[Pod 1]
SVC --> P2[Pod 2]
SVC --> P3[Pod 3]
DEP[Deployment] -.gestiona.-> P1
DEP -.gestiona.-> P2
DEP -.gestiona.-> P3Explicación: el Deployment crea y vigila los tres Pods. El Service recibe el tráfico del usuario y lo reparte entre los Pods disponibles. Si el Pod 2 cae, el Deployment crea uno nuevo y el Service deja de enviarle tráfico hasta que esté listo.
- Un manifiesto de Kubernetes paso a paso
En Kubernetes describes el estado deseado en ficheros YAML declarativos. Aquí un Deployment y un Service para nuestra aplicación:
apiVersion: apps/v1
kind: Deployment
metadata:
name: miapp-deployment
spec:
replicas: 3 # numero de Pods deseados
selector:
matchLabels:
app: miapp # selecciona los Pods con esta etiqueta
template: # plantilla de cada Pod
metadata:
labels:
app: miapp
spec:
containers:
- name: miapp
image: miregistro.io/miapp:1.0
ports:
- containerPort: 8080
resources: # limites de recursos
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe: # cuando esta listo para recibir trafico
httpGet:
path: /health
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: miapp-service
spec:
selector:
app: miapp # enruta hacia los Pods con esta etiqueta
ports:
- port: 80 # puerto del Service
targetPort: 8080 # puerto del contenedor
type: ClusterIP # accesible solo dentro del clusterExplicación campo a campo:
kind: Deploymentyreplicas: 3: pedimos un Deployment que mantenga tres réplicas.selector.matchLabels+template.metadata.labels: el Deployment vincula los Pods que tengan la etiquetaapp: miapp. Las etiquetas son el pegamento de Kubernetes.image: la imagen del registro que vimos antes.resources.requests/limits:requestses lo que el Pod reserva como mínimo;limitses el máximo que puede consumir.250msignifica 0,25 de CPU;256Mison 256 mebibytes de RAM. Esto evita que un Pod ahogue a los demás.readinessProbe: Kubernetes consulta/healthpara saber cuándo el Pod está listo. Hasta que responda OK, el Service no le envía tráfico.- En el Service,
selector: app: miappconecta el Service con esos Pods;port: 80es donde escucha el Service ytargetPort: 8080es el puerto del contenedor;type: ClusterIPlo hace accesible solo internamente (para exponerlo al exterior se usaLoadBalancero unIngress). - El
---separa dos documentos YAML en el mismo fichero.
# Aplicar el manifiesto al cluster kubectl apply -f miapp.yaml # Ver el estado de los Pods kubectl get pods # Escalar a 5 replicas en caliente kubectl scale deployment miapp-deployment --replicas=5
Explicación: kubectl apply envía el estado deseado al clúster, que se encarga de alcanzarlo. kubectl get pods muestra los Pods y su estado. kubectl scale cambia el número de réplicas sin editar el fichero.
Errores Comunes y Consejos
- Construir imágenes enormes. Usa multi-stage builds e imágenes base ligeras (
alpine,distroless). Una imagen de 1 GB es lenta de desplegar y un riesgo de seguridad. - Ejecutar como root. Define un
USERno privilegiado en el Dockerfile. Un contenedor comprometido como root es más peligroso. - No definir requests y limits. Sin límites, un Pod puede consumir toda la memoria del nodo y tumbar a sus vecinos.
- Olvidar las probes. Sin
readinessProbe/livenessProbe, Kubernetes envía tráfico a Pods que aún no están listos o no reinicia los que se han colgado. - Tratar los Pods como mascotas. Los Pods son ganado, no mascotas: son efímeros y reemplazables. No guardes estado dentro de ellos; usa volúmenes o servicios externos.
- Consejo: versiona siempre las imágenes con etiquetas explícitas (
miapp:1.0), nunca confíes enlatesten producción porque es ambiguo y dificulta los rollbacks.
Ejercicios
- Explica con tus palabras por qué un contenedor arranca en segundos y una VM en minutos.
- En el manifiesto de ejemplo, ¿qué ocurre si eliminas manualmente uno de los tres Pods del Deployment? ¿Por qué?
- Quieres exponer tu aplicación al tráfico externo de Internet. El Service del ejemplo es de tipo
ClusterIP. ¿Qué cambiarías y qué alternativas tienes?
Soluciones
- El contenedor comparte el kernel del sistema operativo anfitrión y solo arranca el proceso de la aplicación con sus dependencias, mientras que la VM debe arrancar un sistema operativo invitado completo (kernel, servicios, etc.) sobre el hipervisor. Menos que arrancar implica menos tiempo.
- El Deployment detecta que solo hay 2 Pods cuando el estado deseado son 3 y crea automáticamente un Pod nuevo para volver a 3 réplicas. Kubernetes reconcilia continuamente el estado real con el declarado.
- Cambiarías
type: ClusterIPportype: LoadBalancer(que aprovisiona un balanceador de carga del proveedor cloud con IP pública) o, de forma más habitual y flexible, mantendrías el Service comoClusterIPy colocarías delante un Ingress con reglas de enrutamiento por host/ruta y terminación TLS.
Conclusión
Has aprendido la diferencia entre contenedores y VMs, cómo Docker empaqueta aplicaciones mediante imágenes y Dockerfiles, por qué hace falta un orquestador y los tres pilares de Kubernetes (Pod, Deployment, Service) plasmados en un manifiesto YAML real. Los contenedores te dan portabilidad y Kubernetes te da escala y resiliencia. En la siguiente lección exploraremos un modelo aún más abstracto en el que ni siquiera gestionas contenedores ni servidores: la arquitectura serverless.
Curso de Arquitectura de Aplicaciones
Módulo 1: Fundamentos de la Arquitectura de Aplicaciones
- ¿Qué es la Arquitectura de Aplicaciones?
- El Rol del Arquitecto de Software
- Atributos de Calidad y Requisitos No Funcionales
- Decisiones Arquitectónicas y Compromisos (Trade-offs)
- Documentación de Arquitectura: Vistas y el Modelo C4
Módulo 2: Principios y Tácticas de Diseño
- Acoplamiento, Cohesión y Separación de Responsabilidades
- Principios SOLID Aplicados a la Arquitectura
- DRY, KISS, YAGNI y Otros Principios de Diseño
- Tácticas Arquitectónicas para los Atributos de Calidad
- Gestión de la Deuda Técnica
Módulo 3: Estilos y Patrones Arquitectónicos
- Arquitectura Monolítica
- Arquitectura en Capas (N-Tier)
- Arquitectura Cliente-Servidor
- Arquitectura Hexagonal (Puertos y Adaptadores)
- Arquitectura Limpia y Cebolla (Clean & Onion)
Módulo 4: Arquitecturas Distribuidas y Microservicios
- Introducción a los Sistemas Distribuidos
- Arquitectura de Microservicios
- Descomposición de Servicios y Bounded Contexts
- API Gateway, Service Discovery y Comunicación entre Servicios
- Patrones de Resiliencia: Circuit Breaker, Retry y Bulkhead
- El Teorema CAP y la Consistencia de Datos
Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería
- Fundamentos de la Arquitectura Orientada a Eventos
- Mensajería Asíncrona: Colas y Brokers
- Patrones de Eventos: Event Sourcing y CQRS
- Gestión de Transacciones Distribuidas: Patrón Saga
- Streaming de Datos en Tiempo Real
Módulo 6: Diseño Dirigido por el Dominio (DDD)
- Conceptos Fundamentales del DDD
- Diseño Estratégico: Bounded Contexts y Lenguaje Ubicuo
- Diseño Táctico: Entidades, Agregados y Repositorios
- Mapeo de Contextos (Context Mapping)
Módulo 7: Datos y Persistencia
- Estrategias de Persistencia: SQL vs NoSQL
- Patrones de Acceso a Datos: Repository, Unit of Work y DAO
- Base de Datos por Servicio y Gestión de Datos Distribuidos
- Caché y Estrategias de Invalidación
Módulo 8: Arquitectura en la Nube y Despliegue
- Fundamentos del Cloud Computing (IaaS, PaaS, SaaS)
- Contenedores y Orquestación con Docker y Kubernetes
- Arquitectura Serverless
- Patrones de Diseño Cloud-Native
- Infraestructura como Código (IaC)
Módulo 9: Calidad, Seguridad y Observabilidad
- Escalabilidad: Horizontal vs Vertical y Balanceo de Carga
- Alta Disponibilidad y Tolerancia a Fallos
- Seguridad por Diseño y Autenticación/Autorización
- Observabilidad: Logging, Métricas y Trazabilidad
- Rendimiento y Pruebas de Carga
