Saltar al contenido principal
Jordi Izquierdo Montalbán
Lo que practico

Fundamentos

Despliegue y contenedores

Empaqueto en contenedores y despliego con una forma probada de volver atrás.

Qué es un buen despliegue

Desplegar es llevar un software desde el repositorio hasta el lugar donde lo usan personas reales. La señal de que se hace bien es que resulta aburrido: repetible, automatizado, observable y con una vuelta atrás que se ha probado antes de necesitarla. Casi todo lo que sigue persigue esa propiedad.

Hay dos ideas que organizan el resto. La primera: el artefacto es inmutable; no se parchea un servidor en marcha, se sustituye por uno nuevo construido desde cero. La segunda: la configuración se separa del artefacto, de modo que la misma imagen sirve en todos los entornos.

Contenedores: qué son y qué no son

Las máquinas virtuales llevan su propio sistema operativo; los contenedores comparten el núcleo.

Una máquina virtual emula un ordenador completo, con su propio núcleo, sobre un hipervisor. Un contenedor es un proceso normal del sistema operativo anfitrión al que se le restringe lo que ve y lo que puede consumir. Dos mecanismos del núcleo Linux lo hacen posible, a nivel conceptual:

  • Namespaces: dan al proceso su propia vista de los recursos: su árbol de procesos, su red, su sistema de ficheros, sus usuarios. Dentro del contenedor parece estar solo en el sistema.
  • cgroups (grupos de control): limitan y contabilizan cuánta CPU, memoria y disco puede usar.
AspectoMáquina virtualContenedor
AislamientoFuerte: núcleo propioMás débil: comparte el núcleo del anfitrión
ArranqueDecenas de segundos o minutosSegundos o menos
TamañoGigabytesDe megabytes a unos cientos de megabytes
DensidadPocas por servidorMuchos por servidor
Sistema operativo distinto del anfitriónSíNo (comparte el núcleo)
Caso típicoAislar inquilinos no confiables, otros sistemas operativosEmpaquetar y escalar aplicaciones propias

Imágenes y capas

Cada instrucción del Dockerfile añade una capa de solo lectura que se puede cachear y compartir.

Una imagen es una plantilla de solo lectura formada por capas apiladas. Cada instrucción del Dockerfile que modifica ficheros crea una capa. Un contenedor es una instancia en ejecución de una imagen, con una fina capa de escritura efímera encima.

Consecuencias prácticas:

  • Las capas se cachean y comparten: si no cambia una capa ni las anteriores, no se reconstruye ni se vuelve a descargar.
  • Por eso se ordena el Dockerfile de lo que menos cambia a lo que más cambia: sistema base, dependencias, código.
  • Borrar un fichero en una capa posterior no reduce el tamaño: sigue en la capa anterior. Hay que evitar crearlo, o usar multi-stage.
  • Todo lo escrito en la capa del contenedor se pierde al destruirlo. Los datos persistentes van en volúmenes o en servicios externos.

Un Dockerfile bien hecho

Se compila con las herramientas pesadas y solo se copia el resultado a la imagen final.

La construcción en varias etapas separa lo que se necesita para compilar de lo que se necesita para ejecutar. El ejemplo es una aplicación Node, pero la estructura se aplica a cualquier lenguaje.

# Etapa 1: dependencias y build
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
 
# Etapa 2: imagen final mínima
FROM node:22-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/package.json ./package.json
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD node dist/healthcheck.js
CMD ["node", "dist/server.js"]

El fichero .dockerignore evita enviar al build lo que no debe estar en la imagen:

.git
node_modules
.env
.env.*
dist
coverage
*.log
Dockerfile
docker-compose*.yml

Buenas prácticas

PrácticaPor qué
Imagen base mínima con versión fijada (o digest)Menos superficie de ataque y builds reproducibles
Varias etapasLa imagen final no lleva compiladores ni herramientas de desarrollo
Usuario sin privilegios (USER)Un compromiso del proceso no da root dentro del contenedor
Copiar primero los ficheros de dependenciasAprovecha la caché de capas
.dockerignoreBuilds más rápidos y sin secretos accidentales
Un proceso por contenedorSe escala, reinicia y supervisa de forma independiente
Forma exec en CMD (lista JSON)El proceso recibe las señales de parada
Sin secretos en la imagenLas capas se pueden inspeccionar
Escanear la imagen en el pipelineDetecta vulnerabilidades conocidas en la base y las dependencias
Sistema de ficheros de solo lectura cuando se puedaImpide modificar la imagen en ejecución

Docker Compose: varios servicios en un fichero

Compose describe una aplicación formada por varios contenedores, sus redes y sus volúmenes. Es ideal para entornos de desarrollo, pruebas de integración y despliegues pequeños en un único servidor.

