---
name: gcp-cloud
description: >
  Google Cloud Platform: Cloud Run para contenedores serverless, GKE para
  Kubernetes, Cloud SQL con proxy, BigQuery para analytics, Pub/Sub para
  mensajería async e IAM con mínimo privilegio. Cargar cuando se despliegue en
  GCP, se configure infraestructura GCP con Terraform, se diseñen pipelines con
  BigQuery o Dataflow, o se configure observabilidad con Cloud Monitoring.
version: "1.0.0"
herramientasPermitidas: [Read, Bash]
evolvable: true  # default para skill estandar
exclusiones:
  - "No cargar para despliegues en AWS o Azure — para AWS cargar `cloud-aws`, para Azure cargar `azure-cloud`."
  - "No cargar para configurar pipelines de ML con Vertex AI, AutoML o Vertex Pipelines — para ML en GCP usar la documentación de Vertex AI directamente."
  - "No cargar para análisis de costos GCP detallado o compromisos de uso (committed use discounts) — para optimización de costos usar GCP Billing directamente."
  - "No cargar para configurar Firebase (Firestore, Auth, Hosting para apps móviles) — Firebase tiene su propio modelo de deployment; para Firebase cargar el skill correspondiente o usar su documentación."
---
# GCP Cloud — Google Cloud Platform

Skill para desplegar y operar sistemas en Google Cloud Platform. Cubre Cloud Run,
GKE, Cloud SQL, BigQuery, Pub/Sub, Cloud Functions e IAM. Para patrones AWS ver
`Skill("cloud-aws")`.

---

## Cuándo NO cargar

- La tarea es desplegar en AWS: cargar `cloud-aws`.
- La tarea es desplegar en Azure: cargar `azure-cloud`.
- La tarea es configurar Vertex AI o pipelines de ML: usar la documentación de Vertex AI directamente.
- La tarea es Firebase (Firestore, Auth, Hosting): usar la documentación de Firebase directamente.

## Cuándo cargar este skill

- Al desplegar servicios en Cloud Run o configurar GKE
- Al conectar a Cloud SQL desde código Python o Node
- Al diseñar pipelines de datos con BigQuery o Dataflow
- Al configurar Pub/Sub para comunicación async entre servicios
- Al definir IAM roles, service accounts o Workload Identity
- Al escribir Terraform para infraestructura GCP
- Al configurar observabilidad con Cloud Monitoring y Cloud Logging

---

## 1. Cloud Run — despliegue de APIs serverless

```yaml
# service.yaml para Cloud Run
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: mi-api
  annotations:
    run.googleapis.com/ingress: internal-and-cloud-load-balancing
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "1"
        autoscaling.knative.dev/maxScale: "10"
        run.googleapis.com/cloudsql-instances: proyecto:region:instancia
    spec:
      serviceAccountName: mi-api-sa@proyecto.iam.gserviceaccount.com
      containers:
        - image: gcr.io/proyecto/mi-api:latest
          resources:
            limits:
              cpu: "1"
              memory: 512Mi
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: database-url
                  key: latest
```

**Puntos clave de Cloud Run:**
- `minScale: "1"` para APIs que no toleran cold starts (pago mínimo constante)
- `minScale: "0"` solo para workloads con latencia tolerable en arranque en frío
- Los secretos SIEMPRE vienen de Secret Manager vía `secretKeyRef`, nunca hardcodeados
- `run.googleapis.com/cloudsql-instances` activa el Cloud SQL Auth Proxy automático

---

## 2. Cloud SQL — conexión con Python Connector

```python
# SQLAlchemy con Cloud SQL Python Connector (sin abrir puertos a internet)
from google.cloud.sql.connector import Connector
import sqlalchemy

connector = Connector()

def getconn():
    return connector.connect(
        "proyecto:region:instancia",  # Instance connection name
        "pg8000",
        user="mi_usuario",
        password="mi_contraseña",
        db="mi_base",
    )

engine = sqlalchemy.create_engine(
    "postgresql+pg8000://",
    creator=getconn,
    pool_size=5,
    max_overflow=2,
    pool_timeout=30,
)
```

**Regla**: NUNCA exponer Cloud SQL con IP pública sin autorización de red. Usar
siempre Cloud SQL Auth Proxy (automático en Cloud Run) o el Python Connector.

---

## 3. BigQuery — queries parametrizadas para analytics

