La lliçó anterior va acabar amb una pregunta incòmoda. Has construït un pipeline d'integració contínua que es dispara amb cada push, que etiqueta les imatges amb el SHA del commit i que publica a Artifact Registry tot el que passa les proves. I des de quin repositori es dispara exactament?
Perquè l'inventari real del codi d'AlpinaShop en aquest moment és el següent. El catàleg web és en un repositori Git local al portàtil d'en Dani, amb un remot que apunta a un servidor que l'empresa va deixar de mantenir fa dos anys; funciona perquè ningú no fa git push. Els startup scripts de les plantilles d'instància del MIG estan enganxats al camp de metadades de la plantilla, i enlloc més. Els manifestos de Kubernetes del namespace tienda són en una carpeta compartida de Drive anomenada "GKE definitiu (v3) FINAL". Els notebooks de la Lucía viuen a la seva instància de Workbench. I les comandes de gcloud amb què la Marta va crear la VPC, el balancejador i les regles de Cloud Armor no són enlloc, llevat de l'historial del seu terminal, que es trunca als 10.000 ordres.
Res del que es construeixi a sobre —ni CI/CD, ni infraestructura com a codi, ni revisió de canvis— és possible sense resoldre això primer. Aquesta lliçó ho resol, i ho fa amb una dosi gran d'honestedat sobre quina eina convé fer servir de debò el 2026.
Contingut
- Per què el control de versions és el fonament de tot
- Què és Cloud Source Repositories i com es fa servir
- La conversa honesta: CSR està en desús
- Connectar GitHub o GitLab amb GCP
- Workload Identity Federation: accedir a GCP sense claus JSON
- Taula de decisió: CSR, GitHub o GitLab
- Estratègia de branques per a un equip petit
- Què ha d'estar i què no ha d'estar al repositori
- L'estructura de repositoris d'AlpinaShop
- Revisió de codi i protecció de branques
- Missatges de commit i versionatge semàntic
- Ganxos de qualitat abans del commit
- Connectar els repositoris amb els activadors de Cloud Build
- Per què el control de versions és el fonament de tot
Val la pena dir explícitament què proporciona un sistema de control de versions, perquè quan es porten anys utilitzant-lo s'oblida que resol diversos problemes diferents alhora:
| Capacitat | Què permet | Què es trenca sense ella a AlpinaShop |
|---|---|---|
| Història | Saber què va canviar, quan i per què | Ningú no sap per què el timeout del catàleg és 45 s |
| Atribució | Saber qui va fer cada canvi | Davant d'una fallada, no hi ha a qui preguntar |
| Reversió | Tornar a un estat anterior conegut | L'única tornada enrere és reescriure a mà |
| Ramificació | Treballar en paral·lel sense trepitjar-se | En Dani i la Marta es passen fitxers per Slack |
| Revisió | Que una altra persona miri abans de fusionar | Ningú no revisa res |
| Identitat immutable | Un SHA identifica un estat exacte | El $COMMIT_SHA de 06-01 no existeix |
| Automatització | Reaccionar a esdeveniments del repositori | No hi ha activadors possibles |
Fixa't en les dues últimes files, perquè són les que connecten amb la lliçó anterior. El pipeline de Cloud Build etiqueta cada imatge amb $COMMIT_SHA, i aquella etiqueta és el que fa possible saber quin codi és en producció i tornar enrere. Sense repositori remot, no hi ha SHA, no hi ha activador i no hi ha pipeline. Tot el que s'ha construït a 06-01 depèn de resoldre això.
I hi ha un aspecte cultural que importa tant com el tècnic: el control de versions converteix el codi de propietat personal en propietat de l'equip. Mentre el catàleg sigui al portàtil d'en Dani, és d'en Dani —amb el que això implica quan se'n va de vacances, quan es posa malalt o quan canvia d'empresa—. Tan bon punt és en un repositori compartit amb història i revisió, és d'AlpinaShop.
- Què és Cloud Source Repositories i com es fa servir
Cloud Source Repositories (CSR) és el servei de repositoris Git privats allotjats a Google Cloud. Un repositori de CSR és un repositori Git normal i corrent: clone, commit, push, pull, branques i etiquetes funcionen exactament igual. El que hi afegeix és la seva integració amb la resta de GCP.
Crear i clonar un repositori:
# Crear el repositori dins del projecte de CI/CD
gcloud source repos create alpinashop-catalogo --project=alpinashop-cicd
# Clonar-lo: gcloud configura l'autenticació per tu
gcloud source repos clone alpinashop-catalogo --project=alpinashop-cicd
cd alpinashop-catalogo
# A partir d'aquí és Git normal
git add .
git commit -m "Importacio inicial del cataleg web"
git push origin mainEl gcloud source repos clone fa alguna cosa més que un git clone: configura un ajudant de credencials que utilitza la teva identitat de gcloud per autenticar-te. No hi ha contrasenyes ni claus SSH per gestionar; l'accés es controla amb IAM, igual que qualsevol altre recurs de GCP:
# En Dani pot llegir i escriure al repositori
gcloud projects add-iam-policy-binding alpinashop-cicd \
--member='group:[email protected]' \
--role=roles/source.writer
# L'equip de dades només llegeix
gcloud projects add-iam-policy-binding alpinashop-cicd \
--member='group:[email protected]' \
--role=roles/source.readerSi prefereixes autenticar-te sense gcloud —per exemple des d'una màquina que no el té— existeixen les credencials generades manualment i l'accés per SSH amb una clau registrada al teu perfil.
El que CSR aporta de debò, i que explica per què va existir:
- Integració nativa amb Cloud Build: els activadors de 06-01 funcionen sense connectar res extern ni instal·lar aplicacions de tercers.
- Els permisos són IAM: els mateixos grups, rols i condicions de 03-04 governen l'accés al codi. No hi ha un sistema de permisos paral·lel per mantenir sincronitzat.
- Cerca de codi a tots els repositoris del projecte des de la consola.
- Integració amb Error Reporting i Cloud Debugger: quan una excepció s'agrupa a Error Reporting, la consola pot mostrar la línia de codi concreta del repositori.
- Rèplica automàtica des de GitHub o Bitbucket, per tenir una còpia dins de GCP.
- Les dades no surten de la teva organització de GCP, cosa que en alguns contextos regulats simplifica una conversa amb el departament legal.
Tot això és real i funciona. I tot i així, no és el que AlpinaShop farà servir.
- La conversa honesta: CSR està en desús
Cal dir-ho amb claredat, perquè el nom d'aquesta lliçó porta el servei al títol i seria fàcil donar la impressió equivocada:
Cloud Source Repositories està en desús. Des del 2024 no s'habilita per a clients nous que no l'haguessin utilitzat prèviament, no rep funcionalitat nova i Google recomana explícitament utilitzar GitHub, GitLab o un altre proveïdor extern connectat a Cloud Build.
Els repositoris existents continuen funcionant i no hi ha una data d'apagada anunciada. Però no s'ha de començar un projecte nou amb CSR el 2026, i convé entendre per què va desaparèixer, perquè la raó és interessant i no és tècnica.
CSR allotjava Git perfectament. El que mai no va tenir va ser la resta: revisió de codi amb comentaris en línia, plantilles de pull request, discussions, gestió d'incidències, wiki, integració amb l'ecosistema d'eines que tothom fa servir. I resulta que el repositori no és el producte: el producte és el flux de col·laboració al voltant del repositori. Google va competir en la part fàcil —emmagatzemar objectes Git— contra plataformes que havien construït la part difícil.
Per això el patró dominant avui és: el codi a GitHub o GitLab, l'execució a GCP. I per això aquesta lliçó dedica la resta del seu espai a fer bé aquella connexió.
I per què continua valent la pena conèixer CSR? Per tres raons pràctiques: perquè te'l trobaràs en infraestructures heretades i l'hauràs d'operar o migrar; perquè el seu model de permisos basat en IAM continua sent una idea excel·lent que convé entendre; i perquè la rèplica des de GitHub continua sent útil quan algú exigeix una còpia del codi dins de l'organització de GCP.
Si et toca migrar de CSR a GitHub, el procediment és curt perquè Git és Git:
# Clon complet amb totes les branques i etiquetes
git clone --mirror https://source.developers.google.com/p/alpinashop-cicd/r/alpinashop-catalogo
cd alpinashop-catalogo.git
# Empènyer-ho tot a la destinació nova
git remote set-url origin [email protected]:alpinashop/alpinashop-catalogo.git
git push --mirror originEl que no migra aquella comanda: els permisos IAM (cal refer-los al proveïdor nou) i els activadors de Cloud Build, que cal recrear apuntant a la connexió nova.
- Connectar GitHub o GitLab amb GCP
AlpinaShop tria GitHub, i necessita que Cloud Build reaccioni als seus esdeveniments. La connexió ha evolucionat i el 2026 convé fer servir la forma moderna.
Existeixen dues generacions de connexió de repositoris, i la diferència importa:
| Aspecte | 1a generació (aplicació de GitHub) | 2a generació (Cloud Build repositories) |
|---|---|---|
| Mecanisme | Aplicació de GitHub instal·lada a l'organització | Recurs Connection + Repository gestionat |
| Gestió | Només per consola | Consola, gcloud i Terraform |
| Credencials | Gestionades per l'aplicació | Testimoni a Secret Manager, controlat per tu |
| Repositoris | S'enllacen un a un per consola | Enllaçables per API, en lot |
| Proveïdors | GitHub, GitHub Enterprise | GitHub, GitHub Enterprise, GitLab, Bitbucket |
| Recomanació | Heretat | La que cal fer servir |
La segona generació és la correcta perquè l'enllaç del repositori deixa de ser un clic irreproduïble en una consola i passa a ser un recurs declarable —cosa que encaixa amb tot el que arriba a 06-05 i 06-07—.
El procediment, pas a pas:
# 1. Habilitar les API necessàries
gcloud services enable cloudbuild.googleapis.com secretmanager.googleapis.com \
--project=alpinashop-cicd
# 2. Guardar el testimoni d'accés personal de GitHub a Secret Manager
# (amb permisos: repo, read:user, read:org — res més)
printf 'ghp_XXXXXXXXXXXXXXXXXXXX' | gcloud secrets create github-token-alpinashop \
--data-file=- --project=alpinashop-cicd
# 3. Donar accés al secret al compte de servei de Cloud Build
NUM=$(gcloud projects describe alpinashop-cicd --format='value(projectNumber)')
gcloud secrets add-iam-policy-binding github-token-alpinashop \
--member="serviceAccount:service-${NUM}@gcp-sa-cloudbuild.iam.gserviceaccount.com" \
--role=roles/secretmanager.secretAccessor --project=alpinashop-cicd
# 4. Crear la connexió (requereix una autorització interactiva al navegador)
gcloud builds connections create github alpinashop-github \
--region=europe-west1 --project=alpinashop-cicd
# 5. Enllaçar el repositori concret
gcloud builds repositories create alpinashop-catalogo \
--remote-uri=https://github.com/alpinashop/alpinashop-catalogo.git \
--connection=alpinashop-github \
--region=europe-west1 --project=alpinashop-cicdFixa't en el pas 2: el testimoni de GitHub va a Secret Manager, exactament igual que la contrasenya de la base de dades a 03-06. Un testimoni amb permís d'escriptura sobre tots els teus repositoris és una credencial de primer nivell, i mereix el mateix tractament: emmagatzematge xifrat, accés concedit a una identitat concreta i rotació periòdica. Anotar la data de caducitat del testimoni al calendari de l'equip evita el clàssic "el pipeline va deixar de funcionar el dimarts i ningú no sap per què".
Amb la connexió creada, un activador es defineix contra el recurs Repository:
gcloud builds triggers create github \
--name=catalogo-main \
--repository=projects/alpinashop-cicd/locations/europe-west1/connections/alpinashop-github/repositories/alpinashop-catalogo \
--branch-pattern='^main$' \
--build-config=cloudbuild.yaml \
--region=europe-west1 \
--service-account=projects/alpinashop-cicd/serviceAccounts/[email protected] \
--project=alpinashop-cicdAmb GitLab el procediment és anàleg: gcloud builds connections create gitlab, amb un testimoni d'accés de GitLab a Secret Manager. I per a GitLab autoallotjat darrere d'un tallafoc existeix la connexió mitjançant un agent de servei a la VPC, que és exactament el cas que cal quan el servidor de Git és intern.
- Workload Identity Federation: accedir a GCP sense claus JSON
Hi ha un segon sentit de la connexió, i és el que més problemes de seguretat causa: que les GitHub Actions accedeixin a recursos de GCP. AlpinaShop ho necessita perquè un workflow pugui publicar a Artifact Registry o consultar BigQuery sense dependre de Cloud Build.
La manera antiga de fer-ho, i per què és dolenta:
# NO FACIS AIXÒ
gcloud iam service-accounts keys create clave.json \
[email protected]
# ...i enganxar el contingut de clave.json en un secret de GitHubAquella clau JSON és una credencial permanent, sense caducitat i transferible. Qui l'obté és aquell compte de servei, per sempre, des de qualsevol lloc del món. No hi ha manera de saber si s'ha copiat. Es filtra en registres, en captures de pantalla, en commits accidentals, i la seva rotació és un procés manual que ningú no fa. Les claus JSON de compte de servei són, amb diferència, la causa més comuna d'incidents de seguretat greus a GCP.
Workload Identity Federation elimina el problema d'arrel. La idea, que connecta directament amb el que s'ha vist a 03-04:
GCP confia en l'emissor de testimonis de GitHub. Quan una GitHub Action s'executa, GitHub li lliura un testimoni OIDC de curta durada que diu "soc el workflow X del repositori Y de l'organització Z". GCP verifica aquell testimoni, comprova que coincideix amb una condició que tu has definit, i l'intercanvia per credencials temporals.
flowchart LR
A[GitHub Action<br/>en execució] -->|1. demana testimoni OIDC| B[Emissor OIDC<br/>de GitHub]
B -->|2. testimoni signat:<br/>repo, branca, workflow| A
A -->|3. presenta el testimoni| C[Workload Identity Pool<br/>a GCP]
C -->|4. verifica signatura<br/>i condició d'atributs| D{Coincideix?}
D -->|Sí| E[Credencial temporal<br/>del compte de servei]
D -->|No| F[Rebutjat]
E --> G[Accés a Artifact Registry,<br/>BigQuery, etc.]
La configuració completa:
PROY=alpinashop-cicd
NUM=$(gcloud projects describe $PROY --format='value(projectNumber)')
# 1. Crear el pool d'identitats de càrrega de treball
gcloud iam workload-identity-pools create github-pool \
--location=global --display-name="GitHub Actions" --project=$PROY
# 2. Crear el proveïdor OIDC, amb la condició d'atributs
gcloud iam workload-identity-pools providers create-oidc github-provider \
--location=global --workload-identity-pool=github-pool \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref" \
--attribute-condition="assertion.repository_owner == 'alpinashop'" \
--project=$PROY
# 3. Permetre que NOMÉS el repositori del catàleg suplanti el compte de servei
gcloud iam service-accounts add-iam-policy-binding \
sa-github-catalogo@${PROY}.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects/${NUM}/locations/global/workloadIdentityPools/github-pool/attribute.repository/alpinashop/alpinashop-catalogo" \
--project=$PROYEl pas 3 és el que cal entendre bé. El principalSet amb attribute.repository/alpinashop/alpinashop-catalogo significa que només els workflows d'aquell repositori concret poden obtenir credencials d'aquell compte de servei. Un workflow d'un altre repositori de la mateixa organització presenta un testimoni vàlid però amb un altre repository, no coincideix amb el principalSet, i és rebutjat.
Si a més vols restringir per branca —que només main pugui desplegar—, s'utilitza attribute.ref:
I així es consumeix des del workflow:
# .github/workflows/publicar.yml
name: Publicar imatge
on:
push:
branches: [main]
permissions:
contents: read
id-token: write # imprescindible: permet demanar el testimoni OIDC
jobs:
publicar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
service_account: '[email protected]'
# Fixa-t'hi: NO hi ha cap clau, cap secret, cap JSON
- uses: google-github-actions/setup-gcloud@v2
- run: |
gcloud auth configure-docker europe-west1-docker.pkg.dev --quiet
docker build -t europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:${GITHUB_SHA} .
docker push europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:${GITHUB_SHA}La línia id-token: write a permissions és obligatòria i s'oblida constantment; sense ella, GitHub no emet el testimoni OIDC i l'autenticació falla amb un missatge que no ajuda gaire.
| Aspecte | Clau JSON | Workload Identity Federation |
|---|---|---|
| Caducitat | Cap | Minuts |
| Si es filtra | Accés permanent | Inútil fora de context |
| Rotació | Manual, ningú no la fa | Automàtica per disseny |
| Àmbit | Qualsevol que la tingui | Només el repositori/branca declarat |
| Auditoria | "El compte X va fer alguna cosa" | Quin repositori i quin workflow |
| Feina de manteniment | Alta | Cap |
La regla pràctica: si estàs creant una clau JSON de compte de servei el 2026, gairebé sempre hi ha una alternativa millor. Dins de GCP, comptes de servei adjuntats al recurs. Fora de GCP, federació d'identitat. Les claus JSON queden per a casos residuals, i en aquells casos han de tenir caducitat, rotació i una organization policy que limiti on es poden crear —tema de 07-07—.
- Taula de decisió: CSR, GitHub o GitLab
La comparació sense adorns, que és el que AlpinaShop necessita per decidir:
| Criteri | Cloud Source Repositories | GitHub | GitLab |
|---|---|---|---|
| Estat | En desús, sense novetats | Actiu, líder de mercat | Actiu, fort en autoallotjat |
| Cost | Gratuït fins a un límit baix d'emmagatzematge i dades | Gratis en privat; de pagament per funcions avançades | Igual; edició comunitària autoallotjable gratis |
| Revisió de codi | Pràcticament inexistent | Pull requests madurs, suggeriments, propietaris de codi | Merge requests, molt complets |
| CI/CD propi | No, depèn de Cloud Build | GitHub Actions, ecosistema enorme | GitLab CI, molt potent i integrat |
| Integració amb GCP | Nativa, permisos IAM | Excel·lent (2a gen + federació) | Molt bona (2a gen + federació) |
| Ecosistema i plantilles | Nul | El més gran amb diferència | Ampli |
| Incidències i projectes | No | Sí | Sí, inclòs DevSecOps |
| Autoallotjat | No aplica | GitHub Enterprise Server | El seu punt fort |
| Dependència de proveïdor | Alta amb Google | Mitjana (Git és portable) | Baixa, es pot autoallotjar |
| Contractació de talent | Ningú no el coneix | Tothom el coneix | Molt conegut |
La decisió d'AlpinaShop és GitHub, per quatre raons ordenades per pes:
- La revisió de codi és l'objectiu principal. El problema d'AlpinaShop no és on guardar bytes: és que ningú no mira els canvis abans que arribin a producció. GitHub ho resol; CSR, no.
- Qualsevol persona que s'incorpori ja el sap fer servir. Amb un equip de tres persones tècniques, el temps de formació compta.
- La integració amb GCP és de primera classe gràcies a la 2a generació i a la federació d'identitat. El suposat avantatge de CSR ha deixat de ser-ho.
- La dependència és baixa de debò. Un repositori Git és portable:
git clone --mirrori ja ets en un altre lloc. El que sí que lliga són les incidències, les pull requests i els workflows d'Actions, i per això el pipeline principal d'AlpinaShop viu acloudbuild.yaml—un fitxer neutral— en lloc de a GitHub Actions.
Quan triaries GitLab en el seu lloc: si necessites autoallotjar per requisits reguladors, si vols un CI/CD integrat sense dependre de Cloud Build, o si valores tenir incidències, codi, CI i registre de contenidors en un únic producte. És una elecció perfectament defensable.
Quan continuaries amb CSR: només si ja el fas servir i funciona. Migrar per migrar no és una prioritat; migrar quan calgui revisió de codi de debò, sí.
- Estratègia de branques per a un equip petit
Amb el repositori decidit, la següent pregunta és com s'organitza la feina a dins. Hi ha dues escoles dominants.
GitFlow defineix branques de llarga vida: main (el que és en producció), develop (integració), feature/*, release/* i hotfix/*. Un canvi neix en una branca de característica, es fusiona a develop, entra en una branca de versió, s'estabilitza i per fi arriba a main.
Trunk-based defineix una única branca de llarga vida, main, sempre desplegable. Els canvis es fan en branques curtes —hores o un parell de dies com a molt— que es fusionen a main tan bon punt passen la revisió i la CI.
| Aspecte | GitFlow | Trunk-based |
|---|---|---|
| Branques de llarga vida | main i develop |
Només main |
| Vida d'una branca de treball | Dies o setmanes | Hores o un parell de dies |
| Dolor en fusionar | Alt: divergeixen molt | Baix: divergeixen poc |
| Encaixa amb CI | Malament: s'integra tard | És la premissa de la CI |
| Complexitat | Alta | Baixa |
| Idoni per a | Versions empaquetades, diversos mantinguts alhora | Serveis web amb desplegament continu |
| Funcions a mitges | Branca llarga sense fusionar | Feature flags |
AlpinaShop tria trunk-based, i les raons són concretes:
- És un servei web, no un producte empaquetat. No hi ha clients amb la versió 2.3 que necessitin un pedaç mentre es desenvolupa la 3.0. Només hi ha una versió: la que és a
alpinashop-prod. - L'equip són tres persones. La coordinació que justifica GitFlow no existeix quan cabeu en una trucada.
- És coherent amb la CI de 06-01. La "I" d'integració contínua significa integrar contínuament; una branca de tres setmanes és exactament el contrari, i el pipeline que vas construir només aporta valor si els canvis arriben a
mainsovint. - Els conflictes de fusió creixen de manera no lineal amb el temps. Dues branques d'un dia gairebé mai no xoquen; dues branques de tres setmanes sempre ho fan, i resoldre aquell conflicte és on s'esmunyen els errors.
El flux de treball diari resultant:
git switch main && git pull # 1. Partir sempre de main actualitzat
git switch -c feat/filtro-por-marca # 2. Branca curta i amb nom clar
# ... feina, commits petits ...
git push -u origin feat/filtro-por-marca # 3. Publicar i obrir la pull request
# 4. La CI de 06-01 s'executa sola sobre la PR
# 5. La Marta o en Dani revisen
# 6. Fusió amb squash a main → es desplega a alpinashop-dev
git switch main && git pull
git branch -d feat/filtro-por-marca # 7. Esborrar la brancaL'objecció evident: i si una funcionalitat triga tres setmanes? La resposta de trunk-based no és "mantén la branca tres setmanes", sinó fusionar codi incomplet però inactiu, protegit per un interruptor:
# catalogo/config.py — l'interruptor es llegeix de l'entorn, no del codi
NOU_CERCADOR = os.environ.get("FEATURE_NOU_CERCADOR", "false").lower() == "true"
# catalogo/rutas.py
@app.get("/buscar")
def cercar():
if NOU_CERCADOR:
return cercador_semantic(request.args["q"]) # 05-06, en proves
return cercador_classic(request.args["q"]) # el que funciona avuiEl codi nou arriba a main i a producció des del primer dia, desactivat. S'activa a alpinashop-dev per provar-lo, després per a un percentatge d'usuaris, i per fi per a tothom. I si alguna cosa va malament, desactivar-lo és canviar una variable d'entorn, no revertir un desplegament. És una reversió de segons.
La contrapartida honesta: els interruptors acumulen complexitat i branques mortes al codi. La disciplina associada és esborrar-los tan bon punt la funcionalitat està consolidada, amb una data de caducitat apuntada a la mateixa pull request que els introdueix.
- Què ha d'estar i què no ha d'estar al repositori
Una regla útil com a punt de partida: si és text que descriu com funciona o com es construeix el sistema, va al repositori.
| Sí que va al repositori | Per què |
|---|---|
| Codi de l'aplicació | Evident |
Dockerfile i .dockerignore |
La imatge ha de ser reproduïble des del codi |
cloudbuild.yaml i variants |
El pipeline es revisa com el codi (06-01) |
| Manifestos de Kubernetes | Avui són a Drive; allà no es revisen ni es versionen |
Terraform (.tf) |
La infraestructura com a codi arriba a 06-07 |
| Proves | Sense elles la CI no comprova res |
requirements.txt amb versions fixades |
Reproductibilitat de les dependències |
| Migracions de base de dades | Canvis d'esquema revisables i ordenats |
| Definicions de pipelines de dades i ML | Els components KFP de 05-07 |
README i decisions d'arquitectura |
DA-001, DA-002 i DA-003 han d'estar escrites |
| Configuració no sensible per entorn | dev.tfvars, prod.tfvars |
| No va al repositori | On va | Per què |
|---|---|---|
| Contrasenyes, testimonis, claus d'API | Secret Manager (03-06) | Git no oblida mai |
| Claus JSON de compte de servei | No haurien d'existir (apartat 5) | — |
| Certificats i claus privades | Secret Manager o KMS | Igual que a dalt |
Estat de Terraform (.tfstate) |
Bucket de Cloud Storage (06-07) | Conté valors sensibles |
| Dades de clients, bolcats de BD | Cloud Storage amb control d'accés | RGPD |
| Artefactes compilats, imatges | Artifact Registry | El repositori no és un magatzem binari |
| Fitxers grans (models, CSV) | Cloud Storage o Git LFS | Inflen el repositori per sempre |
.env amb valors reals |
Variables d'entorn o Secret Manager | Es filtra en fer git add . |
La frase clau sobre els secrets mereix subratllar-se: Git no oblida. Si puges una contrasenya i l'esborres al commit següent, continua sent a l'historial, a cada clon que algú hagi fet i a cada rèplica. Reescriure la història és dolorós i mai no és complet. L'única resposta correcta davant d'un secret filtrat és rotar-lo immediatament, i només després netejar l'historial si val la pena.
Un .gitignore sensat és la primera línia de defensa:
# Secrets i credencials
*.env
.env.*
*-key.json
*credentials*.json
*.pem
*.p12
# Estat de Terraform: MAI a Git
*.tfstate
*.tfstate.*
.terraform/
*.tfvars.secret
# Entorns i artefactes locals
venv/
__pycache__/
*.pyc
.pytest_cache/
dist/
build/
# Dades
*.csv
*.parquet
datos/Però la defensa real és automàtica, i és el tema de l'apartat 12.
- L'estructura de repositoris d'AlpinaShop
El debat monorepo davant de polirepo té molta literatura i poca resposta universal:
| Aspecte | Monorepo | Polirepo |
|---|---|---|
| Canvi que creua components | Una sola PR atòmica | Diverses PR coordinades |
| Permisos per component | Difícil d'acotar | Natural |
| CI | Necessita filtres per ruta | Simple |
| Descobriment de codi | Tot a la vista | Cal saber on mirar |
| Dependències entre parts | Fàcils de compartir, fàcils d'embolicar | Fronteres explícites |
| Eines necessàries | Més | Menys |
Per a un equip de tres persones, la complexitat d'un monorepo amb eines de construcció avançades no es justifica. Però quaranta repositoris diminuts tampoc. AlpinaShop es queda al punt intermedi: quatre repositoris agrupats per cicle de vida i per qui els toca.
| Repositori | Contingut | Qui el manté | Pipeline |
|---|---|---|---|
alpinashop-catalogo |
Flask, Dockerfile, manifestos de tienda, cloudbuild.yaml |
Dani | catalogo-main, catalogo-pr |
alpinashop-infra |
Terraform de la VPC, balancejador, IAM, Cloud SQL, buckets | Marta | plan en PR, apply amb aprovació |
alpinashop-datos |
Pipelines de Dataflow, consultes SQL, DAG/Workflows de 04-06 | Lucía | Proves + desplegament de plantilles |
alpinashop-ml |
Components KFP, codi d'entrenament, notebooks nets | Lucía i Dani | cloudbuild-ml.yaml de 06-01 |
El criteri de divisió no és tècnic, és de ritme i de responsabilitat. El catàleg canvia diverses vegades al dia i el toca en Dani. La infraestructura canvia un cop per setmana, requereix aprovació de la Marta i el seu pipeline és completament diferent —un plan que es revisa, no unes proves que passen—. Ficar-los junts obligaria a executar el pipeline d'infraestructura per cada canvi d'una plantilla HTML.
I un advertiment sobre el repositori d'infraestructura: és el més perillós dels quatre. Qui pot fusionar una PR allà pot modificar regles de tallafoc, polítiques d'IAM i bases de dades. La seva protecció de branques ha de ser la més estricta, i els seus revisors obligatoris han d'incloure sempre algú de gcp-seguridad@.
- Revisió de codi i protecció de branques
Que existeixi un repositori no impedeix que algú empenyi directament a main sense que ningú ho miri. Això ho impedeix la protecció de branques.
Les regles que AlpinaShop activa sobre main als quatre repositoris:
| Regla | Efecte | Per què |
|---|---|---|
| Prohibit el push directe | Tot canvi passa per pull request | Sense això, la resta és decoratiu |
| Almenys una aprovació | Una altra persona ha mirat el canvi | L'objectiu principal de la lliçó |
| La CI ha d'estar en verd | No es fusiona amb proves vermelles | Dona sentit al pipeline de 06-01 |
| Les aprovacions caduquen en haver-hi canvis nous | No s'aprova una versió i se'n fusiona una altra | Evita un truc molt utilitzat |
| Historial lineal | Fusió amb squash o rebase | Història llegible, revertir és trivial |
| Ningú no se salta les regles, ni els administradors | Sense excepcions | Les excepcions es tornen norma |
| Escaneig de secrets | Bloqueja el push si detecta credencials | Última xarxa abans del desastre |
Al repositori alpinashop-infra s'hi afegeix una regla més: revisor obligatori de l'equip de seguretat, mitjançant el fitxer CODEOWNERS:
# CODEOWNERS del repositori alpinashop-infra * @alpinashop/infraestructura /iam/ @alpinashop/seguridad @alpinashop/infraestructura /red/firewall.tf @alpinashop/seguridad @alpinashop/infraestructura /produccion/ @alpinashop/seguridad @alpinashop/infraestructura
Qualsevol PR que toqui IAM, regles de tallafoc o el directori de producció exigeix l'aprovació d'algú de gcp-seguridad@. És l'equivalent en el codi de la separació de funcions de 03-04.
Què mirar en una revisió, en ordre d'importància real: primer, és correcte? —fa el que diu, contempla els casos límit—; segon, és segur? —secrets, validació d'entrades, permisos que s'amplien—; tercer, està provat? —hi ha una prova que fallaria sense aquest canvi—; quart, s'entén? —ho llegirà algú d'aquí a un any—; i finalment, l'estil, que hauria d'estar automatitzat i no discutir-se a la revisió. Aquell últim punt estalvia una quantitat enorme de fricció: si el formatatge el decideix una eina, ningú no discuteix sobre comes.
I dues normes de convivència que valen més que qualsevol llista: pull requests petites —una PR de 40 línies rep comentaris útils, una de 2.000 rep un "lgtm"— i revisions ràpides, perquè una PR que espera dos dies bloqueja qui la va escriure i acumula conflictes.
- Missatges de commit i versionatge semàntic
Un historial de commits amb "canvis", "fix", "una altra vegada" i "ara sí" no serveix de res. Un historial bo respon a per què es va fer cada cosa, que és el que no es pot deduir del codi.
AlpinaShop adopta Conventional Commits, un format senzill amb un benefici gran:
<tipus>(<àmbit>): <resum en imperatiu, menys de 72 caràcters> <cos: PER QUÈ es va fer, no què es va fer — això ja és al diff> <peu: referències, canvis incompatibles>
feat(cercador): afegir filtre per marca al cataleg El 18 % de les cerques incloïen un nom de marca al text lliure, cosa que produïa resultats pobres. El filtre redueix la consulta a la marca abans d'aplicar el rànquing. Refs: #142
fix(comandes): reintentar la connexio a Cloud SQL despres d'una fallada de xarxa El pool de connexions no es recuperava després d'un tall breu i l'aplicació retornava 500 fins que es reiniciava el pod. S'afegeix reintent amb retrocés exponencial, coherent amb 04-04. Refs: #158
Els tipus habituals: feat (funcionalitat nova), fix (correcció), docs, refactor, test, chore (manteniment), perf i ci.
El benefici concret: el format és analitzable per màquines. D'allà surten automàticament el registre de canvis i la versió següent, segons versionatge semàntic (MAJOR.MENOR.PEDAÇ):
| Tipus de commit | Increment | Exemple |
|---|---|---|
fix: |
PEDAÇ | 2.4.1 → 2.4.2 |
feat: |
MENOR | 2.4.1 → 2.5.0 |
BREAKING CHANGE: al peu o feat!: |
MAJOR | 2.4.1 → 3.0.0 |
I aquí hi ha un matís honest que convé dir: el versionatge semàntic està pensat per a artefactes que altres consumeixen —biblioteques, API públiques—. Per al catàleg web d'AlpinaShop, que és un servei intern desplegat contínuament, la versió importa molt menys que el SHA del commit, que ja és un identificador exacte. AlpinaShop utilitza etiquetes semàntiques per marcar candidates a producció —és l'activador per etiqueta de 06-01— i per tenir noms pronunciables en una reunió, no com a mecanisme de compatibilitat.
- Ganxos de qualitat abans del commit
Una fallada detectada a la CI costa minuts d'espera. La mateixa fallada detectada abans de fer el commit costa segons. Els ganxos de pre-commit executen comprovacions ràpides en local abans que el commit es creï.
# .pre-commit-config.yaml, a l'arrel de cada repositori
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files # evita pujar models i CSV enormes
args: ['--maxkb=500']
- id: check-merge-conflict
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff # errors i estil
args: [--fix]
- id: ruff-format # formatatge automàtic
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0
hooks:
- id: gitleaks # EL MÉS IMPORTANT: busca secretspip install pre-commit
pre-commit install # instal·la el ganxo a .git/hooks
pre-commit run --all-files # primera passada sobre tot el repositoriEl ganxo de gitleaks és el que justifica tot el mecanisme per si sol. Busca patrons de credencials —claus d'API, testimonis, claus privades, claus JSON de GCP— i bloqueja el commit si en troba algun. Recorda el de l'apartat 8: Git no oblida. És infinitament més barat bloquejar el commit que rotar una clau de la passarel·la de pagament filtrada.
Dos advertiments sobre els ganxos locals. El primer: són opcionals per naturalesa, perquè qualsevol se'ls pot saltar amb git commit --no-verify. Per això les mateixes comprovacions s'han de repetir a la CI, on no es poden esquivar. Els ganxos són comoditat per a qui desenvolupa, no un control de seguretat. El segon: han de ser ràpids. Un ganxo que triga trenta segons fa que tothom aprengui a fer servir --no-verify. El que és lent —la bateria completa de proves— va a la CI, no aquí.
- Connectar els repositoris amb els activadors de Cloud Build
El tancament de la lliçó és unir el de 06-01 amb el d'aquí. Aquest és el mapa complet d'AlpinaShop:
flowchart TD
A[Dani obre PR<br/>feat/filtro-por-marca] --> B[Activador catalogo-pr]
B --> C[cloudbuild-pr.yaml:<br/>instal·lar, pytest, ruff, bandit]
C --> D{Verd?}
D -->|No| E[PR bloquejada<br/>per protecció de branca]
D -->|Sí| F[Marta revisa]
F --> G[Squash a main]
G --> H[Activador catalogo-main]
H --> I[cloudbuild.yaml:<br/>construir + publicar :SHA]
I --> J[Desplegament a<br/>alpinashop-dev]
J --> K{Etiqueta v2.5.0}
K --> L[Activador de promoció<br/>amb aprovació]
L --> M[alpinashop-prod]
Els quatre activadors que AlpinaShop deixa configurats:
| Activador | Repositori | Esdeveniment | Configuració | Què fa |
|---|---|---|---|---|
catalogo-pr |
alpinashop-catalogo |
PR contra main |
cloudbuild-pr.yaml |
Prova, no desplega |
catalogo-main |
alpinashop-catalogo |
Push a main |
cloudbuild.yaml |
Construeix, publica, desplega a dev |
catalogo-prod |
alpinashop-catalogo |
Etiqueta ^v\d+\.\d+\.\d+$ |
cloudbuild-prod.yaml, --require-approval |
Promociona a producció |
ml-main |
alpinashop-ml |
Push a main |
cloudbuild-ml.yaml |
Compila i llança el pipeline de Vertex AI |
I una comprovació final que val la pena fer explícita: l'activador de producció s'activa en crear una etiqueta, així que qui pugui crear etiquetes pot iniciar un desplegament a producció. La protecció d'etiquetes és tan necessària com la de branques:
# Regla de protecció d'etiquetes a GitHub Patró: v* Qui pot crear: només l'equip @alpinashop/infraestructura
Sense aquella regla, l'aprovació manual de 06-01 continua protegint el desplegament —ningú no arriba a producció sense que la Marta aprovi—, però qualsevol podria omplir la cua de compilacions pendents. Amb ella, el camí a producció està acotat de principi a fi.
Errors Habituals i Consells
Començar un projecte nou amb Cloud Source Repositories el 2026. Està en desús, no rep funcionalitat nova i no té revisió de codi, que és just el que es necessita. Coneix-lo per operar el que és heretat; tria GitHub o GitLab per al que és nou.
Crear claus JSON de compte de servei per a la CI. Credencials permanents i transferibles, la causa més comuna d'incidents greus a GCP. Fes servir Workload Identity Federation. I si trobes claus JSON antigues per la infraestructura, planifica'n l'eliminació com si fos deute de seguretat, perquè ho és.
Oblidar id-token: write als permisos del workflow de GitHub Actions. Sense aquella línia no s'emet el testimoni OIDC i l'autenticació falla amb un error confús. És el primer entrebanc de tothom.
Branques de característica que viuen setmanes. Cada dia que una branca viu augmenta la probabilitat d'un conflicte dolorós i endarrereix el senyal de la CI. Branques curtes i interruptors de funcionalitat per al que no està llest.
Pujar un secret i "arreglar-ho" esborrant-lo al commit següent. Continua a l'historial i a tots els clons. Rota el secret immediatament; la neteja de l'historial és secundària.
Confiar només en els ganxos de pre-commit. Se salten amb --no-verify. Repeteix sempre les comprovacions a la CI, on són obligatòries.
Pull requests enormes. Una PR de 2.000 línies no es revisa, s'aprova. Divideix en canvis petits amb una intenció cadascun; la qualitat dels comentaris és inversament proporcional a la mida del diff.
Discutir estil a les revisions. Automatitza el formatatge amb ruff-format o equivalent i dedica la revisió al que una màquina no pot avaluar: si l'enfocament és correcte i si és segur.
No protegir les etiquetes. Si el desplegament a producció es dispara amb una etiqueta, qui crea etiquetes inicia desplegaments. Protegeix-les igual que les branques.
Consell final: la migració es fa per parts. No intentis portar els quatre repositoris a GitHub el mateix dia. Comença pel catàleg, que és el que més es toca, munta el seu pipeline, comprova que l'equip s'hi ha adaptat, i després ves amb la infraestructura. I comença per treure de Drive els manifestos de Kubernetes: és la mitja hora de feina amb millor relació benefici/cost de tot el mòdul.
Exercicis
Exercici 1: rescatar el codi dispers
Fes un pla concret de tres fases per portar al control de versions tots els actius dispersos d'AlpinaShop: el repositori local d'en Dani, els startup scripts als metadades de les plantilles d'instància, els manifestos de Kubernetes a Drive, els notebooks de la Lucía i les comandes de gcloud de la Marta. Per a cada actiu, indica a quin repositori va, què cal revisar abans de pujar-lo i quin risc concret té aquella pujada.
Exercici 2: configurar l'accés de GitHub Actions amb mínim privilegi
AlpinaShop vol un workflow de GitHub Actions a alpinashop-datos que, en fer push a main, validi les consultes SQL executant un dry run contra BigQuery a alpinashop-datos i publiqui un informe al bucket alpinashop-datalake. Escriu la configuració de Workload Identity Federation i els rols IAM exactes, i explica què impedeix que un workflow del repositori alpinashop-catalogo faci servir aquella mateixa identitat.
Exercici 3: triar estratègia de branques amb un cas incòmode
AlpinaShop signa un contracte amb una cadena de botigues que necessita una versió personalitzada del catàleg, amb el seu propi disseny i algunes regles de preus diferents, mantinguda durant almenys dos anys en paral·lel a la versió pública. En Dani proposa crear una branca cliente-x de llarga vida. Analitza la proposta, digues si trunk-based continua sent vàlid i proposa la solució que recomanaries, amb les seves contrapartides.
Solucions
Solució 1
Fase 1 — Rescatar el que corre risc de perdre's (aquesta setmana).
| Actiu | Destinació | Revisar abans | Risc |
|---|---|---|---|
| Repositori local d'en Dani | alpinashop-catalogo |
Historial complet a la recerca de secrets | Alt: si en algun commit antic hi ha una contrasenya, pujar-lo la publica a l'equip |
| Manifestos de Drive | alpinashop-catalogo/k8s/ |
Quina versió és la que està realment aplicada | Mitjà: hi ha tres versions i no se sap quina corre |
| Startup scripts | alpinashop-infra/scripts/ |
Credencials incrustades a l'script | Alt: els startup scripts són un lloc clàssic per enganxar contrasenyes |
Per al repositori d'en Dani, el procediment correcte és escanejar l'historial complet, no només l'estat actual:
git clone --mirror /ruta/local/catalogo catalogo-auditoria.git
gitleaks detect --source=catalogo-auditoria.git --report-path=hallazgos.jsonSi apareixen secrets, hi ha dos camins. El pragmàtic: rotar totes les credencials trobades i pujar l'historial tal qual, acceptant que conté valors ja invàlids. El purista: reescriure l'historial amb git filter-repo. Per a AlpinaShop recomano el pragmàtic —rotar és obligatori en tots dos casos, i la reescriptura aporta poc si els valors ja no serveixen—, llevat que hi hagi dades personals, on la reescriptura sí que és necessària.
Per als manifestos, la comprovació imprescindible és què està aplicat de debò, no quin fitxer es diu FINAL:
Es netegen els camps generats pel clúster (status, metadata.uid, resourceVersion, anotacions d'última configuració) i allò és la versió que va al repositori. La veritat és al clúster, no a Drive.
Fase 2 — Estructurar i protegir (setmana següent). Crear els quatre repositoris, activar la protecció de branques i CODEOWNERS, instal·lar els ganxos de pre-commit amb gitleaks i connectar els activadors de Cloud Build.
Fase 3 — El difícil (setmanes següents).
| Actiu | Destinació | Com | Risc |
|---|---|---|---|
| Notebooks de la Lucía | alpinashop-ml/notebooks/ |
Netejar sortides amb nbstripout; extreure el reutilitzable a mòduls |
Alt: els notebooks solen contenir dades de clients a les sortides |
Comandes de gcloud de la Marta |
alpinashop-infra/ com a Terraform |
No transcriure comandes: exportar l'estat real, 06-05 i 06-07 | Mitjà: laboriós, però sense pèrdua |
El detall dels notebooks mereix èmfasi: una cel·la executada amb df.head() sobre alpinashop_analitica deixa noms, correus i adreces guardats dins del .ipynb. Pujar un notebook sense netejar sortides és una fuita de dades personals al repositori, amb Git no oblidant res. nbstripout com a ganxo de pre-commit ho resol de manera automàtica i no és negociable a alpinashop-ml.
I l'observació sobre les comandes de la Marta: transcriure-les a un script seria l'error. Aquell historial està incomplet i conté ordres que es van executar i després es van desfer. El correcte és exportar la configuració real de la infraestructura viva, que és exactament el procediment de 06-05.
Solució 2
Configuració de la federació, reutilitzant el pool ja creat:
PROY=alpinashop-datos
NUM=$(gcloud projects describe $PROY --format='value(projectNumber)')
gcloud iam service-accounts create sa-gha-datos \
--display-name="GitHub Actions - validacio SQL" --project=$PROY
# Només el repositori alpinashop-datos, i només la branca main
gcloud iam service-accounts add-iam-policy-binding \
sa-gha-datos@${PROY}.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects/${NUM}/locations/global/workloadIdentityPools/github-pool/attribute.repository/alpinashop/alpinashop-datos" \
--project=$PROYRols exactes, aplicant mínim privilegi de debò:
| Rol | Àmbit | Per què aquell i no un altre |
|---|---|---|
roles/bigquery.jobUser |
Projecte alpinashop-datos |
Permet llançar treballs; un dry run no llegeix dades |
roles/bigquery.metadataViewer |
Dataset alpinashop_analitica |
El dry run necessita els esquemes per validar |
roles/storage.objectCreator |
Bucket alpinashop-datalake, prefix d'informes |
Crear, no llegir ni esborrar |
Fixa't en el que no porta: ni bigquery.dataViewer —un dry run valida sintaxi i esquema, no executa la consulta ni llegeix una sola fila— ni storage.objectAdmin —crear informes no requereix poder esborrar els existents—. I l'objectCreator acotat al prefix mitjançant una condició IAM (03-04) impedeix que un workflow compromès sobreescrigui dades del data lake.
Què impedeix que un workflow d'alpinashop-catalogo faci servir aquesta identitat: el principalSet està lligat a attribute.repository/alpinashop/alpinashop-datos. El testimoni OIDC que GitHub emet porta signat el nom del repositori d'origen, i GitHub no permet falsificar-lo —l'emet el seu propi servei, no el workflow—. Un workflow d'alpinashop-catalogo presentaria un testimoni amb repository: alpinashop/alpinashop-catalogo, que no coincideix amb el principalSet, i GCP rebutjaria l'intercanvi. Aquella és la propietat que fa que la federació sigui substancialment més segura que una clau JSON: la identitat la certifica un tercer de confiança i està lligada al context d'execució, no a la possessió d'un fitxer.
I una millora addicional: l'attribute-condition del proveïdor (assertion.repository_owner == 'alpinashop') actua com a segona barrera a nivell de proveïdor. Sense ella, qualsevol repositori de GitHub del món podria almenys intentar l'intercanvi, i encara que el principalSet el rebutjaria igualment, és millor filtrar-lo abans. Defensa en profunditat.
Solució 3
La proposta de la branca de llarga vida és dolenta, i convé explicar per què abans de proposar-ne una alternativa. Una branca cliente-x mantinguda dos anys produeix, amb total certesa:
- Divergència creixent: cada correcció de seguretat cal aplicar-la dues vegades, i en algun moment se n'oblidarà una. Aquell oblit serà just l'important.
- Fusions cada vegada més cares: als sis mesos, portar els canvis de
mainacliente-xés un projecte en si mateix. - Duplicació de tota la resta: dos pipelines, dos conjunts d'imatges, dos desplegaments, dues sèries de mètriques.
- Un sistema que ningú no prova sencer: la CI provaria
main, icliente-xquedaria amb proves velles.
Però trunk-based continua sent vàlid, perquè el problema real no és d'estratègia de branques: és d'arquitectura. La pregunta correcta no és "com mantinc dues branques?", sinó "com faig que una sola base de codi serveixi dos clients?".
La solució recomanada: multiinquilí per configuració. Una única branca main, una única imatge, i el comportament diferent resolt en temps d'execució:
# catalogo/inquilino.py
@dataclass(frozen=True)
class Inquili:
id: str
tema: str # plantilles i fulls d'estil
regles_preu: str # estratègia de preus a aplicar
domini: str
INQUILINS = {
"public": Inquili("public", "alpina", "estandard", "www.alpinashop.example"),
"cliente-x": Inquili("cliente-x", "clientex", "majorista", "tienda.cliente-x.example"),
}
def inquili_actual() -> Inquili:
# Resolució per domini: el mateix desplegament serveix els dos
return INQUILINS.get(DOMINIS.get(request.host), INQUILINS["public"])El que canvia per inquilí són dades i configuració: el tema visual, les regles de preu, el domini, potser el subconjunt de catàleg. Res d'això justifica una branca.
| Enfocament | Cost de manteniment | Risc de divergència | Complexitat del codi | Recomanat? |
|---|---|---|---|---|
| Branca de llarga vida | Molt alt i creixent | Segur | Baixa | No |
| Multiinquilí per configuració | Baix | Cap | Mitjana | Sí |
| Repositori i producte separats | Molt alt | N/A: són productes diferents | Baixa | Només si divergeixen de debò |
| Nucli compartit com a biblioteca | Mitjà | Mitjana | Alta | Si hi ha molts clients |
Les contrapartides, dites amb honestedat. El multiinquilí no és gratis: introdueix condicionals al codi, cal provar els dos camins, i existeix un risc nou i seriós —una fuita entre inquilins, que el client X vegi dades del públic o al revés—. Això exigeix que l'identificador d'inquilí s'apliqui a la capa d'accés a dades i es provi explícitament, no que es confiï que cada consulta se'n recordi de filtrar.
I hi ha un punt de ruptura que cal reconèixer: si d'aquí a un any el client X demana un flux de compra completament diferent, un catàleg amb una altra estructura i un procés de facturació propi, ja no són dues configuracions del mateix producte, sinó dos productes. En aquell moment la separació és correcta, i es fa com a productes separats amb una biblioteca comuna, no com una branca.
La regla que se n'endú un d'aquí: les branques serveixen per separar la feina en el temps, no per separar variants en l'espai. Quan algú proposa una branca de llarga vida per a una variant de producte, el senyal és que hi ha una decisió d'arquitectura pendent de prendre. I la conversa amb comercial també importa: si el contracte amb el client X permet una mica de flexibilitat en el disseny i en les regles de preu, el multiinquilí surt gairebé gratis; si exigeix un producte completament diferent, el cost real d'aquell contracte és molt més gran del que semblava en signar-lo, i això convé saber-ho abans.
Conclusió
AlpinaShop té per fi un fonament sobre el qual construir.
Saps què aporta el control de versions —història, atribució, reversió, ramificació, revisió, identitat immutable i automatització— i que les dues últimes són les que fan possible tot el de 06-01: sense repositori remot no hi ha $COMMIT_SHA, no hi ha activador i no hi ha pipeline.
Coneixes Cloud Source Repositories: repositoris Git privats a GCP, gcloud source repos clone amb autenticació per identitat de gcloud, permisos governats per IAM amb els mateixos grups de 03-04, cerca de codi i integració nativa amb Cloud Build i Error Reporting. I saps la veritat incòmoda: està en desús, no admet clients nous, no rep funcionalitat i mai no va tenir revisió de codi, que és el que de debò es necessita. El coneixes per operar infraestructures heretades i per migrar-les amb git clone --mirror, no per començar res nou.
Saps connectar GitHub o GitLab amb GCP utilitzant la 2a generació de repositoris de Cloud Build —connexió i repositori com a recursos declarables, testimoni a Secret Manager— i per què és preferible a l'aplicació de GitHub de la primera generació. I sobretot saps fer servir Workload Identity Federation perquè les GitHub Actions accedeixin a GCP sense cap clau JSON: el pool, el proveïdor OIDC, l'attribute-condition, el principalSet lligat a un repositori i a una branca, i l'id-token: write que tothom oblida. Amb la regla que resumeix l'apartat: si el 2026 estàs creant una clau JSON de compte de servei, gairebé sempre hi ha una alternativa millor.
Tens la taula de decisió honesta entre CSR, GitHub i GitLab, i la decisió d'AlpinaShop —GitHub— amb les seves quatre raons, juntament amb els casos en què GitLab seria millor elecció. Tens l'estratègia de branques: trunk-based amb branques curtes, perquè és un servei web amb una sola versió viva, perquè l'equip són tres persones, perquè és la premissa mateixa de la integració contínua i perquè els conflictes creixen de manera no lineal amb el temps. Amb els interruptors de funcionalitat com a resposta a "això triga tres setmanes", i amb la disciplina d'esborrar-los després.
Saps què va al repositori —codi, Dockerfile, pipelines, manifestos, Terraform, proves, migracions, decisions d'arquitectura— i què no —secrets, estat de Terraform, dades, artefactes, fitxers grans—, amb l'advertiment que val per tota la lliçó: Git no oblida, i davant d'un secret filtrat l'única resposta correcta és rotar-lo. Tens l'estructura de quatre repositoris d'AlpinaShop dividida per ritme i responsabilitat, amb alpinashop-infra assenyalat com el més perillós. Tens la protecció de branques amb les seves set regles, CODEOWNERS exigint revisió de seguretat per a IAM i tallafoc, els missatges de commit convencionals, el versionatge semàntic amb el seu matís honest, i els ganxos de pre-commit amb gitleaks com a xarxa de seguretat que no substitueix la CI.
I tens els quatre activadors connectats de principi a fi: pull request que prova, main que construeix i desplega en desenvolupament, etiqueta protegida que promociona a producció amb aprovació, i el pipeline d'ML compilant-se sol.
Amb això, el catàleg es construeix, es prova i es desplega sol. Però queden peces del sistema que no són ni una aplicació web ni un pipeline de dades: són reaccions a esdeveniments. Quan arriba un missatge al topic imagenes-subidas, alguna cosa ha de despertar, cridar la Vision API, escriure a BigQuery i tornar-se a adormir. Muntar una màquina virtual o un pod per a això seria absurd.
A 06-03 arriben les Cloud Functions, i amb elles es tanca per fi la punta solta que va deixar el mòdul 5.
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