services:
  web:
    image: registro.ejemplo.com/equipo/web:1.4.2
    ports:
      - "8080:3000"
    environment:
      DATABASE_URL: postgres://app@db:5432/app
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "node", "dist/healthcheck.js"]
      interval: 30s
      timeout: 3s
      retries: 3
 
  db:
    image: postgres:16
    volumes:
      - datos:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      retries: 5
 
volumes:
  datos:

Compose no sustituye a un orquestador: no reparte contenedores entre varias máquinas ni repone nodos caídos. Cuando se necesita alta disponibilidad real entre servidores, entra Kubernetes u otro orquestador.

Registros y etiquetado

El registro es el almacén de imágenes (Docker Hub, GitHub Container Registry, registros de los proveedores cloud o uno propio). Es el punto de encuentro entre el pipeline, que publica, y los entornos, que descargan.

  • Etiquetar con la versión semántica y con el hash corto del commit, para saber siempre qué código contiene.
  • Considerar las etiquetas como móviles por naturaleza: la única referencia inmutable es el digest (resumen criptográfico). Desplegar por digest garantiza que se ejecuta exactamente lo probado.
  • Evitar latest en producción: no dice qué versión es ni permite volver atrás con precisión.
  • Definir una política de retención para no acumular imágenes viejas, conservando las que estén desplegadas y las necesarias para volver atrás.
  • Usar registros privados con credenciales de solo lectura en los entornos y de escritura solo en el pipeline.

Orquestación: los conceptos de Kubernetes

El tráfico entra por el ingress, pasa por un service y llega a los pods que mantiene un deployment.

Un orquestador decide en qué máquina corre cada contenedor, lo repone si falla, lo escala y conecta los servicios entre sí. Kubernetes funciona de forma declarativa: se describe el estado deseado y un bucle de control trabaja continuamente para igualar el real a ese estado.

ObjetoFunción
PodLa unidad mínima: uno o varios contenedores que comparten red y almacenamiento. Es efímero
DeploymentMantiene un número de réplicas de un pod y gestiona las actualizaciones progresivas y la vuelta atrás
ServiceDirección y nombre estables delante de un conjunto de pods, con balanceo de carga
IngressEntrada del tráfico HTTP externo: enrutado por dominio o ruta y terminación TLS
ConfigMapConfiguración no sensible, separada de la imagen
SecretDatos sensibles; por defecto solo codificados, por lo que conviene cifrado en reposo y control de acceso estricto
NamespacePartición lógica del clúster para separar equipos o entornos
StatefulSet y volúmenes persistentesPara cargas con estado e identidad estable
Job y CronJobTareas puntuales y programadas
HorizontalPodAutoscalerAjusta las réplicas según la carga

Sondas (probes)

  • Liveness: indica si el proceso está atascado; si falla, se reinicia el contenedor.
  • Readiness: indica si puede recibir tráfico; si falla, se retira del servicio sin reiniciarlo.
  • Startup: da margen a las aplicaciones de arranque lento antes de aplicar las otras.

Además, conviene declarar peticiones y límites de recursos (CPU y memoria), un presupuesto de interrupciones (PodDisruptionBudget) para no quedarse sin réplicas durante mantenimientos, y apagado ordenado que atienda la señal de parada y termine las peticiones en curso.

Los doce factores

La metodología de las aplicaciones de doce factores resume cómo diseñar software que se despliega bien en entornos modernos.

FactorIdea
Código baseUn repositorio, muchos despliegues
DependenciasDeclaradas y aisladas, nunca implícitas
ConfiguraciónEn el entorno, no en el código
Servicios de apoyoBases de datos y colas como recursos enlazables por configuración
Build, release, runEtapas separadas; el release combina build y configuración
ProcesosSin estado y sin compartir nada; el estado vive en servicios externos
Vinculación de puertosLa app expone su propio servicio
ConcurrenciaSe escala añadiendo procesos
DesechabilidadArranque rápido y parada limpia
Paridad dev y prodEntornos lo más parecidos posible
LogsUn flujo de eventos a la salida estándar, no ficheros gestionados por la app
Procesos administrativosTareas puntuales (migraciones) con el mismo código y entorno

Configuración y secretos

La configuración es todo lo que varía entre entornos: URLs, credenciales, interruptores. Se inyecta al arrancar, por variables de entorno o ficheros montados, nunca dentro de la imagen.

  • Las variables de entorno son simples, pero pueden filtrarse en volcados y en herramientas de diagnóstico.
  • Los gestores de secretos (Vault, los de los proveedores cloud, secretos cifrados en Git con herramientas como SOPS) añaden control de acceso, auditoría y rotación.
  • Los secretos se rotan y se pueden revocar sin reconstruir la imagen.
  • Distinguir configuración (puede ser pública) de secreto (no).
  • Validar la configuración al arrancar y fallar de inmediato si falta algo, en lugar de descubrirlo con la primera petición.