```python
from google.cloud import bigquery

client = bigquery.Client()

# BIEN: parámetros explícitos (previene inyección)
query = """
    SELECT usuario_id, SUM(monto) AS total_ventas
    FROM `proyecto.dataset.ventas`
    WHERE fecha >= @fecha_inicio
      AND region = @region
    GROUP BY usuario_id
    ORDER BY total_ventas DESC
    LIMIT 100
"""
job_config = bigquery.QueryJobConfig(
    query_parameters=[
        bigquery.ScalarQueryParameter("fecha_inicio", "DATE", "2026-01-01"),
        bigquery.ScalarQueryParameter("region", "STRING", "MX"),
    ]
)
resultados = client.query(query, job_config=job_config).result()

for fila in resultados:
    print(f"Usuario {fila.usuario_id}: ${fila.total_ventas:,.2f}")
```

**Anti-patrón**: NUNCA usar f-strings con inputs de usuario en queries BigQuery.
Siempre usar `QueryJobConfig` con `query_parameters`.

---

## 4. Pub/Sub — mensajería async entre servicios

```python
from google.cloud import pubsub_v1
import json

# Publicar evento
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path("proyecto", "mi-topico")

future = publisher.publish(
    topic_path,
    json.dumps({"evento": "pago_completado", "orden_id": "123"}).encode("utf-8"),
    origen="api-pagos",           # atributos de mensaje para filtrado
    version="1",
)
future.result()  # Bloquea hasta confirmar publicación

# Procesar en suscriptor (Cloud Run o Cloud Function con HTTP push)
def procesar_mensaje(message: pubsub_v1.subscriber.message.Message) -> None:
    datos = json.loads(message.data.decode("utf-8"))
    try:
        procesar_pago(datos)
        message.ack()
    except Exception:
        message.nack()  # Reencola para reintentos
```

---

## 5. IAM — mínimo privilegio por servicio

```bash
# Crear service account dedicado por servicio
gcloud iam service-accounts create mi-api-sa \
    --display-name="Service account para mi-api" \
    --project=$PROJECT_ID

# Otorgar solo los permisos necesarios
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:mi-api-sa@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/cloudsql.client"

gcloud secrets add-iam-policy-binding database-url \
    --member="serviceAccount:mi-api-sa@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/secretmanager.secretAccessor"
```

---

## 6. Reglas obligatorias

| Regla | Justificación | Verificación |
|-------|--------------|--------------|
| Service account dedicado por servicio | La SA por defecto tiene permisos excesivos | Verificar que `serviceAccountName` está en cada deployment |
| Secretos en Secret Manager, no en env vars | Las env vars aparecen en logs y dashboards | Buscar `secretKeyRef` en lugar de valores hardcodeados |
| NUNCA usar roles primitivos en producción | Owner/Editor tienen acceso total al proyecto | Usar roles predefinidos granulares o custom roles |
| Etiquetas en todos los recursos | Sin etiquetas no se puede analizar el costo por servicio | `gcloud ... --labels=env=prod,equipo=backend` |
| Workload Identity para GKE | Las claves de SA en archivos son un riesgo de seguridad | Ver [recursos/gke.md](recursos/gke.md) |

---

## 7. Anti-patrones

| MAL | BIEN |
|-----|------|
| Credenciales JSON de SA montadas como archivo en contenedor | Workload Identity o Application Default Credentials |
| `gcloud auth application-default` en producción | Service account con mínimo privilegio |
| Cloud SQL con IP pública sin autorización de red | Cloud SQL Auth Proxy o Python Connector |
| `roles/editor` o `roles/owner` para un servicio | Roles granulares: `roles/cloudsql.client` |
| f-strings con inputs de usuario en BigQuery | `QueryJobConfig` con `query_parameters` |
| `minScale: "0"` en API REST pública | `minScale: "1"` para evitar cold starts |

---

## 8. Checklist de verificación

- [ ] Cada servicio tiene su propio service account (no el SA por defecto)
- [ ] Los secretos vienen de Secret Manager vía `secretKeyRef`
- [ ] No hay credenciales en variables de entorno planas ni en el código
- [ ] Cloud SQL usa Auth Proxy (Cloud Run) o Python Connector (GKE/local)
- [ ] IAM usa roles predefinidos o custom, nunca Owner/Editor en producción
- [ ] Todos los recursos tienen etiquetas `env`, `equipo` y `servicio`
- [ ] Queries BigQuery usan `query_parameters`, nunca concatenación de strings
- [ ] Cloud Run tiene `minScale` explícito según tolerancia a cold starts

---

## 9. Referencias

