La lección anterior terminó con una pregunta incómoda. Has construido un pipeline de integración continua que se dispara con cada push, que etiqueta las imágenes con el SHA del commit y que publica en Artifact Registry todo lo que pasa las pruebas. ¿Y desde qué repositorio se dispara exactamente?
Porque el inventario real del código de AlpinaShop en este momento es el siguiente. El catálogo web está en un repositorio Git local en el portátil de Dani, con un remoto que apunta a un servidor que la empresa dejó de mantener hace dos años; funciona porque nadie hace git push. Los startup scripts de las plantillas de instancia del MIG están pegados en el campo de metadatos de la plantilla, y en ningún otro sitio. Los manifiestos de Kubernetes del namespace tienda están en una carpeta compartida de Drive llamada "GKE definitivo (v3) FINAL". Los notebooks de Lucía viven en su instancia de Workbench. Y los comandos de gcloud con los que Marta creó la VPC, el balanceador y las reglas de Cloud Armor no están en ninguna parte, salvo en el historial de su terminal, que se trunca a los 10.000 comandos.
Nada de lo que se construya encima —ni CI/CD, ni infraestructura como código, ni revisión de cambios— es posible sin resolver esto primero. Esta lección lo resuelve, y lo hace con una dosis grande de honestidad sobre qué herramienta conviene usar de verdad en 2026.
Contenido
- Por qué el control de versiones es el cimiento de todo
- Qué es Cloud Source Repositories y cómo se usa
- La conversación honesta: CSR está en desuso
- Conectar GitHub o GitLab con GCP
- Workload Identity Federation: acceder a GCP sin claves JSON
- Tabla de decisión: CSR, GitHub o GitLab
- Estrategia de ramas para un equipo pequeño
- Qué debe y qué no debe estar en el repositorio
- La estructura de repositorios de AlpinaShop
- Revisión de código y protección de ramas
- Mensajes de commit y versionado semántico
- Ganchos de calidad antes del commit
- Conectar los repositorios con los disparadores de Cloud Build
- Por qué el control de versiones es el cimiento de todo
Merece la pena decir explícitamente qué proporciona un sistema de control de versiones, porque cuando se lleva años usándolo se olvida que resuelve varios problemas distintos a la vez:
| Capacidad | Qué permite | Qué se rompe sin ella en AlpinaShop |
|---|---|---|
| Historia | Saber qué cambió, cuándo y por qué | Nadie sabe por qué el timeout del catálogo es 45 s |
| Atribución | Saber quién hizo cada cambio | Ante un fallo, no hay a quién preguntar |
| Reversión | Volver a un estado anterior conocido | La única vuelta atrás es reescribir a mano |
| Ramificación | Trabajar en paralelo sin pisarse | Dani y Marta se pasan ficheros por Slack |
| Revisión | Que otra persona mire antes de fusionar | Nadie revisa nada |
| Identidad inmutable | Un SHA identifica un estado exacto | El $COMMIT_SHA de 06-01 no existe |
| Automatización | Reaccionar a eventos del repositorio | No hay disparadores posibles |
Fíjate en las dos últimas filas, porque son las que conectan con la lección anterior. El pipeline de Cloud Build etiqueta cada imagen con $COMMIT_SHA, y esa etiqueta es lo que hace posible saber qué código está en producción y volver atrás. Sin repositorio remoto, no hay SHA, no hay disparador y no hay pipeline. Todo lo construido en 06-01 depende de resolver esto.
Y hay un aspecto cultural que importa tanto como el técnico: el control de versiones convierte el código de propiedad personal en propiedad del equipo. Mientras el catálogo esté en el portátil de Dani, es de Dani —con lo que eso implica cuando se va de vacaciones, cuando se pone enfermo o cuando cambia de empresa—. En cuanto está en un repositorio compartido con historia y revisión, es de AlpinaShop.
- Qué es Cloud Source Repositories y cómo se usa
Cloud Source Repositories (CSR) es el servicio de repositorios Git privados alojados en Google Cloud. Un repositorio de CSR es un repositorio Git normal y corriente: clone, commit, push, pull, ramas y etiquetas funcionan exactamente igual. Lo que añade es su integración con el resto de GCP.
Crear y clonar un repositorio:
# Crear el repositorio dentro del proyecto de CI/CD
gcloud source repos create alpinashop-catalogo --project=alpinashop-cicd
# Clonarlo: gcloud configura la autenticación por ti
gcloud source repos clone alpinashop-catalogo --project=alpinashop-cicd
cd alpinashop-catalogo
# A partir de aquí es Git normal
git add .
git commit -m "Importación inicial del catálogo web"
git push origin mainEl gcloud source repos clone hace algo más que un git clone: configura un ayudante de credenciales que usa tu identidad de gcloud para autenticarte. No hay contraseñas ni claves SSH que gestionar; el acceso se controla con IAM, igual que cualquier otro recurso de GCP:
# Dani puede leer y escribir en el repositorio
gcloud projects add-iam-policy-binding alpinashop-cicd \
--member='group:[email protected]' \
--role=roles/source.writer
# El equipo de datos solo lee
gcloud projects add-iam-policy-binding alpinashop-cicd \
--member='group:[email protected]' \
--role=roles/source.readerSi prefieres autenticarte sin gcloud —por ejemplo desde una máquina que no lo tiene— existen las credenciales generadas manualmente y el acceso por SSH con una clave registrada en tu perfil.
Lo que CSR aporta de verdad, y que explica por qué existió:
- Integración nativa con Cloud Build: los disparadores de 06-01 funcionan sin conectar nada externo ni instalar aplicaciones de terceros.
- Los permisos son IAM: los mismos grupos, roles y condiciones de 03-04 gobiernan el acceso al código. No hay un sistema de permisos paralelo que mantener sincronizado.
- Búsqueda de código en todos los repositorios del proyecto desde la consola.
- Integración con Error Reporting y Cloud Debugger: cuando una excepción se agrupa en Error Reporting, la consola puede mostrar la línea de código concreta del repositorio.
- Réplica automática desde GitHub o Bitbucket, para tener una copia dentro de GCP.
- Los datos no salen de tu organización de GCP, lo que en algunos contextos regulados simplifica una conversación con el departamento legal.
Todo eso es real y funciona. Y aun así, no es lo que AlpinaShop va a usar.
- La conversación honesta: CSR está en desuso
Hay que decirlo con claridad, porque el nombre de esta lección lleva el servicio en el título y sería fácil dar la impresión equivocada:
Cloud Source Repositories está en desuso. Desde 2024 no se habilita para clientes nuevos que no lo hubieran usado previamente, no recibe funcionalidad nueva y Google recomienda explícitamente usar GitHub, GitLab u otro proveedor externo conectado a Cloud Build.
Los repositorios existentes siguen funcionando y no hay una fecha de apagado anunciada. Pero no se debe empezar un proyecto nuevo con CSR en 2026, y conviene entender por qué desapareció, porque la razón es interesante y no es técnica.
CSR alojaba Git perfectamente. Lo que nunca tuvo fue el resto: revisión de código con comentarios en línea, plantillas de pull request, discusiones, gestión de incidencias, wiki, integración con el ecosistema de herramientas que todo el mundo usa. Y resulta que el repositorio no es el producto: el producto es el flujo de colaboración alrededor del repositorio. Google compitió en la parte fácil —almacenar objetos Git— contra plataformas que habían construido la parte difícil.
Por eso el patrón dominante hoy es: el código en GitHub o GitLab, la ejecución en GCP. Y por eso esta lección dedica el resto de su espacio a hacer bien esa conexión.
¿Y por qué sigue mereciendo la pena conocer CSR? Por tres razones prácticas: porque te lo encontrarás en infraestructuras heredadas y tendrás que operarlo o migrarlo; porque su modelo de permisos basado en IAM sigue siendo una idea excelente que conviene entender; y porque la réplica desde GitHub sigue siendo útil cuando alguien exige una copia del código dentro de la organización de GCP.
Si te toca migrar de CSR a GitHub, el procedimiento es corto porque Git es Git:
# Clon completo con todas las ramas y etiquetas
git clone --mirror https://source.developers.google.com/p/alpinashop-cicd/r/alpinashop-catalogo
cd alpinashop-catalogo.git
# Empujar todo al destino nuevo
git remote set-url origin [email protected]:alpinashop/alpinashop-catalogo.git
git push --mirror originLo que no migra ese comando: los permisos IAM (hay que rehacerlos en el proveedor nuevo) y los disparadores de Cloud Build, que hay que recrear apuntando a la conexión nueva.
- Conectar GitHub o GitLab con GCP
AlpinaShop elige GitHub, y necesita que Cloud Build reaccione a sus eventos. La conexión ha evolucionado y en 2026 conviene usar la forma moderna.
Existen dos generaciones de conexión de repositorios, y la diferencia importa:
| Aspecto | 1ª generación (aplicación de GitHub) | 2ª generación (Cloud Build repositories) |
|---|---|---|
| Mecanismo | Aplicación de GitHub instalada en la organización | Recurso Connection + Repository gestionado |
| Gestión | Solo por consola | Consola, gcloud y Terraform |
| Credenciales | Gestionadas por la aplicación | Token en Secret Manager, controlado por ti |
| Repositorios | Se enlazan uno a uno por consola | Enlazables por API, en lote |
| Proveedores | GitHub, GitHub Enterprise | GitHub, GitHub Enterprise, GitLab, Bitbucket |
| Recomendación | Heredado | La que hay que usar |
La segunda generación es la correcta porque el enlace del repositorio deja de ser un clic irreproducible en una consola y pasa a ser un recurso declarable —lo que encaja con todo lo que llega en 06-05 y 06-07—.
El procedimiento, paso a paso:
# 1. Habilitar las APIs necesarias
gcloud services enable cloudbuild.googleapis.com secretmanager.googleapis.com \
--project=alpinashop-cicd
# 2. Guardar el token de acceso personal de GitHub en Secret Manager
# (con permisos: repo, read:user, read:org — nada más)
printf 'ghp_XXXXXXXXXXXXXXXXXXXX' | gcloud secrets create github-token-alpinashop \
--data-file=- --project=alpinashop-cicd
# 3. Dar acceso al secreto a la cuenta de servicio 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 conexión (requiere una autorización interactiva en el navegador)
gcloud builds connections create github alpinashop-github \
--region=europe-west1 --project=alpinashop-cicd
# 5. Enlazar el repositorio concreto
gcloud builds repositories create alpinashop-catalogo \
--remote-uri=https://github.com/alpinashop/alpinashop-catalogo.git \
--connection=alpinashop-github \
--region=europe-west1 --project=alpinashop-cicdFíjate en el paso 2: el token de GitHub va a Secret Manager, exactamente igual que la contraseña de la base de datos en 03-06. Un token con permiso de escritura sobre todos tus repositorios es una credencial de primer nivel, y merece el mismo tratamiento: almacenamiento cifrado, acceso concedido a una identidad concreta y rotación periódica. Anotar la fecha de caducidad del token en el calendario del equipo evita el clásico "el pipeline dejó de funcionar el martes y nadie sabe por qué".
Con la conexión creada, un disparador se define contra el recurso 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-cicdCon GitLab el procedimiento es análogo: gcloud builds connections create gitlab, con un token de acceso de GitLab en Secret Manager. Y para GitLab autoalojado detrás de un firewall existe la conexión mediante un agente de servicio en la VPC, que es exactamente el caso que hace falta cuando el servidor de Git es interno.
- Workload Identity Federation: acceder a GCP sin claves JSON
Hay un segundo sentido de la conexión, y es el que más problemas de seguridad causa: que las GitHub Actions accedan a recursos de GCP. AlpinaShop lo necesita para que un workflow pueda publicar en Artifact Registry o consultar BigQuery sin depender de Cloud Build.
La forma antigua de hacerlo, y por qué es mala:
# NO HAGAS ESTO
gcloud iam service-accounts keys create clave.json \
[email protected]
# ...y pegar el contenido de clave.json en un secreto de GitHubEsa clave JSON es una credencial permanente, sin caducidad y transferible. Quien la obtiene es esa cuenta de servicio, para siempre, desde cualquier lugar del mundo. No hay forma de saber si se ha copiado. Se filtra en logs, en capturas de pantalla, en commits accidentales, y su rotación es un proceso manual que nadie hace. Las claves JSON de cuenta de servicio son, con diferencia, la causa más común de incidentes de seguridad graves en GCP.
Workload Identity Federation elimina el problema de raíz. La idea, que conecta directamente con lo visto en 03-04:
GCP confía en el emisor de tokens de GitHub. Cuando una GitHub Action se ejecuta, GitHub le entrega un token OIDC de corta duración que dice "soy el workflow X del repositorio Y de la organización Z". GCP verifica ese token, comprueba que coincide con una condición que tú has definido, y lo intercambia por credenciales temporales.
flowchart LR
A[GitHub Action<br/>en ejecución] -->|1. pide token OIDC| B[Emisor OIDC<br/>de GitHub]
B -->|2. token firmado:<br/>repo, rama, workflow| A
A -->|3. presenta el token| C[Workload Identity Pool<br/>en GCP]
C -->|4. verifica firma<br/>y condición de atributos| D{¿Coincide?}
D -->|Sí| E[Credencial temporal<br/>de la cuenta de servicio]
D -->|No| F[Rechazado]
E --> G[Acceso a Artifact Registry,<br/>BigQuery, etc.]
La configuración completa:
PROY=alpinashop-cicd
NUM=$(gcloud projects describe $PROY --format='value(projectNumber)')
# 1. Crear el pool de identidades de carga de trabajo
gcloud iam workload-identity-pools create github-pool \
--location=global --display-name="GitHub Actions" --project=$PROY
# 2. Crear el proveedor OIDC, con la condición de atributos
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. Permitir que SOLO el repositorio del catálogo suplante a la cuenta de servicio
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 paso 3 es el que hay que entender bien. El principalSet con attribute.repository/alpinashop/alpinashop-catalogo significa que solo los workflows de ese repositorio concreto pueden obtener credenciales de esa cuenta de servicio. Un workflow de otro repositorio de la misma organización presenta un token válido pero con otro repository, no coincide con el principalSet, y es rechazado.
Si además quieres restringir por rama —que solo main pueda desplegar—, se usa attribute.ref:
Y así se consume desde el workflow:
# .github/workflows/publicar.yml
name: Publicar imagen
on:
push:
branches: [main]
permissions:
contents: read
id-token: write # imprescindible: permite pedir el token 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]'
# Fíjate: NO hay ninguna clave, ningún secreto, ningún 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ínea id-token: write en permissions es obligatoria y se olvida constantemente; sin ella, GitHub no emite el token OIDC y la autenticación falla con un mensaje que no ayuda demasiado.
| Aspecto | Clave JSON | Workload Identity Federation |
|---|---|---|
| Caducidad | Ninguna | Minutos |
| Si se filtra | Acceso permanente | Inútil fuera de contexto |
| Rotación | Manual, nadie la hace | Automática por diseño |
| Ámbito | Cualquiera que la tenga | Solo el repositorio/rama declarado |
| Auditoría | "La cuenta X hizo algo" | Qué repositorio y qué workflow |
| Trabajo de mantenimiento | Alto | Ninguno |
La regla práctica: si estás creando una clave JSON de cuenta de servicio en 2026, hay casi siempre una alternativa mejor. Dentro de GCP, cuentas de servicio adjuntas al recurso. Fuera de GCP, federación de identidad. Las claves JSON quedan para casos residuales, y en esos casos deben tener caducidad, rotación y una organization policy que limite dónde pueden crearse —tema de 07-07—.
- Tabla de decisión: CSR, GitHub o GitLab
La comparación sin adornos, que es lo que AlpinaShop necesita para decidir:
| Criterio | Cloud Source Repositories | GitHub | GitLab |
|---|---|---|---|
| Estado | En desuso, sin novedades | Activo, líder de mercado | Activo, fuerte en autoalojado |
| Coste | Gratuito hasta un límite bajo de almacenamiento y datos | Gratis en privado; de pago por funciones avanzadas | Igual; edición comunitaria autoalojable gratis |
| Revisión de código | Prácticamente inexistente | Pull requests maduros, sugerencias, propietarios de código | Merge requests, muy completos |
| CI/CD propio | No, depende de Cloud Build | GitHub Actions, ecosistema enorme | GitLab CI, muy potente e integrado |
| Integración con GCP | Nativa, permisos IAM | Excelente (2ª gen + federación) | Muy buena (2ª gen + federación) |
| Ecosistema y plantillas | Nulo | El mayor con diferencia | Amplio |
| Incidencias y proyectos | No | Sí | Sí, incluido DevSecOps |
| Autoalojado | No aplica | GitHub Enterprise Server | Su punto fuerte |
| Dependencia de proveedor | Alta con Google | Media (Git es portable) | Baja, se puede autoalojar |
| Contratación de talento | Nadie lo conoce | Todo el mundo lo conoce | Muy conocido |
La decisión de AlpinaShop es GitHub, por cuatro razones ordenadas por peso:
- La revisión de código es el objetivo principal. El problema de AlpinaShop no es dónde guardar bytes: es que nadie mira los cambios antes de que lleguen a producción. GitHub resuelve eso; CSR, no.
- Cualquier persona que se incorpore ya sabe usarlo. Con un equipo de tres personas técnicas, el tiempo de formación cuenta.
- La integración con GCP es de primera clase gracias a la 2ª generación y a la federación de identidad. La supuesta ventaja de CSR ha dejado de serlo.
- La dependencia es baja de verdad. Un repositorio Git es portable:
git clone --mirrory estás en otro sitio. Lo que sí ata son las incidencias, los pull requests y los workflows de Actions, y por eso el pipeline principal de AlpinaShop vive encloudbuild.yaml—un fichero neutral— en lugar de en GitHub Actions.
Cuándo elegirías GitLab en su lugar: si necesitas autoalojar por requisitos regulatorios, si quieres un CI/CD integrado sin depender de Cloud Build, o si valoras tener incidencias, código, CI y registro de contenedores en un único producto. Es una elección perfectamente defendible.
Cuándo seguirías con CSR: solo si ya lo usas y funciona. Migrar por migrar no es una prioridad; migrar cuando se necesite revisión de código en serio, sí.
- Estrategia de ramas para un equipo pequeño
Con el repositorio decidido, la siguiente pregunta es cómo se organiza el trabajo dentro de él. Hay dos escuelas dominantes.
GitFlow define ramas de larga vida: main (lo que está en producción), develop (integración), feature/*, release/* y hotfix/*. Un cambio nace en una rama de característica, se fusiona a develop, entra en una rama de versión, se estabiliza y por fin llega a main.
Trunk-based define una única rama de larga vida, main, siempre desplegable. Los cambios se hacen en ramas cortas —horas o un par de días como mucho— que se fusionan a main en cuanto pasan la revisión y la CI.
| Aspecto | GitFlow | Trunk-based |
|---|---|---|
| Ramas de larga vida | main y develop |
Solo main |
| Vida de una rama de trabajo | Días o semanas | Horas o un par de días |
| Dolor al fusionar | Alto: divergen mucho | Bajo: divergen poco |
| Encaja con CI | Mal: se integra tarde | Es la premisa de la CI |
| Complejidad | Alta | Baja |
| Idóneo para | Versiones empaquetadas, varios mantenidos a la vez | Servicios web con despliegue continuo |
| Funciones a medias | Rama larga sin fusionar | Feature flags |
AlpinaShop elige trunk-based, y las razones son concretas:
- Es un servicio web, no un producto empaquetado. No hay clientes con la versión 2.3 que necesiten un parche mientras se desarrolla la 3.0. Solo hay una versión: la que está en
alpinashop-prod. - El equipo son tres personas. La coordinación que justifica GitFlow no existe cuando cabéis en una llamada.
- Es coherente con la CI de 06-01. La "I" de integración continua significa integrar continuamente; una rama de tres semanas es exactamente lo contrario, y el pipeline que construiste solo aporta valor si los cambios llegan a
maina menudo. - Los conflictos de fusión crecen de forma no lineal con el tiempo. Dos ramas de un día casi nunca chocan; dos ramas de tres semanas siempre lo hacen, y resolver ese conflicto es donde se cuelan los errores.
El flujo de trabajo diario resultante:
git switch main && git pull # 1. Partir siempre de main actualizado
git switch -c feat/filtro-por-marca # 2. Rama corta y con nombre claro
# ... trabajo, commits pequeños ...
git push -u origin feat/filtro-por-marca # 3. Publicar y abrir el pull request
# 4. La CI de 06-01 se ejecuta sola sobre el PR
# 5. Marta o Dani revisan
# 6. Fusión con squash a main → se despliega en alpinashop-dev
git switch main && git pull
git branch -d feat/filtro-por-marca # 7. Borrar la ramaLa objeción evidente: ¿y si una funcionalidad tarda tres semanas? La respuesta de trunk-based no es "mantén la rama tres semanas", sino fusionar código incompleto pero inactivo, protegido por un interruptor:
# catalogo/config.py — el interruptor se lee del entorno, no del código
NUEVO_BUSCADOR = os.environ.get("FEATURE_NUEVO_BUSCADOR", "false").lower() == "true"
# catalogo/rutas.py
@app.get("/buscar")
def buscar():
if NUEVO_BUSCADOR:
return buscador_semantico(request.args["q"]) # 05-06, en pruebas
return buscador_clasico(request.args["q"]) # lo que funciona hoyEl código nuevo llega a main y a producción desde el primer día, desactivado. Se activa en alpinashop-dev para probarlo, después para un porcentaje de usuarios, y por fin para todos. Y si algo va mal, desactivarlo es cambiar una variable de entorno, no revertir un despliegue. Es una vuelta atrás de segundos.
La contrapartida honesta: los interruptores acumulan complejidad y ramas muertas en el código. La disciplina asociada es borrarlos en cuanto la funcionalidad está consolidada, con una fecha de caducidad apuntada en el propio pull request que los introduce.
- Qué debe y qué no debe estar en el repositorio
Una regla útil como punto de partida: si es texto que describe cómo funciona o cómo se construye el sistema, va al repositorio.
| Sí va al repositorio | Por qué |
|---|---|
| Código de la aplicación | Evidente |
Dockerfile y .dockerignore |
La imagen debe ser reproducible desde el código |
cloudbuild.yaml y variantes |
El pipeline se revisa como el código (06-01) |
| Manifiestos de Kubernetes | Hoy están en Drive; ahí no se revisan ni se versionan |
Terraform (.tf) |
La infraestructura como código llega en 06-07 |
| Pruebas | Sin ellas la CI no comprueba nada |
requirements.txt con versiones fijadas |
Reproducibilidad de las dependencias |
| Migraciones de base de datos | Cambios de esquema revisables y ordenados |
| Definiciones de pipelines de datos y ML | Los componentes KFP de 05-07 |
README y decisiones de arquitectura |
DA-001, DA-002 y DA-003 tienen que estar escritas |
| Configuración no sensible por entorno | dev.tfvars, prod.tfvars |
| No va al repositorio | Dónde va | Por qué |
|---|---|---|
| Contraseñas, tokens, claves de API | Secret Manager (03-06) | Git no olvida nunca |
| Claves JSON de cuenta de servicio | No deberían existir (apartado 5) | — |
| Certificados y claves privadas | Secret Manager o KMS | Igual que arriba |
Estado de Terraform (.tfstate) |
Bucket de Cloud Storage (06-07) | Contiene valores sensibles |
| Datos de clientes, volcados de BD | Cloud Storage con control de acceso | RGPD |
| Artefactos compilados, imágenes | Artifact Registry | El repositorio no es un almacén binario |
| Ficheros grandes (modelos, CSV) | Cloud Storage o Git LFS | Hinchan el repositorio para siempre |
.env con valores reales |
Variables de entorno o Secret Manager | Se filtra al hacer git add . |
La frase clave sobre los secretos merece subrayarse: Git no olvida. Si subes una contraseña y la borras en el commit siguiente, sigue estando en el historial, en cada clon que alguien haya hecho y en cada réplica. Reescribir la historia es doloroso y nunca es completo. La única respuesta correcta ante un secreto filtrado es rotarlo inmediatamente, y solo después limpiar el historial si merece la pena.
Un .gitignore sensato es la primera línea de defensa:
# Secretos y credenciales
*.env
.env.*
*-key.json
*credentials*.json
*.pem
*.p12
# Estado de Terraform: NUNCA a Git
*.tfstate
*.tfstate.*
.terraform/
*.tfvars.secret
# Entornos y artefactos locales
venv/
__pycache__/
*.pyc
.pytest_cache/
dist/
build/
# Datos
*.csv
*.parquet
datos/Pero la defensa real es automática, y es el tema del apartado 12.
- La estructura de repositorios de AlpinaShop
El debate monorepo frente a polirepo tiene mucha literatura y poca respuesta universal:
| Aspecto | Monorepo | Polirepo |
|---|---|---|
| Cambio que cruza componentes | Un solo PR atómico | Varios PR coordinados |
| Permisos por componente | Difícil de acotar | Natural |
| CI | Necesita filtros por ruta | Simple |
| Descubrimiento de código | Todo a la vista | Hay que saber dónde mirar |
| Dependencias entre partes | Fáciles de compartir, fáciles de enredar | Fronteras explícitas |
| Herramientas necesarias | Más | Menos |
Para un equipo de tres personas, la complejidad de un monorepo con herramientas de construcción avanzadas no se justifica. Pero cuarenta repositorios diminutos tampoco. AlpinaShop se queda en el punto intermedio: cuatro repositorios agrupados por ciclo de vida y por quién los toca.
| Repositorio | Contenido | Quién lo mantiene | Pipeline |
|---|---|---|---|
alpinashop-catalogo |
Flask, Dockerfile, manifiestos de tienda, cloudbuild.yaml |
Dani | catalogo-main, catalogo-pr |
alpinashop-infra |
Terraform de la VPC, balanceador, IAM, Cloud SQL, buckets | Marta | plan en PR, apply con aprobación |
alpinashop-datos |
Pipelines de Dataflow, consultas SQL, DAG/Workflows de 04-06 | Lucía | Pruebas + despliegue de plantillas |
alpinashop-ml |
Componentes KFP, código de entrenamiento, notebooks limpios | Lucía y Dani | cloudbuild-ml.yaml de 06-01 |
El criterio de división no es técnico, es de ritmo y de responsabilidad. El catálogo cambia varias veces al día y lo toca Dani. La infraestructura cambia una vez por semana, requiere aprobación de Marta y su pipeline es completamente distinto —un plan que se revisa, no unas pruebas que pasan—. Meterlos juntos obligaría a ejecutar el pipeline de infraestructura por cada cambio de una plantilla HTML.
Y una advertencia sobre el repositorio de infraestructura: es el más peligroso de los cuatro. Quien puede fusionar un PR ahí puede modificar reglas de firewall, políticas de IAM y bases de datos. Su protección de ramas debe ser la más estricta, y sus revisores obligatorios deben incluir siempre a alguien de gcp-seguridad@.
- Revisión de código y protección de ramas
Que exista un repositorio no impide que alguien empuje directamente a main sin que nadie lo mire. Eso lo impide la protección de ramas.
Las reglas que AlpinaShop activa sobre main en los cuatro repositorios:
| Regla | Efecto | Por qué |
|---|---|---|
| Prohibido el push directo | Todo cambio pasa por pull request | Sin esto, el resto es decorativo |
| Al menos una aprobación | Otra persona ha mirado el cambio | El objetivo principal de la lección |
| La CI debe estar en verde | No se fusiona con pruebas rojas | Da sentido al pipeline de 06-01 |
| Aprobaciones caducan al haber cambios nuevos | No se aprueba una versión y se fusiona otra | Evita un truco muy usado |
| Historial lineal | Fusión con squash o rebase | Historia legible, revertir es trivial |
| Nadie se salta las reglas, ni los administradores | Sin excepciones | Las excepciones se vuelven norma |
| Escaneo de secretos | Bloquea el push si detecta credenciales | Última red antes del desastre |
En el repositorio alpinashop-infra se añade una regla más: revisor obligatorio del equipo de seguridad, mediante el fichero CODEOWNERS:
# CODEOWNERS del repositorio alpinashop-infra * @alpinashop/infraestructura /iam/ @alpinashop/seguridad @alpinashop/infraestructura /red/firewall.tf @alpinashop/seguridad @alpinashop/infraestructura /produccion/ @alpinashop/seguridad @alpinashop/infraestructura
Cualquier PR que toque IAM, reglas de firewall o el directorio de producción exige la aprobación de alguien de gcp-seguridad@. Es el equivalente en el código de la separación de funciones de 03-04.
Qué mirar en una revisión, en orden de importancia real: primero, ¿es correcto? —¿hace lo que dice, contempla los casos límite—; segundo, ¿es seguro? —secretos, validación de entradas, permisos que se amplían—; tercero, ¿está probado? —¿hay una prueba que fallaría sin este cambio—; cuarto, ¿se entiende? —lo va a leer alguien dentro de un año—; y por último, el estilo, que debería estar automatizado y no discutirse en la revisión. Ese último punto ahorra una cantidad enorme de fricción: si el formateo lo decide una herramienta, nadie discute sobre comas.
Y dos normas de convivencia que valen más que cualquier lista: pull requests pequeños —un PR de 40 líneas recibe comentarios útiles, uno de 2.000 recibe un "lgtm"— y revisiones rápidas, porque un PR que espera dos días bloquea a quien lo escribió y acumula conflictos.
- Mensajes de commit y versionado semántico
Un historial de commits con "cambios", "fix", "otra vez" y "ahora sí" no sirve para nada. Un historial bueno responde a por qué se hizo cada cosa, que es lo que no se puede deducir del código.
AlpinaShop adopta Conventional Commits, un formato sencillo con un beneficio grande:
<tipo>(<ámbito>): <resumen en imperativo, menos de 72 caracteres> <cuerpo: POR QUÉ se hizo, no qué se hizo — eso ya está en el diff> <pie: referencias, cambios incompatibles>
feat(buscador): añadir filtro por marca en el catálogo El 18 % de las búsquedas incluían un nombre de marca en el texto libre, lo que producía resultados pobres. El filtro reduce la consulta a la marca antes de aplicar el ranking. Refs: #142
fix(pedidos): reintentar la conexión a Cloud SQL tras un fallo de red El pool de conexiones no recuperaba tras un corte breve y la aplicación devolvía 500 hasta que se reiniciaba el pod. Se añade reintento con retroceso exponencial, coherente con 04-04. Refs: #158
Los tipos habituales: feat (funcionalidad nueva), fix (corrección), docs, refactor, test, chore (mantenimiento), perf y ci.
El beneficio concreto: el formato es analizable por máquinas. De ahí salen automáticamente el registro de cambios y la versión siguiente, según versionado semántico (MAYOR.MENOR.PARCHE):
| Tipo de commit | Incremento | Ejemplo |
|---|---|---|
fix: |
PARCHE | 2.4.1 → 2.4.2 |
feat: |
MENOR | 2.4.1 → 2.5.0 |
BREAKING CHANGE: en el pie o feat!: |
MAYOR | 2.4.1 → 3.0.0 |
Y aquí hay un matiz honesto que conviene decir: el versionado semántico está pensado para artefactos que otros consumen —bibliotecas, APIs públicas—. Para el catálogo web de AlpinaShop, que es un servicio interno desplegado continuamente, la versión importa mucho menos que el SHA del commit, que ya es un identificador exacto. AlpinaShop usa etiquetas semánticas para marcar candidatas a producción —es el disparador por etiqueta de 06-01— y para tener nombres pronunciables en una reunión, no como mecanismo de compatibilidad.
- Ganchos de calidad antes del commit
Un fallo detectado en la CI cuesta minutos de espera. El mismo fallo detectado antes de hacer el commit cuesta segundos. Los ganchos de pre-commit ejecutan comprobaciones rápidas en local antes de que el commit se cree.
# .pre-commit-config.yaml, en la raíz de cada repositorio
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 subir modelos y 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 # errores y estilo
args: [--fix]
- id: ruff-format # formateo automático
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0
hooks:
- id: gitleaks # EL MÁS IMPORTANTE: busca secretospip install pre-commit
pre-commit install # instala el gancho en .git/hooks
pre-commit run --all-files # primera pasada sobre todo el repositorioEl gancho de gitleaks es el que justifica todo el mecanismo por sí solo. Busca patrones de credenciales —claves de API, tokens, claves privadas, claves JSON de GCP— y bloquea el commit si encuentra alguno. Recuerda lo del apartado 8: Git no olvida. Es infinitamente más barato bloquear el commit que rotar una clave de la pasarela de pago filtrada.
Dos advertencias sobre los ganchos locales. La primera: son opcionales por naturaleza, porque cualquiera puede saltárselos con git commit --no-verify. Por eso las mismas comprobaciones deben repetirse en la CI, donde no se pueden esquivar. Los ganchos son comodidad para quien desarrolla, no un control de seguridad. La segunda: deben ser rápidos. Un gancho que tarda treinta segundos hace que todo el mundo aprenda a usar --no-verify. Lo lento —la suite completa de pruebas— va en la CI, no aquí.
- Conectar los repositorios con los disparadores de Cloud Build
El cierre de la lección es unir lo de 06-01 con lo de aquí. Este es el mapa completo de AlpinaShop:
flowchart TD
A[Dani abre PR<br/>feat/filtro-por-marca] --> B[Disparador catalogo-pr]
B --> C[cloudbuild-pr.yaml:<br/>instalar, pytest, ruff, bandit]
C --> D{¿Verde?}
D -->|No| E[PR bloqueado<br/>por protección de rama]
D -->|Sí| F[Marta revisa]
F --> G[Squash a main]
G --> H[Disparador catalogo-main]
H --> I[cloudbuild.yaml:<br/>construir + publicar :SHA]
I --> J[Despliegue en<br/>alpinashop-dev]
J --> K{Etiqueta v2.5.0}
K --> L[Disparador de promoción<br/>con aprobación]
L --> M[alpinashop-prod]
Los cuatro disparadores que AlpinaShop deja configurados:
| Disparador | Repositorio | Evento | Configuración | Qué hace |
|---|---|---|---|---|
catalogo-pr |
alpinashop-catalogo |
PR contra main |
cloudbuild-pr.yaml |
Prueba, no despliega |
catalogo-main |
alpinashop-catalogo |
Push a main |
cloudbuild.yaml |
Construye, publica, despliega en dev |
catalogo-prod |
alpinashop-catalogo |
Etiqueta ^v\d+\.\d+\.\d+$ |
cloudbuild-prod.yaml, --require-approval |
Promociona a producción |
ml-main |
alpinashop-ml |
Push a main |
cloudbuild-ml.yaml |
Compila y lanza el pipeline de Vertex AI |
Y una comprobación final que merece la pena hacer explícita: el disparador de producción se activa al crear una etiqueta, así que quien pueda crear etiquetas puede iniciar un despliegue a producción. La protección de etiquetas es tan necesaria como la de ramas:
# Regla de protección de etiquetas en GitHub Patrón: v* Quién puede crear: solo el equipo @alpinashop/infraestructura
Sin esa regla, la aprobación manual de 06-01 sigue protegiendo el despliegue —nadie llega a producción sin que Marta apruebe—, pero cualquiera podría llenar la cola de compilaciones pendientes. Con ella, el camino a producción está acotado de principio a fin.
Errores Comunes y Consejos
Empezar un proyecto nuevo con Cloud Source Repositories en 2026. Está en desuso, no recibe funcionalidad nueva y carece de revisión de código, que es justo lo que se necesita. Conócelo para operar lo heredado; elige GitHub o GitLab para lo nuevo.
Crear claves JSON de cuenta de servicio para la CI. Credenciales permanentes y transferibles, la causa más común de incidentes graves en GCP. Usa Workload Identity Federation. Y si encuentras claves JSON antiguas por la infraestructura, planifica su eliminación como si fuera deuda de seguridad, porque lo es.
Olvidar id-token: write en los permisos del workflow de GitHub Actions. Sin esa línea no se emite el token OIDC y la autenticación falla con un error confuso. Es el primer tropiezo de todo el mundo.
Ramas de característica que viven semanas. Cada día que una rama vive aumenta la probabilidad de un conflicto doloroso y retrasa la señal de la CI. Ramas cortas e interruptores de funcionalidad para lo que no está listo.
Subir un secreto y "arreglarlo" borrándolo en el commit siguiente. Sigue en el historial y en todos los clones. Rota el secreto inmediatamente; la limpieza del historial es secundaria.
Confiar solo en los ganchos de pre-commit. Se saltan con --no-verify. Repite siempre las comprobaciones en la CI, donde son obligatorias.
Pull requests enormes. Un PR de 2.000 líneas no se revisa, se aprueba. Divide en cambios pequeños con una intención cada uno; la calidad de los comentarios es inversamente proporcional al tamaño del diff.
Discutir estilo en las revisiones. Automatiza el formateo con ruff-format o equivalente y dedica la revisión a lo que una máquina no puede evaluar: si el enfoque es correcto y si es seguro.
No proteger las etiquetas. Si el despliegue a producción se dispara con una etiqueta, quien crea etiquetas inicia despliegues. Protégelas igual que las ramas.
Consejo final: la migración se hace por partes. No intentes llevar los cuatro repositorios a GitHub el mismo día. Empieza por el catálogo, que es el que más se toca, monta su pipeline, comprueba que el equipo se ha adaptado, y después ve con la infraestructura. Y empieza por sacar de Drive los manifiestos de Kubernetes: es la media hora de trabajo con mejor relación beneficio/coste de todo el módulo.
Ejercicios
Ejercicio 1: rescatar el código disperso
Haz un plan concreto de tres fases para llevar al control de versiones todos los activos dispersos de AlpinaShop: el repositorio local de Dani, los startup scripts en los metadatos de las plantillas de instancia, los manifiestos de Kubernetes en Drive, los notebooks de Lucía y los comandos de gcloud de Marta. Para cada activo, indica a qué repositorio va, qué hay que revisar antes de subirlo y qué riesgo concreto tiene esa subida.
Ejercicio 2: configurar el acceso de GitHub Actions con mínimo privilegio
AlpinaShop quiere un workflow de GitHub Actions en alpinashop-datos que, al hacer push a main, valide las consultas SQL ejecutando un dry run contra BigQuery en alpinashop-datos y publique un informe en el bucket alpinashop-datalake. Escribe la configuración de Workload Identity Federation y los roles IAM exactos, y explica qué impide que un workflow del repositorio alpinashop-catalogo use esa misma identidad.
Ejercicio 3: elegir estrategia de ramas con un caso incómodo
AlpinaShop firma un contrato con una cadena de tiendas que necesita una versión personalizada del catálogo, con su propio diseño y algunas reglas de precios distintas, mantenida durante al menos dos años en paralelo a la versión pública. Dani propone crear una rama cliente-x de larga vida. Analiza la propuesta, di si trunk-based sigue siendo válido y propón la solución que recomendarías, con sus contrapartidas.
Soluciones
Solución 1
Fase 1 — Rescatar lo que corre riesgo de perderse (esta semana).
| Activo | Destino | Revisar antes | Riesgo |
|---|---|---|---|
| Repositorio local de Dani | alpinashop-catalogo |
Historial completo en busca de secretos | Alto: si en algún commit antiguo hay una contraseña, subirlo la publica al equipo |
| Manifiestos de Drive | alpinashop-catalogo/k8s/ |
Qué versión es la que está realmente aplicada | Medio: hay tres versiones y no se sabe cuál corre |
| Startup scripts | alpinashop-infra/scripts/ |
Credenciales embebidas en el script | Alto: los startup scripts son un sitio clásico para pegar contraseñas |
Para el repositorio de Dani, el procedimiento correcto es escanear el historial completo, no solo el estado actual:
git clone --mirror /ruta/local/catalogo catalogo-auditoria.git
gitleaks detect --source=catalogo-auditoria.git --report-path=hallazgos.jsonSi aparecen secretos, hay dos caminos. El pragmático: rotar todas las credenciales encontradas y subir el historial tal cual, aceptando que contiene valores ya inválidos. El purista: reescribir el historial con git filter-repo. Para AlpinaShop recomiendo el pragmático —rotar es obligatorio en ambos casos, y la reescritura aporta poco si los valores ya no sirven—, salvo que haya datos personales, donde la reescritura sí es necesaria.
Para los manifiestos, la comprobación imprescindible es qué está aplicado de verdad, no qué fichero se llama FINAL:
Se limpian los campos generados por el clúster (status, metadata.uid, resourceVersion, anotaciones de última configuración) y eso es la versión que va al repositorio. La verdad está en el clúster, no en Drive.
Fase 2 — Estructurar y proteger (semana siguiente). Crear los cuatro repositorios, activar la protección de ramas y CODEOWNERS, instalar los ganchos de pre-commit con gitleaks y conectar los disparadores de Cloud Build.
Fase 3 — Lo difícil (siguientes semanas).
| Activo | Destino | Cómo | Riesgo |
|---|---|---|---|
| Notebooks de Lucía | alpinashop-ml/notebooks/ |
Limpiar salidas con nbstripout; extraer lo reutilizable a módulos |
Alto: los notebooks suelen contener datos de clientes en las salidas |
Comandos de gcloud de Marta |
alpinashop-infra/ como Terraform |
No transcribir comandos: exportar el estado real, 06-05 y 06-07 | Medio: laborioso, pero sin pérdida |
El detalle de los notebooks merece énfasis: una celda ejecutada con df.head() sobre alpinashop_analitica deja nombres, correos y direcciones guardados dentro del .ipynb. Subir un notebook sin limpiar salidas es una fuga de datos personales al repositorio, con Git no olvidando nada. nbstripout como gancho de pre-commit lo resuelve de forma automática y es no negociable en alpinashop-ml.
Y la observación sobre los comandos de Marta: transcribirlos a un script sería el error. Ese historial está incompleto y contiene comandos que se ejecutaron y después se deshicieron. Lo correcto es exportar la configuración real de la infraestructura viva, que es exactamente el procedimiento de 06-05.
Solución 2
Configuración de la federación, reutilizando el pool ya creado:
PROY=alpinashop-datos
NUM=$(gcloud projects describe $PROY --format='value(projectNumber)')
gcloud iam service-accounts create sa-gha-datos \
--display-name="GitHub Actions - validacion SQL" --project=$PROY
# Solo el repositorio alpinashop-datos, y solo la rama 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=$PROYRoles exactos, aplicando mínimo privilegio de verdad:
| Rol | Ámbito | Por qué ese y no otro |
|---|---|---|
roles/bigquery.jobUser |
Proyecto alpinashop-datos |
Permite lanzar trabajos; un dry run no lee datos |
roles/bigquery.metadataViewer |
Dataset alpinashop_analitica |
El dry run necesita los esquemas para validar |
roles/storage.objectCreator |
Bucket alpinashop-datalake, prefijo de informes |
Crear, no leer ni borrar |
Fíjate en lo que no lleva: ni bigquery.dataViewer —un dry run valida sintaxis y esquema, no ejecuta la consulta ni lee una sola fila— ni storage.objectAdmin —crear informes no requiere poder borrar los existentes—. Y el objectCreator acotado al prefijo mediante una condición IAM (03-04) impide que un workflow comprometido sobrescriba datos del data lake.
Qué impide que un workflow de alpinashop-catalogo use esta identidad: el principalSet está atado a attribute.repository/alpinashop/alpinashop-datos. El token OIDC que GitHub emite lleva firmado el nombre del repositorio de origen, y GitHub no permite falsificarlo —lo emite su propio servicio, no el workflow—. Un workflow de alpinashop-catalogo presentaría un token con repository: alpinashop/alpinashop-catalogo, que no coincide con el principalSet, y GCP rechazaría el intercambio. Esa es la propiedad que hace que la federación sea sustancialmente más segura que una clave JSON: la identidad la certifica un tercero de confianza y está atada al contexto de ejecución, no a la posesión de un fichero.
Y una mejora adicional: el attribute-condition del proveedor (assertion.repository_owner == 'alpinashop') actúa como segunda barrera a nivel de proveedor. Sin ella, cualquier repositorio de GitHub en el mundo podría al menos intentar el intercambio, y aunque el principalSet lo rechazaría igualmente, es mejor filtrarlo antes. Defensa en profundidad.
Solución 3
La propuesta de la rama de larga vida es mala, y conviene explicar por qué antes de proponer alternativa. Una rama cliente-x mantenida dos años produce, con total certeza:
- Divergencia creciente: cada corrección de seguridad hay que aplicarla dos veces, y en algún momento una se olvidará. Ese olvido será justo el importante.
- Fusiones cada vez más caras: a los seis meses, traer los cambios de
mainacliente-xes un proyecto en sí mismo. - Duplicación de todo lo demás: dos pipelines, dos conjuntos de imágenes, dos despliegues, dos series de métricas.
- Un sistema que nadie prueba entero: la CI probaría
main, ycliente-xquedaría con pruebas viejas.
Pero trunk-based sigue siendo válido, porque el problema real no es de estrategia de ramas: es de arquitectura. La pregunta correcta no es "¿cómo mantengo dos ramas?", sino "¿cómo hago que una sola base de código sirva a dos clientes?".
La solución recomendada: multi-inquilino por configuración. Una única rama main, una única imagen, y el comportamiento distinto resuelto en tiempo de ejecución:
# catalogo/inquilino.py
@dataclass(frozen=True)
class Inquilino:
id: str
tema: str # plantillas y hojas de estilo
reglas_precio: str # estrategia de precios a aplicar
dominio: str
INQUILINOS = {
"publico": Inquilino("publico", "alpina", "estandar", "www.alpinashop.example"),
"cliente-x": Inquilino("cliente-x", "clientex", "mayorista", "tienda.cliente-x.example"),
}
def inquilino_actual() -> Inquilino:
# Resolución por dominio: el mismo despliegue sirve a los dos
return INQUILINOS.get(DOMINIOS.get(request.host), INQUILINOS["publico"])Lo que cambia por inquilino son datos y configuración: el tema visual, las reglas de precio, el dominio, quizá el subconjunto de catálogo. Nada de eso justifica una rama.
| Enfoque | Coste de mantenimiento | Riesgo de divergencia | Complejidad del código | ¿Recomendado? |
|---|---|---|---|---|
| Rama de larga vida | Muy alto y creciente | Certero | Baja | No |
| Multi-inquilino por configuración | Bajo | Ninguno | Media | Sí |
| Repositorio y producto separados | Muy alto | N/A: son productos distintos | Baja | Solo si divergen de verdad |
| Núcleo compartido como biblioteca | Medio | Media | Alta | Si hay muchos clientes |
Las contrapartidas, dichas con honestidad. El multi-inquilino no es gratis: introduce condicionales en el código, hay que probar los dos caminos, y existe un riesgo nuevo y serio —una fuga entre inquilinos, que el cliente X vea datos del público o al revés—. Eso exige que el identificador de inquilino se aplique en la capa de acceso a datos y se pruebe explícitamente, no que se confíe en que cada consulta se acuerde de filtrar.
Y hay un punto de ruptura que hay que reconocer: si dentro de un año el cliente X pide un flujo de compra completamente distinto, un catálogo con otra estructura y un proceso de facturación propio, ya no son dos configuraciones del mismo producto, sino dos productos. En ese momento la separación es correcta, y se hace como productos separados con una biblioteca común, no como una rama.
La regla que se lleva uno de aquí: las ramas sirven para separar el trabajo en el tiempo, no para separar variantes en el espacio. Cuando alguien propone una rama de larga vida para una variante de producto, la señal es que hay una decisión de arquitectura pendiente de tomar. Y la conversación con comercial también importa: si el contrato con el cliente X permite algo de flexibilidad en el diseño y en las reglas de precio, el multi-inquilino sale casi gratis; si exige un producto completamente distinto, el coste real de ese contrato es mucho mayor de lo que parecía al firmarlo, y eso conviene saberlo antes.
Conclusión
AlpinaShop tiene por fin un cimiento sobre el que construir.
Sabes qué aporta el control de versiones —historia, atribución, reversión, ramificación, revisión, identidad inmutable y automatización— y que las dos últimas son las que hacen posible todo lo de 06-01: sin repositorio remoto no hay $COMMIT_SHA, no hay disparador y no hay pipeline.
Conoces Cloud Source Repositories: repositorios Git privados en GCP, gcloud source repos clone con autenticación por identidad de gcloud, permisos gobernados por IAM con los mismos grupos de 03-04, búsqueda de código e integración nativa con Cloud Build y Error Reporting. Y sabes la verdad incómoda: está en desuso, no admite clientes nuevos, no recibe funcionalidad y nunca tuvo revisión de código, que es lo que de verdad se necesita. Lo conoces para operar infraestructuras heredadas y para migrarlas con git clone --mirror, no para empezar nada nuevo.
Sabes conectar GitHub o GitLab con GCP usando la 2ª generación de repositorios de Cloud Build —conexión y repositorio como recursos declarables, token en Secret Manager— y por qué es preferible a la aplicación de GitHub de la primera generación. Y sobre todo sabes usar Workload Identity Federation para que las GitHub Actions accedan a GCP sin ninguna clave JSON: el pool, el proveedor OIDC, el attribute-condition, el principalSet atado a un repositorio y a una rama, y el id-token: write que todo el mundo olvida. Con la regla que resume el apartado: si en 2026 estás creando una clave JSON de cuenta de servicio, casi siempre hay una alternativa mejor.
Tienes la tabla de decisión honesta entre CSR, GitHub y GitLab, y la decisión de AlpinaShop —GitHub— con sus cuatro razones, junto a los casos en los que GitLab sería mejor elección. Tienes la estrategia de ramas: trunk-based con ramas cortas, porque es un servicio web con una sola versión viva, porque el equipo son tres personas, porque es la premisa misma de la integración continua y porque los conflictos crecen de forma no lineal con el tiempo. Con los interruptores de funcionalidad como respuesta a "esto tarda tres semanas", y con la disciplina de borrarlos después.
Sabes qué va al repositorio —código, Dockerfile, pipelines, manifiestos, Terraform, pruebas, migraciones, decisiones de arquitectura— y qué no —secretos, estado de Terraform, datos, artefactos, ficheros grandes—, con la advertencia que vale por toda la lección: Git no olvida, y ante un secreto filtrado la única respuesta correcta es rotarlo. Tienes la estructura de cuatro repositorios de AlpinaShop dividida por ritmo y responsabilidad, con alpinashop-infra señalado como el más peligroso. Tienes la protección de ramas con sus siete reglas, CODEOWNERS exigiendo revisión de seguridad para IAM y firewall, los mensajes de commit convencionales, el versionado semántico con su matiz honesto, y los ganchos de pre-commit con gitleaks como red de seguridad que no sustituye a la CI.
Y tienes los cuatro disparadores conectados de principio a fin: pull request que prueba, main que construye y despliega en desarrollo, etiqueta protegida que promociona a producción con aprobación, y el pipeline de ML compilándose solo.
Con esto, el catálogo se construye, se prueba y se despliega solo. Pero quedan piezas del sistema que no son ni una aplicación web ni un pipeline de datos: son reacciones a eventos. Cuando llega un mensaje al topic imagenes-subidas, algo tiene que despertar, llamar a la Vision API, escribir en BigQuery y volver a dormirse. Montar una máquina virtual o un pod para eso sería absurdo.
En 06-03 llegan las Cloud Functions, y con ellas se cierra por fin la punta suelta que dejó el módulo 5.
Curso de Google Cloud Platform (GCP)
Módulo 1: Introducción a Google Cloud Platform
- ¿Qué es Google Cloud Platform?
- Configuración de tu cuenta de GCP
- Descripción general de la consola de GCP
- Proyectos, jerarquía de recursos y facturación
- Regiones, zonas y modelo de responsabilidad compartida
- Cloud Shell y la CLI de gcloud
Módulo 2: Servicios principales de GCP
- Compute Engine: máquinas virtuales en Google Cloud
- Cloud Storage: almacenamiento de objetos
- Cloud SQL: bases de datos relacionales gestionadas
- App Engine: plataforma como servicio
- Google Kubernetes Engine (GKE)
- Bases de datos NoSQL: Firestore, Bigtable y Spanner
- Cómo elegir el servicio de cómputo adecuado
Módulo 3: Redes y seguridad
- Redes VPC
- Balanceo de carga en la nube
- Cloud CDN
- Gestión de identidad y acceso (IAM)
- Cloud Armor
- Secretos y cifrado: Secret Manager y Cloud KMS
- Cloud DNS, certificados TLS y publicación segura de servicios
Módulo 4: Datos y análisis
- BigQuery: el almacén de datos analítico
- Cloud Dataflow: procesamiento de datos por lotes y en streaming
- Cloud Dataproc: Spark y Hadoop gestionados
- Cloud Pub/Sub: mensajería asíncrona
- Cloud Data Fusion: integración de datos sin código
- Orquestación de pipelines con Cloud Composer y Workflows
- Gobierno del dato y cuadros de mando con Dataplex y Looker Studio
Módulo 5: Aprendizaje automático e IA
- Vertex AI: la plataforma de machine learning de GCP
- AutoML: modelos a medida sin escribir código
- TensorFlow en GCP: entrenamiento y servicio de modelos
- API de lenguaje natural
- API de visión
- IA generativa en Vertex AI: modelos Gemini y embeddings
- MLOps: del modelo al producto con Vertex AI Pipelines
Módulo 6: DevOps y monitoreo
- Cloud Build: integración continua en GCP
- Cloud Source Repositories y gestión del código fuente
- Cloud Functions: funciones sin servidor
- Cloud Monitoring (antes Stackdriver): métricas, paneles y alertas
- Cloud Deployment Manager e infraestructura como código nativa
- Cloud Logging y Cloud Trace: logs, trazas y diagnóstico
- Terraform en GCP: infraestructura como código en la práctica
Módulo 7: Temas avanzados de GCP
- Híbrido y multinube con Anthos
- Computación sin servidor con Cloud Run
- Redes avanzadas: VPC compartida, peering y conectividad híbrida
- Mejores prácticas de seguridad
- Gestión y optimización de costos
- Fiabilidad: SLO, alta disponibilidad y recuperación ante desastres
- Gobierno a escala: organización, políticas y auditoría