Estrategias de despliegue

Todas intercambian velocidad, coste y riesgo de forma distinta.
EstrategiaCómo funcionaVentajaCompromiso
RecreateSe detiene la versión vieja y se arranca la nuevaSimple; sin dos versiones a la vezHay corte de servicio
RollingSe sustituyen las instancias poco a pocoSin corte, sin duplicar infraestructuraConviven dos versiones; la vuelta atrás es lenta
Blue-greenDos entornos completos; se conmuta el tráficoCambio y vuelta atrás casi instantáneosCoste de duplicar entorno; datos compartidos delicados
CanaryUna pequeña fracción del tráfico va a la versión nueva y se amplía si las métricas son buenasLimita el daño de un errorExige buenas métricas y enrutado fino
ShadowLa versión nueva recibe una copia del tráfico real, sin que su respuesta llegue al usuarioPrueba con carga real sin riesgo visibleHay que evitar efectos secundarios duplicados (pagos, correos)
Feature flagsEl código se despliega apagado y se activa por usuario o porcentajeSepara desplegar de liberar; apagado inmediatoLos flags se acumulan y hay que retirarlos
El tráfico se conmuta de un entorno al otro; la vuelta atrás es cambiar la conmutación.

La decisión depende de lo que cuesta fallar y de lo que cuesta protegerse. Cuanta más sofisticación, más infraestructura y observabilidad hacen falta para sacarle partido.

Migraciones de base de datos sin corte

El código se puede sustituir en segundos; el esquema de datos, no. Durante un despliegue progresivo conviven versiones distintas del código sobre la misma base de datos, así que el esquema tiene que servir a ambas.

El esquema cambia en pasos compatibles con la versión anterior y la nueva.

El patrón se llama expandir y contraer:

  1. Expandir: añadir lo nuevo (columna, tabla) de forma compatible con el código antiguo: opcional y sin quitar nada.
  2. Desplegar código que escribe en lo nuevo y en lo viejo, y lee de lo nuevo.
  3. Rellenar los datos históricos en lotes, sin bloquear tablas.
  4. Contraer: cuando ninguna versión desplegada usa lo antiguo, eliminarlo en un despliegue posterior.

Otras reglas: las migraciones van versionadas con el código; deben ser idempotentes; los cambios destructivos (borrar, renombrar) se dividen en varios pasos; se prueban contra una copia de datos realista; y se ejecutan como un paso aparte del arranque de la aplicación, no en cada réplica a la vez.

Vuelta atrás

La vuelta atrás es parte del diseño, no una improvisación. Para que sea posible:

  • Los artefactos anteriores siguen disponibles en el registro.
  • El despliegue de una versión anterior es un único comando o botón, ensayado.
  • Los cambios de esquema son compatibles hacia atrás (de ahí expandir y contraer).
  • Las banderas de funcionalidad permiten apagar un comportamiento sin desplegar.
  • Se decide de antemano qué señal dispara la vuelta atrás: tasa de errores, latencia o fallo de una prueba de humo.

Revertir suele ser más rápido y seguro que arreglar hacia delante bajo presión, salvo cuando hay datos ya transformados que el código antiguo no entiende.

Dónde alojar

Cuanto más abajo, menos se administra y menos control se tiene.
ModeloQué se gestionaCuándo encajaContrapartida
Bare metalTodo, hasta el hardwareRendimiento máximo o requisitos específicosMáxima responsabilidad operativa
VPSSistema operativo y todo lo de encimaProyectos pequeños, coste previsibleParches y seguridad a cargo propio
IaaSMáquinas virtuales, redes y discos bajo demandaControl con elasticidadMucha configuración
PaaSSolo la aplicación; la plataforma se ocupa del restoEquipos pequeños, velocidadMenos control; dependencia del proveedor
Contenedores gestionadosImágenes; el proveedor lleva el orquestadorEscalado sin operar el clústerCostes y límites del servicio
ServerlessFunciones; se paga por usoCargas irregulares o por eventosArranques en frío, límites de duración, dependencia del proveedor
EdgeCódigo ejecutado cerca del usuarioLatencia baja, lógica ligeraEntorno de ejecución restringido; datos distribuidos

No hay una opción mejor en abstracto: se elige según tamaño del equipo, patrón de carga, requisitos de cumplimiento y tolerancia a la dependencia de un proveedor.

Un servicio en un servidor con systemd

En un servidor sin contenedores, systemd supervisa los procesos, los reinicia si caen y los arranca con el sistema.

[Unit]
Description=Servicio web
After=network-online.target
Wants=network-online.target
 
