Las cuatro lecciones anteriores comparaban herramientas de CI/CD entre sí. Esta no, porque Docker y Kubernetes no son herramientas de CI/CD: son el sustrato. Docker aparece dos veces en cualquier pipeline moderno y con papeles completamente distintos —es el entorno donde corre el pipeline y es el formato del artefacto que se despliega—, y confundir esos dos papeles produce imágenes de producción de 1,2 GB que contienen el compilador, los tests y las credenciales del build. Kubernetes, por su parte, es el destino más común hoy y cambia la forma del CD: el modelo de push que usa Reservalia contra ECS tiene un equivalente natural en Kubernetes, pero la comunidad se movió mayoritariamente hacia GitOps, que invierte quién inicia el despliegue. El curso ha usado ambos desde el módulo 2 sin bajar al detalle; esta lección baja. Y termina con la pregunta que casi nunca se hace en voz alta: cuándo NO necesitas Kubernetes.
Contenido
- Los dos papeles de Docker en un pipeline
- BuildKit: cachés de montaje, multi-etapa y secretos
- Caché de capas en el registro y builds multi-plataforma
- Imágenes mínimas, no-root,
HEALTHCHECKy etiquetas OCI - Por qué el digest manda sobre el tag
- Construir imágenes dentro del pipeline: DinD, socket y constructores sin daemon
- Kubernetes como destino: los objetos mínimos
- Sondas y su papel en el rolling update
- Rollback:
kubectl rollout statusyundo - Parametrizar por entorno: Helm y Kustomize
- Push frente a pull: por qué en Kubernetes triunfa GitOps
- Reservalia en Kubernetes, contrastado con ECS
- Kubernetes como plataforma para los propios runners
- Cuándo NO necesitas Kubernetes
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Los dos papeles de Docker en un pipeline
flowchart LR
subgraph P["Papel A · entorno del pipeline"]
R1["Job calidad<br/>contenedor node:22"]
R2["Job test<br/>contenedor node:22 + postgres"]
R3["Job build<br/>contenedor con buildx"]
end
subgraph A["Papel B · formato del artefacto"]
IM["Imagen de runtime<br/>distroless, no-root, 90 MB"]
REG["Registro ECR<br/>identificada por digest"]
DEST["ECS / Kubernetes"]
end
R3 -->|"docker build --push"| IM --> REG --> DEST
| Papel A: entorno del pipeline | Papel B: artefacto desplegable | |
|---|---|---|
| Qué es | La imagen donde se ejecutan los steps | La imagen que se despliega en producción |
| Quién la elige | El image: del job, el container: o el executor |
Tu Dockerfile |
| Qué contiene | Compilador, gestor de paquetes, herramientas, CLIs | Solo lo necesario para ejecutar |
| Tamaño razonable | 500 MB - 1,5 GB, da igual | Lo más pequeño posible |
| Ciclo de vida | Muere con el job | Vive meses en producción |
| Criterio de seguridad | Aislamiento del job | Superficie de ataque mínima |
| Objetivo | Reproducibilidad del build | Reproducibilidad del despliegue + seguridad |
El papel A es el que da reproducibilidad al pipeline: un job que corre en node:22.11-bookworm se comporta igual hoy que dentro de seis meses, independientemente de qué tenga instalado la máquina que lo ejecuta. Es la respuesta al agente contaminado de la 06-01, y por eso todas las herramientas modernas lo usan por defecto.
El papel B es el artefacto inmutable de la 02-06. La confusión entre ambos produce el antipatrón más común de todo el tema: usar la misma imagen para construir y para ejecutar. El resultado es una imagen de producción que incluye el compilador de TypeScript, devDependencies, el historial de Git y, con suerte, ningún fichero de credenciales del build. La separación se hace con multi-etapa, que el curso introdujo en la 02-03 y que aquí llevamos al detalle.
- BuildKit: cachés de montaje, multi-etapa y secretos
BuildKit es el motor de construcción moderno (por defecto en versiones recientes de Docker y en buildx). Aporta tres cosas que cambian de verdad un pipeline: construcción paralela del grafo de etapas, cachés de montaje y secretos que no quedan en la imagen.
# syntax=docker/dockerfile:1.7 # 1 · habilita la sintaxis de BuildKit
# apps/api/Dockerfile — Reservalia
# ---------- etapa de dependencias ----------
FROM node:22.11-bookworm AS deps
WORKDIR /app
COPY package.json package-lock.json ./
COPY apps/api/package.json apps/api/
COPY packages/compartido/package.json packages/compartido/
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm ci --prefer-offline # 2 · caché persistente entre builds
# ---------- etapa de construcción ----------
FROM deps AS build
COPY . .
RUN --mount=type=cache,target=/app/.tsbuildinfo \
npm run build --workspace apps/api
RUN npm ci --omit=dev --prefer-offline # 3 · deja solo dependencias de producción
# ---------- etapa de runtime ----------
FROM gcr.io/distroless/nodejs22-debian12:nonroot AS runtime # 4
WORKDIR /app
COPY --from=build --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=build --chown=nonroot:nonroot /app/apps/api/dist ./dist
USER nonroot # 5
EXPOSE 3000
ENV NODE_ENV=production
LABEL org.opencontainers.image.source="https://github.com/reservalia/reservalia" \
org.opencontainers.image.revision="${GIT_SHA}" \
org.opencontainers.image.licenses="UNLICENSED" # 6
ENTRYPOINT ["/nodejs/bin/node", "dist/servidor.js"]- La línea
# syntax=no es un comentario: le dice a BuildKit qué versión del frontend usar, y es lo que habilita--mount. Sin ella, esas líneas fallan. --mount=type=cachees la mejora más rentable y la menos conocida. La caché de npm se monta durante elRUNy no queda en la imagen, pero persiste entre builds en el constructor. Diferencia respecto al truco clásico de "copiar solopackage.jsonprimero para aprovechar la caché de capas": este funciona aunque el lockfile cambie, porque no depende de invalidar una capa sino de tener los tarballs ya descargados.sharing=lockedevita corrupción con builds concurrentes.npm ci --omit=devdespués de construir es lo que evita llevardevDependenciesal runtime. Es un ahorro habitual de cientos de megabytes.- Imagen de runtime distinta y mínima: apartado 4.
USER nonroot: apartado 4.- Etiquetas OCI: apartado 4.
Y los secretos en tiempo de construcción, que es donde más gente se equivoca:
# MAL: el token queda en el historial de la imagen para siempre
ARG NPM_TOKEN
RUN echo "//registro.example/:_authToken=${NPM_TOKEN}" > .npmrc && npm ci && rm .npmrc
# ↑ borrar el fichero después NO lo elimina: la capa anterior lo contiene
# BIEN: montaje de secreto, no persiste en ninguna capa
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci --prefer-offlineLa regla es la de la 04-03 con un mecanismo concreto: ARG y ENV con secretos son visibles con docker history para cualquiera que tenga la imagen. Borrar el fichero en un RUN posterior no sirve: las capas son inmutables y acumulativas. --mount=type=secret es la única forma correcta.
- Caché de capas en el registro y builds multi-plataforma
En un runner efímero no hay caché local de capas: cada build empieza de cero. La solución es guardar la caché en el registro, que es lo que el ci.yml de Reservalia ya hacía y aquí explicamos.
docker buildx build \
--file apps/api/Dockerfile \
--cache-from type=registry,ref=$ECR/reservalia/api:cache \
--cache-to type=registry,ref=$ECR/reservalia/api:cache,mode=max \
--tag $ECR/reservalia/api:$GIT_SHA \
--push .| Modo de caché | Qué guarda | Cuándo |
|---|---|---|
mode=min (por defecto) |
Solo las capas de la imagen final | Poco útil con multi-etapa: pierde las etapas intermedias |
mode=max |
Capas de todas las etapas, incluidas deps y build |
Lo que quieres casi siempre; ocupa más en el registro |
type=gha |
El almacén de caché de GitHub Actions | Cómodo en Actions; sujeto a sus límites de tamaño y desalojo |
type=inline |
La caché va dentro de la propia imagen | Simple; solo mode=min en la práctica |
Dos avisos. La caché en el registro también hay que limpiarla: es una etiqueta más que crece, y con mode=max crece bastante. Y una caché de capas envenenada es un vector real: si alguien con acceso de escritura al registro sustituye la caché, tus builds pueden incorporar capas ajenas. Los permisos de escritura sobre el repositorio de caché deben ser los mismos que sobre las imágenes (04-03).
Multi-plataforma con buildx, relevante desde que los runners ARM son más baratos y los portátiles de desarrollo son ARM:
docker buildx create --name multi --driver docker-container --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag $ECR/reservalia/api:$GIT_SHA \
--push .Esto produce una lista de manifiestos: un único tag que apunta a dos imágenes, y el destino descarga la que corresponde a su arquitectura. Contrapartidas honestas: si el runner es amd64, la variante arm64 se construye por emulación con QEMU y puede tardar entre tres y diez veces más; la alternativa es construir cada arquitectura en un runner nativo y unir los manifiestos con docker buildx imagetools create, que es más rápido y más complejo. Y si tu aplicación tiene dependencias con binarios nativos, hay que probar de verdad en las dos arquitecturas: compilar no es funcionar.
- Imágenes mínimas, no-root,
HEALTHCHECK y etiquetas OCI
HEALTHCHECK y etiquetas OCI| Base | Tamaño típico | Tiene shell | Superficie | Cuándo |
|---|---|---|---|---|
node:22 (Debian completa) |
~1,1 GB | Sí | Alta | Solo para construir |
node:22-slim |
~200 MB | Sí | Media | Runtime aceptable, fácil de depurar |
node:22-alpine |
~130 MB | Sí (ash) |
Baja | Runtime pequeño; ojo con musl frente a glibc |
gcr.io/distroless/nodejs22 |
~110 MB | No | Muy baja | Runtime de producción |
scratch |
0 | No | Mínima | Binarios estáticos (Go, Rust) |
La diferencia importante no es el tamaño, es qué hay dentro. Una imagen distroless no tiene shell, ni gestor de paquetes, ni curl, ni utilidades: si alguien consigue ejecución en el contenedor, no tiene con qué trabajar. También reduce drásticamente el ruido de los escáneres de vulnerabilidades: la mayoría de los CVE que aparecen en una imagen Debian completa están en paquetes que tu aplicación nunca usa, y ese ruido es lo que hace que los equipos dejen de mirar los informes de la 04-03.
Contrapartida real y hay que decirla: depurar en distroless es incómodo. No puedes hacer kubectl exec -it ... -- sh porque no hay sh. Las respuestas: contenedores efímeros de depuración (kubectl debug --image=busybox --target=api) que se adjuntan al Pod sin modificar la imagen, y buena observabilidad (03-06) para no necesitar entrar. Muchos equipos usan -slim en staging y distroless en producción, lo cual es un compromiso defendible aunque rompe ligeramente la regla de "el mismo artefacto en todos los entornos".
Usuario no-root: por defecto un contenedor corre como root, y aunque el aislamiento del contenedor limita el daño, un proceso root que escape tiene mucho más margen. En Kubernetes esto se refuerza con la política del Pod:
securityContext:
runAsNonRoot: true # el Pod no arranca si la imagen corre como root
runAsUser: 65532
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true # obliga a declarar volúmenes para lo escribible
capabilities: { drop: ["ALL"] }HEALTHCHECK en el Dockerfile es útil con Docker o Compose; en Kubernetes lo ignora el orquestador, que usa sus propias sondas (apartado 8). Ponerlo no sobra —documenta cómo se comprueba que el proceso está sano— pero no confíes en él como mecanismo en un clúster.
Etiquetas OCI: son metadatos estándar que hacen trazable la imagen. org.opencontainers.image.source conecta la imagen con su repositorio y revision con el commit exacto. La pregunta "¿de qué commit salió lo que está en producción?" —primera pregunta de cualquier incidente, según la 03-05— se responde con docker inspect en vez de con arqueología.
- Por qué el digest manda sobre el tag
Ya se estableció en la 02-06; aquí el mecanismo:
# Un tag es un puntero MUTABLE
docker push reservalia/api:v1.4.0 # apunta a sha256:aaa...
docker push reservalia/api:v1.4.0 # ahora apunta a sha256:bbb... y nadie se entera
# Un digest es el hash del manifiesto: INMUTABLE por construcción
docker pull reservalia/api@sha256:aaa1b2c3...Consecuencias prácticas que hay que interiorizar:
FROM node:22en tuDockerfileno es reproducible. Ese tag apunta a imágenes distintas con el tiempo. Para builds verdaderamente reproducibles:FROM node:22.11-bookworm@sha256:.... Contrapartida: hay que actualizarlo, y ahí entra la automatización de dependencias de la 04-02, que también renueva digests de imágenes base.- Desplegar por tag introduce una carrera: entre que el pipeline construye
:v1.4.0y el orquestador lo descarga, ese tag puede apuntar a otra cosa. Por eso elcd.ymlde Reservalia despliega por digest. - La promoción entre entornos es mover un digest, no reconstruir. Staging y producción ejecutan el mismo
sha256:exacto, y eso es lo que hace que "funcionaba en staging" signifique algo. - Los registros permiten tags inmutables (ECR: tag immutability), que impide sobrescribir un tag existente. Actívalo: cierra la puerta a un error humano con consecuencias graves.
- Construir imágenes dentro del pipeline: DinD, socket y constructores sin daemon
Construir una imagen desde un job que ya corre en un contenedor es un problema real con tres soluciones y tres perfiles de riesgo distintos. Es una de las decisiones de seguridad más importantes de un pipeline y casi siempre se toma por inercia.
| Enfoque | Cómo funciona | Riesgo | Rendimiento |
|---|---|---|---|
| Docker-in-Docker (DinD) | Un daemon Docker completo dentro del contenedor del job, en modo privilegiado | Alto: --privileged equivale a acceso al kernel del host; escapar del contenedor es factible |
Caché fría cada vez salvo montaje de volumen |
| Socket del host montado | Se monta /var/run/docker.sock del host |
Muy alto: acceso al daemon del host = root en el host, y visibilidad de los contenedores de otros jobs | Bueno: caché compartida |
| Kaniko | Construye en espacio de usuario, sin daemon | Bajo | Aceptable; caché en registro |
| Buildah | Constructor sin daemon, puede correr rootless | Bajo | Bueno |
| BuildKit rootless | Daemon de BuildKit sin privilegios | Bajo | Muy bueno; soporta --mount=type=cache |
El razonamiento sobre por qué esto importa: en un runner compartido, un job con socket del host montado puede listar y manipular los contenedores de otros jobs, incluidos los que tienen credenciales de producción en su entorno. No hace falta un atacante externo: basta un .gitlab-ci.yml o un workflow modificado en un PR. Es la escalada de privilegios más accesible en un CI mal configurado.
# Kaniko en un job de GitLab CI: sin daemon, sin privilegios
build:
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
script:
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/apps/api/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_SHA}"
--cache=true
--cache-repo "${CI_REGISTRY_IMAGE}/cache"
--image-name-with-digest-file /tmp/digest.txt # el digest, para promocionar
artifacts:
paths: [ /tmp/digest.txt ]# BuildKit rootless como contenedor secundario en un Pod de agente
containers:
- name: buildkit
image: moby/buildkit:v0.16.0-rootless
args: ["--addr", "unix:///run/user/1000/buildkit/buildkitd.sock", "--oci-worker-no-process-sandbox"]
securityContext:
runAsUser: 1000
seccompProfile: { type: Unconfined } # requisito de rootless, no privilegio totalRecomendación práctica: si el runner es compartido, no uses DinD privilegiado ni montes el socket del host. Kaniko o BuildKit rootless cubren el caso normal. DinD es aceptable en runners efímeros de un solo uso y de un solo equipo, donde el radio del compromiso es el propio job. Y en GitHub Actions con runners alojados el problema no se plantea igual, porque la máquina es efímera y de un solo job —lo que es, por cierto, una ventaja de diseño de los runners alojados que no siempre se aprecia—.
- Kubernetes como destino: los objetos mínimos
No hace falta saber Kubernetes entero para desplegar en él. Estos son los objetos que sí hay que entender:
flowchart TD
D["Deployment<br/>estado deseado: 4 replicas, imagen X"] --> RS["ReplicaSet<br/>uno por version"]
RS --> P1["Pod"]
RS --> P2["Pod"]
SVC["Service<br/>IP estable + balanceo"] --> P1
SVC --> P2
ING["Ingress<br/>enrutado HTTP externo"] --> SVC
CM["ConfigMap<br/>configuracion no sensible"] -.-> P1
SEC["Secret<br/>credenciales"] -.-> P1
| Objeto | Qué es | Equivalente mental en ECS |
|---|---|---|
| Pod | Una o varias contenedores que comparten red y ciclo de vida | Una tarea |
| ReplicaSet | Mantiene N Pods idénticos vivos | — (interno) |
| Deployment | Declara el estado deseado y gestiona el reemplazo creando ReplicaSets | Servicio ECS |
| Service | IP y DNS estables con balanceo hacia los Pods sanos | Target group |
| Ingress | Enrutado HTTP externo, TLS, hosts y rutas | ALB con reglas |
| ConfigMap | Configuración no sensible | Variables de entorno de la definición de tarea |
| Secret | Datos sensibles (codificados en base64, no cifrados por defecto) | Secrets Manager / SSM |
# k8s/deployment.yaml — Reservalia API
apiVersion: apps/v1
kind: Deployment
metadata:
name: reservalia-api
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # 1 · nunca bajar de la capacidad actual
maxSurge: 1 # 1 Pod extra durante el reemplazo
selector:
matchLabels: { app: reservalia-api }
template:
metadata:
labels: { app: reservalia-api }
spec:
securityContext:
runAsNonRoot: true
seccompProfile: { type: RuntimeDefault }
containers:
- name: api
# 2 · por digest, no por tag
image: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/reservalia/api@sha256:aaa1b2c3...
ports: [ { containerPort: 3000 } ]
envFrom:
- configMapRef: { name: reservalia-api-config }
- secretRef: { name: reservalia-api-secretos }
resources:
requests: { cpu: "250m", memory: "256Mi" } # 3
limits: { cpu: "1", memory: "512Mi" }
startupProbe: # 4
httpGet: { path: /salud/vivo, port: 3000 }
failureThreshold: 30
periodSeconds: 2
readinessProbe:
httpGet: { path: /salud/listo, port: 3000 }
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet: { path: /salud/vivo, port: 3000 }
periodSeconds: 10
failureThreshold: 3
lifecycle:
preStop: # 5
exec: { command: ["sleep", "5"] }
terminationGracePeriodSeconds: 30maxUnavailable: 0conmaxSurge: 1es el ajuste correcto para no perder capacidad durante el despliegue: primero se crea el Pod nuevo, se espera a que esté listo y solo entonces se retira uno viejo. ConmaxUnavailable: 1(el valor por defecto reparte de otra forma) puedes quedarte por debajo de la capacidad necesaria en hora punta.- Imagen por digest, por lo dicho en el apartado 5. Además, con digest no hace falta
imagePullPolicy: Always: el identificador ya es único. requestsylimitsno son opcionales. Sinrequests, el planificador no sabe qué reservar y coloca mal; sinlimits, un Pod con una fuga de memoria puede tumbar el nodo. Y un detalle que muerde: superar el límite de memoria mata el contenedor (OOMKilled) sin aviso, mientras que superar el de CPU solo lo ralentiza.- Sondas: apartado 8.
preStopcon una pausa corta resuelve una carrera real: cuando un Pod entra en terminación, Kubernetes le envíaSIGTERMa la vez que lo saca de los endpoints del Service, y esas dos cosas no están sincronizadas. Sin la pausa, algunas peticiones llegan a un Pod que ya está cerrando. Cinco segundos eliminan la mayoría de los 502 durante los despliegues.
- Sondas y su papel en el rolling update
Las tres sondas se confunden constantemente y sus efectos son muy distintos:
| Sonda | Pregunta que responde | Si falla | Efecto en el rolling update |
|---|---|---|---|
| startupProbe | ¿Ha terminado de arrancar? | Reinicia el contenedor | Suspende las otras dos mientras corre: protege arranques lentos |
| readinessProbe | ¿Puede atender tráfico ahora? | Lo saca del Service, sin reiniciar | Decisiva: el rollout no avanza hasta que el Pod nuevo está listo |
| livenessProbe | ¿Sigue vivo o está colgado? | Reinicia el contenedor | Puede provocar reinicios en bucle si está mal configurada |
La sonda de disponibilidad es la que gobierna el despliegue y es la que hace que el rolling update de la 03-04 sea seguro: mientras el Pod nuevo no responda 200 en /salud/listo, Kubernetes no retira ningún Pod viejo. Si la aplicación arranca rota, el rollout se queda parado con la versión anterior sirviendo: el fallo se contiene solo.
Dos errores de configuración con consecuencias graves:
La sonda de vida que comprueba dependencias. Si /salud/vivo consulta la base de datos, una caída de la base de datos hace fallar la sonda en todos los Pods, Kubernetes los reinicia todos a la vez, y a una incidencia de base de datos le sumas una caída total de la aplicación con reinicios en bucle. Regla: vida = "el proceso responde"; disponibilidad = "puedo atender peticiones, incluidas mis dependencias".
Sonda de vida sin sonda de arranque en aplicaciones lentas. Si la aplicación tarda 40 s en arrancar y la sonda de vida empieza a los 10 s con umbral de 3 fallos, el contenedor se reinicia antes de terminar de arrancar, para siempre. La startupProbe existe exactamente para esto.
- Rollback:
kubectl rollout status y undo
kubectl rollout status y undo# Desplegar: cambiar la imagen del Deployment (por digest)
kubectl set image deployment/reservalia-api \
api=$ECR/reservalia/api@sha256:aaa1b2c3... --record
# Esperar y FALLAR si no converge: esto es lo que hace que el job de CD sea honesto
kubectl rollout status deployment/reservalia-api --timeout=5m
# Historial y vuelta atrás
kubectl rollout history deployment/reservalia-api
kubectl rollout undo deployment/reservalia-api # a la revisión anterior
kubectl rollout undo deployment/reservalia-api --to-revision=7kubectl rollout status con --timeout es la línea que convierte un despliegue en una verificación: si los Pods nuevos no llegan a estar listos, el comando devuelve error y el job de CD falla, en lugar de dar verde y dejar un rollout atascado que nadie mira. Es exactamente lo que la 03-02 pedía del despliegue idempotente y verificado.
Y el rollback aquí es más rápido que en ECS: kubectl rollout undo restaura el ReplicaSet anterior, cuyas imágenes ya están descargadas en los nodos, así que en segundos hay Pods viejos sirviendo. Los 4 minutos de rollback por digest de Reservalia (03-05) bajarían bastante.
Dos matices honestos. undo revierte la plantilla del Pod, no el estado del mundo: si el despliegue incluyó una migración de base de datos, revertir la imagen no revierte la migración, y ahí sigue mandando expand and contract (04-06). Y --record está en desuso; en un flujo GitOps el historial real está en Git, que es mejor sitio que las anotaciones del objeto.
- Parametrizar por entorno: Helm y Kustomize
El mismo Deployment tiene que existir en dev, staging y producción con distinto número de réplicas, distintos recursos y distinta configuración. Dos enfoques dominantes.
Helm — plantillas con variables:
# helm/reservalia-api/templates/deployment.yaml
spec:
replicas: {{ .Values.replicas }}
template:
spec:
containers:
- name: api
image: "{{ .Values.imagen.repositorio }}@{{ .Values.imagen.digest }}"
resources:
{{- toYaml .Values.recursos | nindent 12 }}# helm/reservalia-api/values-produccion.yaml
replicas: 6
recursos:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "2", memory: "1Gi" }helm upgrade --install reservalia-api ./helm/reservalia-api \
--namespace produccion \
--values helm/reservalia-api/values-produccion.yaml \
--set imagen.digest="sha256:aaa1b2c3..." \
--atomic \ # si falla, revierte automáticamente al estado anterior
--wait \ # espera a que los recursos estén listos
--timeout 10m--atomic --wait es la combinación que convierte helm upgrade en un despliegue con rollback automático incorporado: si los Pods no llegan a estar listos en el plazo, Helm deshace el cambio. Es la línea más valiosa del comando.
Kustomize — superposiciones sobre una base, sin plantillas:
# k8s/overlays/produccion/kustomization.yaml
resources: [ ../../base ]
namespace: produccion
replicas:
- name: reservalia-api
count: 6
images:
- name: reservalia/api
digest: sha256:aaa1b2c3...
patches:
- path: recursos.yaml
target: { kind: Deployment, name: reservalia-api }
configMapGenerator:
- name: reservalia-api-config
literals: [ "LOG_LEVEL=info", "ENTORNO=produccion" ]| Helm | Kustomize | |
|---|---|---|
| Mecanismo | Plantillas Go + valores | Parcheo declarativo de YAML |
| Los ficheros base son… | Plantillas: no son YAML válido | YAML válido y aplicable tal cual |
| Curva de aprendizaje | Media-alta (funciones, nindent, condicionales) |
Baja |
| Distribuir a terceros | Excelente: repositorios de charts, versionado | Pobre |
| Instalar software de terceros | Estándar de facto | Poco práctico |
| Gestión de estado | Guarda releases e historial; helm rollback |
Ninguna: el estado es lo que hay en el clúster |
| Legibilidad al crecer | Empeora: lógica en plantillas | Buena, hasta que hay overlays de overlays |
Integrado en kubectl |
No | Sí (-k) |
En la práctica, muchos equipos usan los dos: Helm para instalar software de terceros (ingress, monitorización, operadores) y Kustomize para sus propias aplicaciones. Es una combinación defendible y muy común. El antipatrón a evitar es un chart propio con tantas condicionales que hay que leer plantillas Go para saber qué se despliega.
- Push frente a pull: por qué en Kubernetes triunfa GitOps
El cd.yml de Reservalia usa push: el pipeline tiene credenciales del entorno y ejecuta el despliegue.
flowchart LR
subgraph PUSH["Modelo push · el cd.yml de Reservalia"]
CI1["Pipeline"] -->|"credenciales de prod"| K1["Cluster / ECS"]
end
subgraph PULL["Modelo pull · GitOps"]
CI2["Pipeline"] -->|"commit: nuevo digest"| G["Repositorio de despliegues"]
AG["Agente en el cluster<br/>Argo CD / Flux"] -->|"lee cada 3 min"| G
AG -->|"aplica desde dentro"| K2["Cluster"]
end
| Push | Pull (GitOps) | |
|---|---|---|
| Quién inicia | El pipeline | Un agente dentro del clúster |
| Credenciales del clúster | En el CI | Nunca salen del clúster |
| Estado deseado | Implícito en el último despliegue | Explícito en Git |
| Deriva de configuración | Invisible | Detectada y corregida |
| Rollback | Volver a ejecutar con el digest anterior | git revert |
| Auditoría | Logs del pipeline | Historial de Git |
| Multi-clúster | Un job por clúster | Un agente por clúster, misma fuente |
| Complejidad | Baja | Media: otro componente que operar |
| Latencia del despliegue | Inmediata | Segundos a minutos (o inmediata con webhook) |
Las dos razones por las que GitOps ganó en Kubernetes, y ninguna es moda:
1. Las credenciales de administrador de un clúster son demasiado poderosas para dejarlas en el CI. kubectl apply requiere permisos amplios; si el CI los tiene, comprometer el CI es comprometer el clúster. Con GitOps el pipeline solo necesita permiso para hacer commit en un repositorio, que es un privilegio mucho menor.
2. Kubernetes es declarativo, y eso encaja de forma natural con Git. El estado deseado es un conjunto de manifiestos; ponerlos en Git y que un agente los reconcilie continuamente es la extensión obvia. Y trae una capacidad que el push no tiene: detección de deriva. Si alguien hace kubectl edit a las tres de la mañana durante una incidencia, el agente lo detecta y lo revierte o lo señala. El "¿por qué producción no es lo que dice el repositorio?" deja de existir.
# El pipeline en GitOps: no despliega, escribe el estado deseado
actualizar-manifiestos:
needs: [publicar]
script:
- git clone https://github.com/reservalia/despliegues.git && cd despliegues
- |
cd overlays/produccion
kustomize edit set image reservalia/api@${DIGEST}
- git commit -am "api: ${DIGEST} (desde ${CI_COMMIT_SHA})"
- git push
# Aquí termina el pipeline. Argo CD detecta el commit y aplica.# La Application de Argo CD, que vive en el clúster
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata: { name: reservalia-api-produccion }
spec:
source:
repoURL: https://github.com/reservalia/despliegues.git
path: overlays/produccion
targetRevision: main
destination: { server: https://kubernetes.default.svc, namespace: produccion }
syncPolicy:
automated: { prune: true, selfHeal: true } # selfHeal: corrige la deriva
retry: { limit: 3 }Contrapartidas honestas de GitOps: hay un componente más que operar y actualizar; el despliegue deja de ser inmediato salvo que configures un webhook; el rastro de un cambio atraviesa dos repositorios, lo que complica responder "¿qué commit de la aplicación está en producción?" si no se automatiza bien; y no resuelve las estrategias de despliegue por sí solo —para canary o blue-green hacen falta Argo Rollouts o Flagger, que es otra pieza más—. La 05-03 ya lo introdujo en el contexto de microservicios; aquí queda el porqué.
- Reservalia en Kubernetes, contrastado con ECS
Ejercicio de contraste, no de migración: Reservalia funciona bien en ECS y la 06-07 dirá por qué eso importa.
| Aspecto | ECS Fargate (hoy) | Kubernetes (hipotético) |
|---|---|---|
| Definición del servicio | Task definition (JSON) + Service | Deployment + Service + Ingress |
| Quién opera el plano de control | AWS, invisible | AWS (EKS) o tú; visible en cualquier caso |
| Rolling update | Nativo del servicio ECS | Nativo del Deployment |
| Canary al 10 % | Pesos en el listener del ALB | Argo Rollouts / Flagger / mallas de servicio |
| Rollback | Volver a desplegar el digest anterior: ~4 min | kubectl rollout undo: segundos |
| Configuración | SSM Parameter Store en la task definition | ConfigMap + Secret (o operador de secretos externos) |
| Autoescalado | Application Auto Scaling | HPA (+ Karpenter o Cluster Autoscaler para nodos) |
| Coste de operación | Bajo | Medio-alto: actualizaciones del clúster, complementos, CRDs |
| Conocimiento necesario en el equipo | Bajo | Alto |
| Portabilidad entre nubes | Nula | Alta (con matices: los complementos suelen ser específicos) |
Lo que Reservalia ganaría: rollback casi instantáneo, portabilidad, un ecosistema enorme de operadores, mejores herramientas de despliegue progresivo, y GitOps con la reducción de privilegios del CI que conlleva.
Lo que pagaría: un clúster que hay que actualizar varias veces al año, complementos que hay que mantener (ingress, gestor de certificados, escalado, observabilidad), la operación de Argo CD, y una curva de aprendizaje real para un equipo de tres personas. Diego preguntaría cuánto cuesta y tendría razón: con 340 negocios y ~9.000 citas al mes, la respuesta honesta es que el rollback de 4 minutos que ya tienen no es el cuello de botella de nada.
Cuándo cambiaría la respuesta: si aparecieran los cinco servicios de la 05-03 con equipos independientes, si hubiera un requisito de multi-nube, o si el equipo de plataforma creciera lo bastante para que operar el clúster no salga del tiempo de producto.
- Kubernetes como plataforma para los propios runners
Un uso que a veces se olvida: el clúster puede alojar los agentes de CI, no solo la aplicación. Ya apareció con el plugin de Kubernetes de Jenkins (06-01) y con el executor kubernetes de GitLab (06-02); en GitHub Actions es ARC (Actions Runner Controller), que la 06-06 detalla.
# Un agente efímero: se crea al empezar el job, se destruye al terminar
apiVersion: v1
kind: Pod
spec:
restartPolicy: Never
containers:
- name: runner
image: node:22-bookworm
resources:
requests: { cpu: "1", memory: "2Gi" }
limits: { cpu: "2", memory: "4Gi" }
- name: buildkit
image: moby/buildkit:v0.16.0-rootless
securityContext: { runAsUser: 1000 }Qué aporta: entorno limpio por job —adiós al agente contaminado—, autoescalado gratis (lo hace el clúster), aprovechamiento de capacidad ociosa, y aislamiento por espacio de nombres entre equipos.
Qué cuesta, con honestidad: un Pod tarda decenas de segundos en arrancar y descargar imágenes, lo que empeora el tiempo de cola si no se cuida (imágenes precargadas en los nodos, o un pequeño grupo de Pods calientes); la caché ya no persiste por definición, así que hay que apoyarse en cachés remotas; y sigue siendo un clúster que operar. Nunca ejecutes agentes de CI en el mismo clúster que producción sin aislamiento fuerte: un job es código arbitrario, y compartir nodos con producción convierte cualquier escape en un incidente grave.
- Cuándo NO necesitas Kubernetes
Este apartado es tan importante como los anteriores y se omite demasiadas veces.
Probablemente no lo necesitas si:
- Tienes menos de cinco servicios. Kubernetes resuelve la coordinación de muchos servicios; con pocos, aporta sobre todo complejidad. Reservalia es el ejemplo.
- Tu equipo no tiene a alguien que sepa operarlo. Un clúster mal operado es peor que ECS o que máquinas virtuales bien operadas. Y "sabe usar
kubectl" no es lo mismo que "sabe operar un clúster": actualizar versiones, gestionar CRDs, diagnosticar red y almacenamiento, revisar políticas de seguridad. - Tu carga es predecible y modesta. El autoescalado fino de Kubernetes brilla con picos; con carga estable, un servicio gestionado hace lo mismo con menos piezas.
- Ya tienes un servicio gestionado que funciona (ECS, Cloud Run, App Runner, App Service). Migrar cuesta meses y hay que poder nombrar qué problema concreto resuelve.
- La razón principal es "portabilidad entre nubes". Es real pero suele ser teórica: el clúster es portable, pero la base de datos gestionada, el balanceador, el almacenamiento de objetos, la gestión de identidades y los complementos específicos no lo son. La portabilidad que se consigue es parcial y se paga cara.
Sí lo necesitas cuando: tienes muchos servicios y varios equipos que necesitan autonomía (05-03); necesitas despliegue progresivo sofisticado y capacidades que solo existen en ese ecosistema; tu carga es muy variable y el empaquetado eficiente de contenedores ahorra dinero de verdad; tienes requisitos de on-premise o multi-nube reales y presupuestados; o ya tienes la plataforma y el equipo, en cuyo caso el coste marginal de una aplicación más es bajo.
La regla, que es la misma que cerró el módulo 5: la complejidad se justifica con un problema concreto que resuelve, no con el hecho de que otros la usen.
Errores Comunes y Consejos
Usar la misma imagen para construir y para ejecutar. Produce imágenes enormes con compiladores y credenciales. Multi-etapa siempre.
Secretos con ARG o ENV. Quedan en el historial de la imagen aunque borres el fichero después. --mount=type=secret.
FROM node:22 sin fijar. El build no es reproducible. Fija versión y, en entornos sensibles, digest, con automatización que lo actualice.
mode=min en la caché de registro con multi-etapa. Pierde las etapas intermedias, que son justo las caras. mode=max.
Montar el socket de Docker del host en jobs de CI. Equivale a dar root del host a cualquier PR. Kaniko o BuildKit rootless.
Sonda de vida que comprueba la base de datos. Convierte una incidencia de base de datos en una caída total con reinicios en bucle. Vida = proceso; disponibilidad = dependencias.
Sin startupProbe en aplicaciones lentas. Bucle de reinicios permanente que parece un bug de la aplicación.
Sin requests ni limits. Planificación mala, y una fuga de memoria tumba el nodo. Recuerda que superar el límite de memoria mata el contenedor sin aviso.
Desplegar por tag en lugar de por digest. Introduce una carrera y rompe la garantía de "el mismo artefacto en todos los entornos".
kubectl apply sin kubectl rollout status --timeout. El job da verde con el rollout atascado. Verde falso (04-04) en su versión más cara.
Secrets de Kubernetes tratados como cifrados. Están en base64, no cifrados. Activa el cifrado en reposo de etcd y usa un operador de secretos externos.
Adoptar Kubernetes por defecto. Es la decisión de arquitectura más cara que se toma con menos análisis.
Ejercicios
Ejercicio 1. La imagen de apps/api pesa 1,3 GB, tarda 6 minutos en construirse en CI, el escáner reporta 180 CVE y el equipo descubrió un token de npm visible con docker history. Reescribe el Dockerfile explicando qué corrige cada cambio y qué mejora esperas en tamaño, tiempo y hallazgos.
Ejercicio 2. Reservalia despliega en Kubernetes con kubectl set image desde el cd.yml, con un kubeconfig de administrador guardado como secreto del CI. Diseña la migración a GitOps con Argo CD: estructura de repositorios, qué hace el pipeline después, qué permisos quedan en el CI, cómo se hace el rollback, y qué se pierde respecto al modelo actual.
Ejercicio 3. Una startup de cuatro personas con un monolito Node y una base de datos PostgreSQL, ~200 usuarios y crecimiento lento, propone migrar a Kubernetes "para estar preparados para escalar". Escribe la respuesta técnica: qué preguntas hay que hacer antes, qué costes reales tiene, qué alternativas hay y bajo qué condiciones cambiaría la recomendación.
Soluciones
Solución 1.
# syntax=docker/dockerfile:1.7
FROM node:22.11-bookworm AS deps
WORKDIR /app
COPY package.json package-lock.json ./
COPY apps/api/package.json apps/api/
COPY packages/compartido/package.json packages/compartido/
# El token va montado: NO queda en ninguna capa
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
--mount=type=cache,target=/root/.npm,sharing=locked \
npm ci --prefer-offline
FROM deps AS build
COPY . .
RUN npm run build --workspace apps/api
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm ci --omit=dev --prefer-offline
FROM gcr.io/distroless/nodejs22-debian12:nonroot AS runtime
WORKDIR /app
COPY --from=build --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=build --chown=nonroot:nonroot /app/apps/api/dist ./dist
USER nonroot
ENV NODE_ENV=production
EXPOSE 3000
LABEL org.opencontainers.image.source="https://github.com/reservalia/reservalia" \
org.opencontainers.image.revision="${GIT_SHA}"
ENTRYPOINT ["/nodejs/bin/node", "dist/servidor.js"]| Cambio | Qué corrige | Efecto esperado |
|---|---|---|
Etapa runtime distroless separada |
La imagen final llevaba compilador, devDependencies y fuentes |
1,3 GB → ~120 MB |
npm ci --omit=dev antes de copiar |
devDependencies en producción |
Parte del ahorro anterior |
--mount=type=secret para .npmrc |
Token visible en docker history |
Desaparece de la imagen |
--mount=type=cache para ~/.npm |
Descarga completa en cada build | 6 min → 2-3 min con caché caliente |
COPY de manifiestos antes que el código |
Cualquier cambio invalidaba la instalación | Capa de dependencias reutilizada entre commits |
Base distroless + USER nonroot |
180 CVE, mayoría en paquetes del sistema sin usar | ~180 → 5-15, y esos sí accionables |
| Etiquetas OCI | Sin trazabilidad imagen → commit | docker inspect responde "¿de dónde salió esto?" |
Acción imprescindible que no está en el Dockerfile: rotar el token de npm. Estuvo en el historial de todas las imágenes publicadas y cualquiera con acceso al registro pudo leerlo. Cambiar el Dockerfile evita repetirlo; no repara lo ocurrido. Es exactamente la lección de la 06-04, y la respuesta de fondo es la misma: si el token puede sustituirse por publicación federada, mejor que rotarlo.
Advertencia sobre distroless: no hay shell, así que el equipo necesita kubectl debug y buena observabilidad antes de adoptarlo, o el primer incidente será desagradable.
Solución 2.
Estructura de repositorios — dos, deliberadamente separados:
reservalia/reservalia → código de la aplicación, CI, Dockerfile
reservalia/despliegues → estado deseado del clúster
base/
deployment.yaml service.yaml ingress.yaml kustomization.yaml
overlays/
staging/ kustomization.yaml recursos.yaml
produccion/ kustomization.yaml recursos.yamlSe separan porque tienen ciclos de vida y permisos distintos: en despliegues se pueden exigir revisores del equipo de plataforma para overlays/produccion sin bloquear el desarrollo diario.
Qué hace el pipeline después:
promocionar:
needs: [publicar]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
with:
repository: reservalia/despliegues
token: ${{ secrets.TOKEN_DESPLIEGUES }} # único permiso que queda
- run: |
cd overlays/staging
kustomize edit set image reservalia/api=$ECR/reservalia/api@${{ needs.publicar.outputs.digest }}
git commit -am "staging: api ${{ github.sha }}" && git pushProducción se promociona con un PR desde overlays/staging a overlays/produccion —revisado y aprobado—, que sustituye a la aprobación del Environment y deja el mismo rastro de auditoría, pero en Git.
Permisos que quedan en el CI: solo un token con permiso de escritura sobre el repositorio despliegues. El kubeconfig de administrador desaparece del CI, que es la ganancia principal: comprometer el CI ya no equivale a comprometer el clúster, solo a poder proponer un cambio que además pasa por revisión en producción.
Rollback: git revert del commit en despliegues y Argo CD reconcilia en el intervalo de sondeo (o de inmediato con webhook). Ventaja añadida: el rollback queda registrado como un commit con autor y motivo, en vez de como un job relanzado que nadie recuerda. Para emergencias se conserva kubectl rollout undo como salida manual, con selfHeal desactivado temporalmente si hace falta —y con la disciplina de que después hay que arreglar Git, o el agente revertirá tu arreglo—.
Qué se pierde: la inmediatez (segundos a minutos, salvo webhook); la simplicidad de un solo repositorio, y con ella la respuesta directa a "¿qué commit está en producción?", que ahora exige mirar dos historiales y por tanto conviene automatizar con una anotación en el manifiesto; un componente más que operar y actualizar; y una curva de aprendizaje para el equipo. La cuenta sale a favor sobre todo por el punto de las credenciales, no por elegancia.
Solución 3.
Preguntas antes de responder nada:
- ¿Qué problema concreto tenéis hoy? Si la respuesta es "ninguno, es por si acaso", no hay caso. Si es "los despliegues nos dan miedo" o "no sabemos escalar en picos", esos problemas tienen soluciones más baratas.
- ¿Cuál es la carga real y su variabilidad? Con 200 usuarios y crecimiento lento, el autoescalado no aporta.
- ¿Quién opera el clúster y qué deja de hacer mientras tanto? Con cuatro personas, es entre media y una persona equivalente.
- ¿Cuál es el objetivo de disponibilidad? Si no hay SLO escrito, no hay forma de justificar complejidad por disponibilidad.
- ¿Qué hay hoy y qué falla exactamente?
Costes reales, que casi nunca se contabilizan:
| Coste | Detalle |
|---|---|
| Plano de control | Coste fijo mensual (EKS/GKE/AKS) aunque no despliegues nada |
| Nodos infrautilizados | Un clúster mínimo con redundancia son varias máquinas siempre encendidas |
| Complementos | Ingress, certificados, monitorización, escalado: todos hay que instalar y actualizar |
| Actualizaciones | Varias versiones al año, con APIs que se retiran y manifiestos que hay que tocar |
| Aprendizaje | Semanas hasta operar con soltura; meses hasta diagnosticar bien |
| Coste de oportunidad | El más grande: lo que esas semanas no se dedicaron al producto |
Alternativas, ordenadas por lo que resuelven:
| Si el problema es… | Solución proporcionada |
|---|---|
| Despliegues manuales y con miedo | CI/CD sobre lo que ya hay: el módulo 3 entero |
| Entornos no reproducibles | Contenedores + IaC (03-03), sin orquestador complejo |
| Necesidad de escalar a veces | Servicio gestionado de contenedores: ECS Fargate, Cloud Run, App Runner |
| Caídas al desplegar | Rolling update con health checks, que esos servicios ya traen |
| "Queremos aprender Kubernetes" | Formación deliberada, no la producción de la empresa |
Recomendación: no migrar ahora. Contenerizar la aplicación —eso sí, porque da reproducibilidad y mantiene la puerta abierta: una aplicación en contenedor con IaC se mueve a Kubernetes más adelante en semanas, no en meses— y desplegar en un servicio gestionado. Escribir la decisión con la fecha y con las condiciones que la revisarían, para que no se replantee cada trimestre.
Condiciones bajo las que cambiaría: llegar a cinco o más servicios con equipos distintos; un requisito contractual de on-premise o multi-nube; una carga con picos de más de un orden de magnitud donde el empaquetado ahorre dinero medible; contratar a alguien con experiencia real de operación; o necesitar capacidades del ecosistema (operadores, despliegue progresivo avanzado) que el servicio gestionado no ofrezca.
Y el argumento que suele cerrar la conversación: "estar preparados para escalar" es la justificación más cara de la ingeniería. Con 200 usuarios, el riesgo real no es no poder escalar —eso se arregla cuando pasa, y con dinero—, es gastar tres meses del equipo en infraestructura mientras el producto no avanza. La preparación efectiva es contenerizar y tener infraestructura como código; el orquestador se elige cuando el problema exista.
Conclusión
Docker y Kubernetes no compiten con las herramientas de las lecciones anteriores: están debajo de ellas y delante de ellas. Docker aparece en dos papeles que hay que mantener separados —entorno reproducible del pipeline y artefacto inmutable que se despliega—, y la separación se materializa con multi-etapa, imágenes de runtime mínimas, usuario no-root y secretos montados que no dejan rastro en el historial. BuildKit añade lo que de verdad acelera un pipeline en runners efímeros: cachés de montaje que sobreviven a los cambios de lockfile y caché de capas en el registro con mode=max. Y la decisión de cómo construir imágenes dentro de un contenedor —DinD privilegiado, socket del host, o constructores sin daemon— es una decisión de seguridad de primer orden que se suele tomar por inercia.
En Kubernetes, lo que hay que dominar para desplegar es un conjunto pequeño: Deployment, Service, Ingress, ConfigMap, Secret, y sobre todo las sondas, porque la de disponibilidad es la que hace que el rolling update de la 03-04 sea seguro y la de vida mal configurada es la que convierte una incidencia en una caída total. kubectl rollout status --timeout es la línea que impide el verde falso, y undo da el rollback más rápido del curso. Para parametrizar por entorno, Helm y Kustomize resuelven lo mismo de forma distinta y conviven bien. Y GitOps ganó por una razón concreta y no por moda: saca las credenciales del clúster fuera del CI y convierte el estado deseado en algo explícito, versionado y con detección de deriva.
El apartado que conviene recordar más tiempo es el último: Kubernetes es la decisión de arquitectura más cara que se toma con menos análisis. Reservalia está mejor en ECS hoy, y saber justificar eso es tan valioso como saber escribir un Deployment.
La última herramienta del recorrido es la que el alumno lleva cinco módulos usando. GitHub Actions a fondo cierra los huecos que quedaron: contextos y expresiones, todos los disparadores que faltan y la diferencia crítica entre pull_request y pull_request_target, los permisos del GITHUB_TOKEN y OIDC, los tres tipos de action con el código completo de una escrita en JavaScript, matrices dinámicas, runners autoalojados con autoescalado, los límites reales del servicio y cómo depurarlo. Después, la 06-07 pondrá las seis herramientas en la misma tabla y dará los criterios para elegir.
Curso de CI/CD: Integración y Despliegue Continuo
Módulo 1: Introducción a CI/CD
- Conceptos Básicos de CI/CD
- Beneficios de CI/CD
- Herramientas Populares de CI/CD
- El Proyecto del Curso: la Aplicación que Vamos a Automatizar
- Métricas DORA: Cómo se Mide la Entrega de Software
Módulo 2: Integración Continua (CI)
- Introducción a la Integración Continua
- Configuración de un Entorno de CI
- Automatización de la Construcción
- Pruebas Automatizadas
- Calidad de Código y Análisis Estático
- Artefactos, Versionado y Promoción
- Integración con Control de Versiones
Módulo 3: Despliegue Continuo (CD)
- Introducción al Despliegue Continuo
- Automatización del Despliegue
- Infraestructura como Código y Entornos Reproducibles
- Estrategias de Despliegue
- Feature Flags, Rollback y Recuperación ante Fallos
- Monitoreo y Retroalimentación
Módulo 4: Prácticas Avanzadas de CI/CD
- Pipelines de CI/CD
- Gestión de Dependencias
- Seguridad en CI/CD
- Escalabilidad y Rendimiento
- Pipeline as Code: Plantillas, Reutilización y Pruebas del Pipeline
- Bases de Datos en el Pipeline: Migraciones Seguras
Módulo 5: Implementación de CI/CD en Proyectos Reales
- Caso de Estudio: Proyecto Web
- Caso de Estudio: Aplicación Móvil
- Caso de Estudio: Microservicios
- Caso de Estudio: Modernizar un Proyecto Legacy
Módulo 6: Herramientas y Tecnologías
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker y Kubernetes
- GitHub Actions a Fondo
- Comparativa y Criterios para Elegir Herramienta
Módulo 7: Ejercicios Prácticos
- Ejercicio 1: Configuración de un Pipeline Básico
- Ejercicio 2: Integración de Pruebas Automatizadas
- Ejercicio 3: Despliegue en un Entorno de Producción
- Ejercicio 4: Monitoreo y Retroalimentación
- Ejercicio 5: Endurecer el Pipeline con Seguridad y Secretos
- Proyecto Final: Pipeline Completo de Extremo a Extremo