| Tema | Recurso |
|------|---------|
| GKE: clusters, node pools, Workload Identity, HPA | [recursos/gke.md](recursos/gke.md) |
| Terraform para GCP: providers, módulos, state remoto | [recursos/terraform-gcp.md](recursos/terraform-gcp.md) |
| Cloud Run pricing y cuotas | [cloud.google.com/run/pricing](https://cloud.google.com/run/pricing) |
| IAM roles predefinidos completos | [cloud.google.com/iam/docs/understanding-roles](https://cloud.google.com/iam/docs/understanding-roles) |
| Secret Manager guía | [cloud.google.com/secret-manager/docs](https://cloud.google.com/secret-manager/docs) |

---

## Gotchas / Errores comunes no obvios

**Cloud Run con `minScale: "0"` tiene cold start de 2-8 segundos para imágenes Python con dependencias pesadas (pandas, sqlalchemy, cryptography), y el primer request del usuario experimenta un timeout 504 si el health check de Cloud Run expira antes de que la app esté lista**: una imagen de 800MB con FastAPI + pandas tarda ~6 segundos en arrancar; el timeout default del load balancer de Cloud Run es 60s pero el timeout de startup del contenedor es 240s. Sin embargo, si el primer request llega mientras el contenedor arranca y el cliente tiene un timeout de 5s, el request falla. Causa: `minScale: "0"` es incompatible con SLAs de latencia estrictos para APIs sincrónicas. Fix: para APIs con latencia requerida < 2s, usar `minScale: "1"`. Para reducir cold start con `minScale: "0"`, optimizar la imagen: usar imagen base `python:3.12-slim`, separar las dependencias pesadas en layers, y considerar instancias precalentadas de Cloud Run (preview feature).

**`gcloud iam service-accounts create` crea el service account en el proyecto correcto pero `gcloud secrets add-iam-policy-binding` falla con `PERMISSION_DENIED` porque el binding se hace sobre el secreto del proyecto A desde un context de proyecto B cuando hay múltiples proyectos en el PATH**: un desarrollador que tiene `gcloud config set project proyecto-dev` en su shell y ejecuta el script de setup hace el binding en `proyecto-dev` en lugar de `proyecto-prod` donde vive el secreto. Causa: `gcloud` usa el proyecto configurado en el contexto activo como default, y los comandos de IAM de recursos específicos (secrets, buckets) no siempre verifican que el recurso pertenece al proyecto configurado. Fix: siempre especificar `--project` explícitamente en comandos de IAM sobre recursos: `gcloud secrets add-iam-policy-binding database-url --project=proyecto-prod --member=...`. En scripts de IaC, hardcodear el project ID en lugar de depender del contexto activo.

**El Cloud SQL Python Connector con `pool_size=5` en Cloud Run causa `TimeoutError: QueuePool limit reached` bajo carga porque cada instancia de Cloud Run crea su propio pool de 5 conexiones y con 20 instancias se generan 100 conexiones simultáneas, saturando el límite de conexiones de Cloud SQL (default 100)**: durante un pico de tráfico con auto-scaling a 20 instancias Cloud Run, Cloud SQL comienza a rechazar nuevas conexiones con error `FATAL: too many connections`. La aplicación reporta 503 aunque el CPU y memoria de Cloud SQL estén al 30%. Causa: Cloud Run escala instancias independientemente y cada instancia tiene su propio pool; el número de conexiones totales = instancias × pool_size. Fix: usar `pool_size=2, max_overflow=1` para Cloud Run (máximo 3 conexiones por instancia), y aumentar `max_connections` en Cloud SQL según `instancias_max × 3 + buffer`. Considerar usar `pgBouncer` o Cloud SQL Proxy con pooling habilitado para workloads con muchas instancias.

**`future.result()` en Pub/Sub publisher bloquea el thread hasta confirmación de publicación y en código síncrono dentro de Cloud Run causa que el request quede colgado si el tópico de Pub/Sub tiene alta latencia o está temporalmente indisponible**: una función que publica 50 eventos por request con `[publisher.publish(...).result() for evento in eventos]` tarda el tiempo total de las 50 confirmaciones en serie, convirtiendo una operación de 200ms en 2-3 segundos cuando Pub/Sub tiene latencia alta. Causa: `future.result()` es una operación bloqueante síncrona; publicar en serie espera la confirmación de cada mensaje antes de enviar el siguiente. Fix: recolectar todos los futures primero y esperar las confirmaciones al final: `futures = [publisher.publish(topic_path, data) for data in payloads]`, luego `[f.result() for f in futures]`. Para volúmenes mayores, usar `PublisherOptions(enable_message_ordering=False)` y procesar los futures con `concurrent.futures.as_completed()`.
