Fundamentos de la computación en la nube

Contenedores y microservicios

Qué empaqueta en verdad un contenedor, por qué dividir un monolito en microservicios cambia cómo un equipo lanza y escala, y qué agrega un orquestador como Kubernetes cuando tienes más contenedores de los que puedes manejar a mano.

Intermedio 19 minutos 5 Objetivos de aprendizaje
  1. Explicar qué es un contenedor y por qué arranca más rápido que una máquina virtual
  2. Describir cómo una imagen de contenedor mantiene el entorno de ejecución idéntico desde una laptop hasta producción
  3. Distinguir una aplicación monolítica de una arquitectura de microservicios por cómo se despliega y se escala cada una
  4. Identificar qué agrega la orquestación de contenedores sobre correr contenedores individuales a mano
  5. Reconocer las contrapartidas operativas que introducen los microservicios a cambio de escalado y despliegue independientes

La lección pasada respondió quién corre el código. Esta pregunta cómo se construye

La lección pasada dejó resuelto el cómputo: una función sin servidor, una máquina virtual, o algo intermedio corre tu código, y el proveedor se encarga de más o menos trabajo alrededor según cuál elijas. Nada de eso dice cómo está armada la aplicación misma. Una aplicación que empezó como un solo código base grande, un solo despliegue, un solo equipo, se topa con un problema específico al crecer: una pequeña corrección en el flujo de checkout significa probar y volver a desplegar toda la aplicación, catálogo, cuentas y todo, y un pico de tráfico en una sola parte de la app te obliga a escalar todo lo demás. Contenedores y microservicios son 2 ideas separadas que, juntas, resuelven exactamente ese problema.

Qué empaqueta en verdad un contenedor

Un contenedor es un proceso aislado empaquetado con todo lo que necesita para correr: tu código, su runtime y sus dependencias, todo en una unidad autocontenida. Empaqueta una aplicación de Node.js en una imagen de contenedor y esa imagen corre idéntica sin importar si arrancó en la laptop de un desarrollador, en un servidor de pruebas o en un clúster de producción, porque la imagen lleva su propio runtime en vez de depender de lo que esté instalado en el anfitrión.

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
CMD ["node", "server.js"]
docker build -t checkout-service:1.0 .
docker run -p 3000:3000 checkout-service:1.0

Ese Dockerfile describe la imagen; docker build la empaqueta; docker run arranca un contenedor a partir de ella. La lección de cómputo ya ubicó a los contenedores en el espectro de control contra comodidad, arrancando en segundos en vez de los minutos de una máquina virtual, porque un contenedor comparte el kernel del sistema operativo anfitrión en vez de arrancar un sistema operativo invitado completo propio.

Del monolito a los microservicios

Un monolito es un solo código base, un solo despliegue, y por lo general una sola base de datos compartida, sin importar cuántas funciones distintas contenga. Una arquitectura de microservicios divide esa misma aplicación en un conjunto de servicios pequeños e independientemente desplegables, cada uno organizado alrededor de una capacidad de negocio (catálogo, carrito, checkout, cuentas de usuario) y comunicándose mediante APIs ligeras en vez de llamadas de función dentro del mismo proceso. Cada servicio típicamente es dueño de su propia base de datos en vez de compartir una, y eso es justo lo que lo hace desplegable y escalable por su cuenta.

Vuelve al escenario de la promoción relámpago. En un monolito, triplicar el tráfico de checkout te obliga a escalar todo el despliegue, catálogo y cuentas incluidos, porque hay una sola unidad desplegable que escalar. Dividido en microservicios, el servicio de checkout escala por su cuenta, y catálogo y cuentas se quedan exactamente igual, porque cada uno es ahora una pieza separada e independientemente desplegable.

La contrapartida que aceptas: independencia a cambio de coordinación

Nada de esto es gratis. Dividir un monolito en microservicios cambia la simplicidad de un solo código base por un conjunto de problemas nuevos que solo aparecen una vez que los servicios están separados. Una llamada de checkout al servicio de usuario ahora cruza una red en vez de llamar a una función dentro del mismo proceso, lo cual significa que puede agotar su tiempo de espera o fallar de formas que una llamada dentro del proceso nunca podría. Los datos descentralizados, cada servicio con su propia base de datos, significan renunciar a una sola transacción compartida en toda la solicitud a cambio de consistencia eventual entre servicios. Y una sola solicitud de cliente que toca 5 servicios es más difícil de depurar que una que toca uno solo, porque ya no hay un único stack trace que leer. Más servicios no es automáticamente mejor, es una contrapartida deliberada de simplicidad por independencia, una que solo paga una vez que el equipo tiene las herramientas operativas para manejarla, que es justo lo que cubre la siguiente lección.

Orquestación: manejar más contenedores de los que una persona puede rastrear

Corre un solo contenedor a mano y docker run es suficiente. Corre 3 réplicas de un servicio de checkout por redundancia, más un servicio de catálogo, más un servicio de carrito, cada uno con sus propias réplicas, y vigilarlos todos a mano deja de funcionar rápido. Un orquestador de contenedores como Kubernetes toma una descripción declarativa de lo que quieres y trabaja continuamente para que la realidad coincida con ella.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: checkout-service
  template:
    metadata:
      labels:
        app: checkout-service
    spec:
      containers:
        - name: checkout
          image: checkout-service:2.1
          ports:
            - containerPort: 8080

Esa línea replicas: 3 es una declaración, no una orden de una sola vez. Si una de las 3 se cae, Kubernetes nota el desajuste entre el estado declarado y la realidad e inicia un reemplazo automáticamente, una capacidad llamada autorreparación. Kubernetes también maneja el descubrimiento de servicios y el balanceo de carga entre esas réplicas, y despliega actualizaciones gradualmente en vez de todas a la vez, así que un equipo deja de cuidar contenedores a mano y empieza a describir el estado que quiere en su lugar.

Pistas de examen: identificar el encaje en un escenario

El escenario dice...Apunta hacia
"Empaquetar una vez, correr idéntico en todas partes"Contenedor
"Equipos independientes despliegan de forma independiente, cada uno es dueño de sus propios datos"Microservicios
"Reiniciar automáticamente instancias caídas, desplegar actualizaciones gradualmente"Orquestación de contenedores (Kubernetes)
"Un solo código base, un solo despliegue, una sola base de datos"Monolito

Dónde te deja esto

Dividir una aplicación en microservicios y empaquetar cada pieza como contenedor son 2 decisiones separadas que resultan reforzarse una a la otra: la capacidad de despliegue independiente que prometen los microservicios solo se sostiene si cada pieza puede en verdad enviarse y escalar por su cuenta, y un contenedor es justo lo que hace eso práctico. Nada de esto, los builds, los despliegues, la cantidad siempre creciente de piezas en movimiento, se mantiene manejable a mano por mucho tiempo. La siguiente lección cubre las prácticas y las herramientas, DevOps e infraestructura como código, que lo mantienen manejable.