[Service]
User=app
Group=app
WorkingDirectory=/opt/app
EnvironmentFile=/etc/app/entorno
ExecStart=/opt/app/bin/servidor
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/app
 
[Install]
WantedBy=multi-user.target

Con systemctl enable --now se activa y arranca, y journalctl -u da los logs. Las opciones de endurecimiento (ProtectSystem, NoNewPrivileges) restringen lo que el proceso puede tocar, con un coste de configuración muy bajo.

Infraestructura como código

La infraestructura se declara en ficheros, se revisa como código y se aplica de forma repetible.

La infraestructura (redes, servidores, bases de datos, permisos) se declara en ficheros versionados, se revisa en pull request y se aplica de forma automatizada.

HerramientaEnfoque
Terraform u OpenTofuDeclarativo: describe el estado final de los recursos; calcula un plan de cambios y lo aplica
PulumiDeclarativo, usando lenguajes de programación generales
AnsibleConfiguración de servidores por tareas idempotentes, sin agente, por SSH
Plantillas del proveedorEquivalentes propios de cada nube

Principios: revisar siempre el plan antes de aplicar; guardar el estado en un almacén remoto con bloqueo y cifrado, porque puede contener secretos; separar estados por entorno; detectar la deriva (cambios hechos a mano); y favorecer la inmutabilidad: ante un cambio, se crea un recurso nuevo y se descarta el viejo, en lugar de modificarlo en caliente. Un servidor al que cada persona ha ido haciendo retoques se vuelve irrepetible.

Copias de seguridad y recuperación ante desastres

Dos preguntas guían la estrategia de recuperación ante desastres.
  • RPO (objetivo de punto de recuperación): cuántos datos se pueden perder, medido en tiempo. Determina cada cuánto hay que copiar.
  • RTO (objetivo de tiempo de recuperación): cuánto se puede tardar en volver a operar. Determina cuánta automatización y redundancia hacen falta.

Reglas que se repiten en cualquier guía seria: la regla 3-2-1 (tres copias, en dos soportes distintos, una fuera del sitio); copias inmutables o aisladas, para que un ataque o un error no las borre también; cifrado de las copias; y, sobre todo, ensayar la restauración, porque una copia que nunca se ha restaurado es una hipótesis. Conviene documentar el procedimiento de recuperación y probarlo de forma periódica, incluido el caso de perder una región entera.

Aplicaciones móviles: la entrega a nivel conceptual

Desplegar una app móvil difiere de desplegar un servicio: el usuario controla cuándo actualiza y existe un intermediario, la tienda de aplicaciones.

  • Cada versión pasa una revisión de la tienda, con plazos que no controla el equipo.
  • Conviven versiones antiguas durante mucho tiempo, así que el servidor debe seguir sirviendo a clientes viejos: la API se versiona y se evita romper compatibilidad.
  • Se usan canales de distribución (pruebas internas, pruebas cerradas, lanzamiento por fases) para exponer una versión a un porcentaje creciente de usuarios.
  • Las banderas remotas y la configuración remota permiten cambiar comportamientos sin pasar por la tienda.
  • Las firmas y certificados son críticos: perder la clave de firma puede impedir actualizar la app.
  • Un mecanismo de actualización forzada permite retirar versiones con fallos graves o inseguras.

Lista de comprobación

  • Hay un único artefacto inmutable por versión, identificado por digest.
  • La imagen usa base mínima, multi-stage, usuario sin privilegios y .dockerignore.
  • La configuración y los secretos se inyectan al arrancar; no hay nada sensible en la imagen.
  • Hay sondas de liveness y readiness, y apagado ordenado.
  • Los recursos (CPU y memoria) están dimensionados y limitados.
  • La estrategia de despliegue encaja con el coste de un fallo.
  • Las migraciones son compatibles con la versión anterior y la nueva.
  • La vuelta atrás es un paso ensayado.
  • La infraestructura está en código, con plan revisado y estado remoto.
  • Las copias de seguridad tienen RPO y RTO definidos y la restauración se ha probado.

Antipatrones

  • Servidores copo de nieve: configurados a mano e irrepetibles.
  • Desplegar con latest o con etiquetas que cambian.
  • Contenedor que ejecuta como root sin necesidad.
  • Varias responsabilidades en un contenedor (aplicación, base de datos y cron juntos).
  • Datos importantes dentro del contenedor, sin volumen ni servicio externo.
  • Secretos en la imagen, en el repositorio o en el Dockerfile.
  • Migraciones destructivas en el mismo despliegue que el código que las necesita.
  • Orquestador por moda: usar Kubernetes para lo que cabe en un servidor.
  • Copias de seguridad sin restauración probada.
  • Entornos manuales no reproducibles, imposibles de recrear tras un desastre.
  • Sin sondas de salud: el balanceador envía tráfico a instancias que no están listas.