Lab práctico · Semana 10: Entrega e implementación — CI/CD, infraestructura como código, APIs y herramientas
CI/CD con Cloud Build, Artifact Registry y Cloud Deploy
Qué vas a construir
Un pipeline completo de entrega: Cloud Build prueba y construye una imagen de contenedor, la sube a Artifact Registry y crea una release en Cloud Deploy, que la despliega automáticamente en el entorno dev de Cloud Run. Después promocionarás la release a prod, que exige aprobación, y harás un rollback.
flowchart LR
SRC["Código en Cloud Shell (~/lab21)"] -->|"gcloud builds submit"| CB["Cloud Build: test + build + push"]
CB -->|"imagen"| AR["Artifact Registry: apps/app"]
CB -->|"gcloud deploy releases create"| CD["Cloud Deploy: app-pipeline"]
CD -->|"rollout automático"| DEV["Cloud Run: app-dev"]
DEV -->|"promote + approve"| PROD["Cloud Run: app-prod"]
Antes de empezar
- Proyecto dedicado (por ejemplo
pca-lab-21) con facturación. Haber hecho el lab 4 (Cloud Run) ayuda. - En Cloud Shell:
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
export REGION=europe-southwest1
export SA=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com
gcloud services enable cloudbuild.googleapis.com artifactregistry.googleapis.com \
clouddeploy.googleapis.com run.googleapis.com compute.googleapis.com
Repositorio de ejemplo: para no depender de una cuenta de GitHub, el «repositorio» es una carpeta en Cloud Shell y lanzas el pipeline con gcloud builds submit. En un proyecto real conectarías GitHub, GitLab, Bitbucket o Secure Source Manager y crearías un trigger para que cada push a main ejecute lo mismo (lo verás al final como ampliación opcional).
Coste: Cloud Build tiene 2.500 minutos gratis al mes por cuenta de facturación; el primer delivery pipeline activo con varios targets es gratuito; Artifact Registry incluye 0,5 GB gratis; Cloud Run escala a cero (precios de septiembre de 2026).
Paso 1: permisos de la cuenta de servicio
En proyectos nuevos, tanto Cloud Build como Cloud Deploy usan por defecto la cuenta de servicio predeterminada de Compute Engine. Le damos solo los roles necesarios (en producción usarías cuentas de servicio dedicadas, una para el build y otra para el despliegue):
for ROL in roles/cloudbuild.builds.builder roles/artifactregistry.writer \
roles/logging.logWriter roles/clouddeploy.releaser roles/clouddeploy.jobRunner \
roles/run.developer roles/iam.serviceAccountUser; do
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA --role=$ROL --condition=None --quiet > /dev/null
done
cloudbuild.builds.builder,artifactregistry.writer,logging.logWriter: construir, subir la imagen y escribir los logs del build.clouddeploy.releaser: que el build pueda crear releases.clouddeploy.jobRunner,run.developer,iam.serviceAccountUser: que Cloud Deploy pueda renderizar y desplegar en Cloud Run (los tres que pide la guía de inicio de Cloud Deploy para Cloud Run).
Paso 2: repositorio de Artifact Registry
gcloud artifacts repositories create apps \
--repository-format=docker --location=$REGION \
--description="Imágenes del lab 21"
Paso 3: la aplicación y su prueba
mkdir -p ~/lab21 && cd ~/lab21
cat > main.py <<'EOF'
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
VERSION = "v1"
def saludo():
return f"Hola desde {os.environ.get('ENTORNO', 'local')} - {VERSION}\n"
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(saludo().encode())
if __name__ == "__main__":
HTTPServer(("", int(os.environ.get("PORT", 8080))), Handler).serve_forever()
EOF
cat > test_main.py <<'EOF'
import unittest
from main import saludo
class TestSaludo(unittest.TestCase):
def test_contiene_version(self):
self.assertIn("v", saludo())
if __name__ == "__main__":
unittest.main()
EOF
cat > Dockerfile <<'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY main.py .
CMD ["python", "main.py"]
EOF
Paso 4: manifiestos de Cloud Run y Skaffold
Cloud Deploy renderiza los manifiestos con Skaffold. Usamos dos perfiles (dev y prod) para tener dos servicios en el mismo proyecto; con proyectos distintos por entorno (lo recomendable en producción) a menudo bastaría uno. app-image es un marcador que Cloud Deploy sustituye por la imagen concreta de cada release.
cat > run-dev.yaml <<'EOF'
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: app-dev
spec:
template:
spec:
containers:
- image: app-image
env:
- name: ENTORNO
value: dev
EOF
cat > run-prod.yaml <<'EOF'
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: app-prod
spec:
template:
spec:
containers:
- image: app-image
env:
- name: ENTORNO
value: prod
EOF
cat > skaffold.yaml <<'EOF'
apiVersion: skaffold/v4beta7
kind: Config
metadata:
name: app
profiles:
- name: dev
manifests:
rawYaml:
- run-dev.yaml
- name: prod
manifests:
rawYaml:
- run-prod.yaml
deploy:
cloudrun: {}
EOF
Paso 5: delivery pipeline y targets
Dos targets en la misma región; prod exige aprobación.
cat > clouddeploy.yaml <<EOF
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
name: app-pipeline
description: Pipeline del lab 21
serialPipeline:
stages:
- targetId: dev
profiles: [dev]
- targetId: prod
profiles: [prod]
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: dev
description: Entorno de desarrollo
run:
location: projects/$PROJECT_ID/locations/$REGION
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: prod
description: Entorno de producción
requireApproval: true
run:
location: projects/$PROJECT_ID/locations/$REGION
EOF
gcloud deploy apply --file=clouddeploy.yaml --region=$REGION
Fíjate en que aquí el heredoc va sin comillas (<<EOF) para que Cloud Shell sustituya $PROJECT_ID y $REGION. Comprueba en la consola, en Cloud Deploy > Delivery pipelines, que aparece app-pipeline con sus dos targets.
Paso 6: el pipeline de Cloud Build
cat > cloudbuild.yaml <<'EOF'
steps:
# 1. Pruebas unitarias: si fallan, el build se detiene y no se despliega nada
- id: test
name: 'python:3.12-slim'
entrypoint: 'python'
args: ['-m', 'unittest', '-v']
# 2. Construir la imagen
- id: build
name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', '${_REGION}-docker.pkg.dev/$PROJECT_ID/apps/app:$BUILD_ID', '.']
# 3. Subirla a Artifact Registry
- id: push
name: 'gcr.io/cloud-builders/docker'
args: ['push', '${_REGION}-docker.pkg.dev/$PROJECT_ID/apps/app:$BUILD_ID']
# 4. Crear la release en Cloud Deploy (se despliega sola en el primer target: dev)
- id: release
name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
entrypoint: 'gcloud'
args:
- 'deploy'
- 'releases'
- 'create'
- 'rel-$BUILD_ID'
- '--delivery-pipeline=app-pipeline'
- '--region=${_REGION}'
- '--images=app-image=${_REGION}-docker.pkg.dev/$PROJECT_ID/apps/app:$BUILD_ID'
substitutions:
_REGION: europe-southwest1
options:
logging: CLOUD_LOGGING_ONLY
EOF
Lanza el build (si cambiaste de región, pásala también como sustitución):
gcloud builds submit --region=$REGION --config=cloudbuild.yaml \
--substitutions=_REGION=$REGION
Verás la salida de cada paso: pruebas, build, push y creación de la release. La creación de la release lanza el rollout a dev.
Paso 7: comprueba dev
Espera un par de minutos y consulta el estado:
export REL=$(gcloud deploy releases list --delivery-pipeline=app-pipeline \
--region=$REGION --sort-by=~createTime --limit=1 --format='value(name.basename())')
echo $REL
gcloud deploy rollouts list --release=$REL --delivery-pipeline=app-pipeline \
--region=$REGION --format='table(name.basename(),targetId,state)'
Cuando el rollout de dev esté en SUCCEEDED, llama al servicio. No es público: usas un token de identidad de tu usuario (propietario del proyecto, con permiso para invocar).
URL_DEV=$(gcloud run services describe app-dev --region=$REGION --format='value(status.url)')
curl -s -H "Authorization: Bearer $(gcloud auth print-identity-token)" $URL_DEV
Debe responder Hola desde dev - v1.
Paso 8: promoción a prod con aprobación
Promociona la misma release (misma imagen, ya probada en dev) al siguiente target:
gcloud deploy releases promote --release=$REL --delivery-pipeline=app-pipeline \
--region=$REGION --to-target=prod
Como prod tiene requireApproval: true, el rollout queda en PENDING_APPROVAL. Apruébalo (en un equipo real lo haría otra persona con el rol roles/clouddeploy.approver):
ROLLOUT_PROD=$(gcloud deploy rollouts list --release=$REL --delivery-pipeline=app-pipeline \
--region=$REGION --filter='targetId=prod' --format='value(name.basename())')
gcloud deploy rollouts approve $ROLLOUT_PROD --release=$REL \
--delivery-pipeline=app-pipeline --region=$REGION
Por consola es más visual: en el pipeline, pulsa Promote en el target dev y después Review / Approve en prod.
URL_PROD=$(gcloud run services describe app-prod --region=$REGION --format='value(status.url)')
curl -s -H "Authorization: Bearer $(gcloud auth print-identity-token)" $URL_PROD
Debe responder Hola desde prod - v1.
Paso 9: nueva versión y rollback
Cambia la versión y vuelve a pasar por el pipeline:
sed -i 's/VERSION = "v1"/VERSION = "v2"/' main.py
gcloud builds submit --region=$REGION --config=cloudbuild.yaml --substitutions=_REGION=$REGION
Cuando dev responda v2, promociónala y apruébala en prod repitiendo el paso 8 (recalcula REL con el comando del paso 7). Después imagina que v2 tiene un fallo en producción y revierte:
gcloud deploy targets rollback prod --delivery-pipeline=app-pipeline --region=$REGION
El rollback crea un nuevo rollout de la release anterior que funcionaba en prod. Si queda en PENDING_APPROVAL por la aprobación obligatoria del target, apruébalo desde la consola (pipeline > target prod > Review) o con gcloud deploy rollouts approve, igual que en el paso 8 pero con la release anterior. app-prod vuelve a responder v1.
Comprueba que funciona
- En Cloud Build > History ves los builds con sus cuatro pasos en verde.
- En Artifact Registry > apps > app hay dos imágenes (una por build), etiquetadas con el ID del build.
- En Cloud Deploy > app-pipeline la visualización muestra las releases, los rollouts de dev y prod, la aprobación y el rollback, todo con su autor y hora (auditoría de cambios).
app-devyapp-prodresponden con la versión esperada.
Ampliación opcional: trigger desde GitHub
Si tienes cuenta de GitHub, sube ~/lab21 a un repositorio, conéctalo en Cloud Build > Repositories (2nd gen) y crea un trigger de tipo push to branch sobre ^main$ que use cloudbuild.yaml, indicando la cuenta de servicio $SA. Cada push ejecutará el pipeline completo hasta dev. Añade un trigger de pull request que solo ejecute el paso de pruebas para validar los cambios antes de fusionarlos.
Ampliación opcional: canary en prod
Añade una estrategia canary a la etapa prod de clouddeploy.yaml y vuelve a aplicar el fichero:
- targetId: prod
profiles: [prod]
strategy:
canary:
runtimeConfig:
cloudRun:
automaticTrafficControl: true
canaryDeployment:
percentages: [25]
verify: falseUna nueva promoción desplegará primero al 25 % del tráfico; avanzas a la fase estable (100 %) con Advance rollout en la consola o con gcloud deploy rollouts advance. Si es el primer despliegue en ese target, Cloud Deploy se salta las fases canary (no hay versión anterior con la que repartir el tráfico).
Limpieza
La forma más segura es borrar el proyecto (gcloud projects delete $PROJECT_ID). Si prefieres conservarlo:
cd ~/lab21
gcloud run services delete app-dev --region=$REGION --quiet
gcloud run services delete app-prod --region=$REGION --quiet
gcloud deploy delete --file=clouddeploy.yaml --region=$REGION --force
gcloud artifacts repositories delete apps --location=$REGION --quiet
gcloud storage ls
El último comando lista los buckets que crearon Cloud Build (PROJECT_ID_cloudbuild) y Cloud Deploy (uno termina en _clouddeploy y otro contiene deploy-artifacts): bórralos con gcloud storage rm -r gs://NOMBRE_DEL_BUCKET. Por último, retira los roles que diste a la cuenta de servicio con gcloud projects remove-iam-policy-binding (mismos parámetros que en el paso 1).
Preguntas para pensar como arquitecto
¿Por qué se promueve la misma release a prod en lugar de volver a construir la imagen?
Porque lo que se probó en dev debe ser exactamente lo que llega a prod. Reconstruir podría producir un artefacto distinto (dependencias actualizadas, otra imagen base) que nadie ha probado. Cloud Deploy fija las imágenes en la release (idealmente por digest) y solo cambia la configuración propia de cada entorno (en el lab, la variable ENTORNO). Es el principio de «construir una vez, desplegar muchas».
¿Qué separarías en una empresa regulada (por ejemplo, EHR Healthcare)?
Proyectos distintos para dev, staging y prod; cuentas de servicio distintas para construir y para desplegar, cada una con el mínimo privilegio; aprobación obligatoria en prod por un grupo distinto del que desarrolla (separación de funciones); Binary Authorization para que solo se desplieguen imágenes construidas por el pipeline y sin vulnerabilidades críticas; y los audit logs de Cloud Deploy y Cloud Build como evidencia para auditoría.
El build necesita descargar dependencias de un Artifactory interno accesible solo por VPN. ¿Qué cambias?
Ejecutar el build en un private pool de Cloud Build conectado a la VPC que tiene la conectividad híbrida (VPN o Interconnect) hacia el Artifactory. El default pool solo tiene acceso a Internet público. Alternativa complementaria: un repositorio remote de Artifact Registry como caché, si el origen lo permite.
¿Qué ventaja tiene Cloud Deploy frente a añadir gcloud run deploy como último paso de Cloud Build?
gcloud run deploy en el build funciona para un entorno, pero no da una progresión modelada de entornos, aprobaciones, promociones, historial auditable de qué versión está en cada entorno, rollback con un comando ni estrategias canary gestionadas. Cloud Deploy separa CI (construir y probar) de CD (entregar y promover), que es lo que el examen espera cuando habla de gestión de versiones y despliegues.