Hay una conversación que AlpinaShop lleva siete módulos aplazando, y que la lección anterior ha vuelto a poner sobre la mesa tres veces: activar los logs de acceso a datos cuesta, Security Command Center Premium se descartó por precio, y tres mil euros se repartieron como si fueran mucho dinero — que para una empresa de cuarenta personas lo son.

Ninguna de esas decisiones se puede tomar bien sin saber de dónde sale cada euro de la factura. Y hoy nadie en AlpinaShop lo sabe.

La factura de julio fueron 1.058 €. La de enero fueron 210 €. Nadie decidió multiplicarla por cinco: fue creciendo lección a lección, servicio a servicio, sin que ninguna decisión concreta pareciera cara. Cuando Marta abrió el desglose para preparar el presupuesto del año siguiente, se encontró con dos partidas que no supo explicar y que juntas eran el 40 % del total.

Esta lección abre esa factura entera. Al terminar sabrás por qué la nube sorprende, entenderás qué es un SKU y cómo consultar la facturación en BigQuery para responder "quién gasta qué", montarás presupuestos que actúan en lugar de solo avisar, conocerás las palancas de ahorro ordenadas por retorno real, cazarás los gastos fantasma con un script, y verás la factura de AlpinaShop antes y después, desglosada y comentada con honestidad — incluido lo que no se pudo ahorrar y lo que se decidió gastar de más a propósito.

Contenido

  1. Por qué la factura de la nube sorprende
  2. Entender la factura: SKU, uso y descuentos
  3. La exportación de facturación a BigQuery
  4. Las consultas que responden "quién gasta qué"
  5. FinOps para una pyme: informar, optimizar, operar
  6. Presupuestos, alertas y acciones automáticas
  7. Cuotas como límite duro
  8. Palancas de ahorro ordenadas por retorno
  9. El coste oculto del tráfico
  10. Etiquetado y asignación de costes
  11. Detección de anomalías y gastos fantasma
  12. El caso completo de AlpinaShop: antes y después
  13. Herramientas: Recommender, Active Assist y la calculadora

  1. Por qué la factura de la nube sorprende

En el modelo tradicional, el gasto en infraestructura era una decisión anual, visible y negociada: se compraban servidores, alguien firmaba, y durante tres años el coste era el mismo cada mes. Era caro, rígido e ineficiente — y predecible.

En la nube ocurre lo contrario, y la diferencia se puede resumir en cuatro puntos:

Factor Efecto
Coste variable La factura depende del uso, y el uso depende del tráfico, de los usuarios y de los errores
Decisiones técnicas con efecto económico Un SELECT * de Lucía puede costar más que el servidor donde corre la tienda
Sin freno natural Nadie firma una orden de compra para crear un clúster. Se crea en dos clics
Desfase temporal El gasto de hoy se ve en la factura de dentro de un mes, cuando ya se hizo

El tercer punto es el decisivo, y merece un ejemplo real. En 04-06, para evaluar Cloud Composer frente a Workflows, Marta creó un entorno de Composer. La evaluación duró dos días, la decisión fue Workflows, y nadie borró el entorno. Cloud Composer no escala a cero: mantiene una infraestructura permanente. Ese entorno lleva cuatro meses funcionando sin que nadie lo use, a unos 280 € al mes.

Nadie decidió gastar 1.120 €. Simplemente, nadie decidió no gastarlos.

La primera regla de la gestión de costes en la nube: el problema casi nunca es que algo sea caro. Es que nadie sabía que estaba encendido.

  1. Entender la factura: SKU, uso y descuentos

La factura de Google Cloud tiene tres niveles de agregación, y hay que saber leerlos en orden inverso al que aparecen.

Nivel Ejemplo Utilidad
Servicio Compute Engine Demasiado grueso: dice qué pero no por qué
SKU "N1 Predefined Instance Core running in EMEA" El nivel útil: unidad facturable concreta
Uso 1.460 horas × 0,0348 $ El detalle

Un SKU (Stock Keeping Unit) es la unidad mínima de facturación: una combinación concreta de recurso, región y modalidad. Cada servicio tiene decenas o cientos.

Ejemplo real del desglose de Compute Engine para el MIG de AlpinaShop antes de retirarlo:

SKU Uso Coste
N1 Predefined Instance Core running in EMEA 1.460 vCPU-h 29,50 €
N1 Predefined Instance Ram running in EMEA 5.840 GiB-h 16,20 €
Balanced PD Capacity in EMEA 40 GiB-mes 3,80 €
Network Inter Zone Egress 12 GiB 0,14 €
Network Internet Egress from EMEA to EMEA 85 GiB 10,20 €

Aprendizaje inmediato: el coste de una VM no es "una VM". Son vCPU y memoria y disco y tráfico, cada uno con su SKU y su precio. Por eso cambiar de e2-medium a e2-small no reduce el coste a la mitad: reduce dos de los cinco SKU.

Los descuentos que se aplican solos y los que hay que pedir

Descuento Cómo se obtiene Ahorro típico
Por uso sostenido (SUD) Automático en Compute Engine si la VM corre buena parte del mes Hasta ~30 %
Por uso comprometido (CUD) Se contrata: 1 o 3 años 30-55 %
Nivel gratuito Automático, mensual Variable
Spot / preemptible Se elige al crear el recurso 60-91 %
Descuentos por volumen Negociados con Google (empresas grandes) Variable

El descuento por uso sostenido es interesante porque es invisible: se aplica solo, y explica por qué la factura de una VM encendida todo el mes es menor que horas × precio de lista. Y también por qué apagar una VM por las noches ahorra menos de lo que la aritmética sugiere: al bajar del umbral, se pierde parte del descuento automático.

Todos los precios de esta lección son órdenes de magnitud en europe-west1 para razonar. Cambian, varían por región y dependen del contrato. Verifícalos siempre en la página de precios oficial y en la calculadora.

  1. La exportación de facturación a BigQuery

En 01-04 se activó y se prometió explotarla. Ha llegado el momento.

La consola de facturación sirve para ver tendencias. Para responder preguntas concretas —¿por qué subió la factura el martes 14?, ¿cuánto cuesta el entorno de desarrollo?— hace falta SQL.

# 1. Dataset destino en alpinashop-datos
bq --location=EU mk --dataset \
  --description="Exportacion de facturacion" \
  alpinashop-datos:facturacion

# 2. La exportacion se configura en la consola:
#    Facturacion > Exportacion de facturacion > Configuracion de BigQuery
#    Activar "Coste detallado" (nivel de recurso), no solo "Coste estandar"

Activa siempre la exportación de coste detallado, aunque genere más filas. La diferencia:

Exportación Granularidad Responde a
Costes estándar Servicio + SKU + proyecto + etiqueta "¿Cuánto gasta BigQuery en producción?"
Costes detallados + recurso concreto "¿Qué bucket concreto cuesta 40 €?"
Precios Catálogo de precios Simulaciones

Sin el detalle por recurso, sabrás que Cloud Storage cuesta 25 € pero no qué bucket. Y esa es justo la pregunta que hay que responder.

Dos avisos prácticos:

  1. Los datos tardan. La exportación se rellena con varias horas de retraso y se corrige durante días: los costes de ayer pueden cambiar mañana. No sirve para alertas en tiempo real; para eso están los presupuestos.
  2. La tabla es enorme y está particionada por _PARTITIONTIME. Consultarla sin filtro de fecha procesa meses de datos, y esa consulta te cuesta dinero. Es irónico y pasa constantemente.

  1. Las consultas que responden "quién gasta qué"

Estas seis consultas cubren el 95 % de las preguntas reales. Merece la pena guardarlas como vistas.

1. Gasto por proyecto, últimos 30 días.

SELECT
  project.id                                   AS proyecto,
  ROUND(SUM(cost), 2)                          AS coste,
  ROUND(SUM(IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0)), 2) AS creditos,
  ROUND(SUM(cost) + SUM(IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0)), 2) AS neto
FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY proyecto
ORDER BY neto DESC

El campo credits es un array anidado con descuentos, promociones y nivel gratuito, y siempre son valores negativos. Sumarlo al coste da el neto real. Olvidarlo es el error número uno al analizar facturación en BigQuery: se ven cifras infladas y nadie entiende por qué no cuadran con la consola.

2. Los 20 SKU más caros: dónde está el dinero de verdad.

SELECT
  service.description AS servicio,
  sku.description     AS sku,
  ROUND(SUM(cost), 2) AS coste,
  ROUND(SUM(usage.amount), 2) AS uso,
  ANY_VALUE(usage.unit) AS unidad
FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY servicio, sku
ORDER BY coste DESC
LIMIT 20

3. Gasto por etiqueta: la que justifica la disciplina de 01-04.

SELECT
  (SELECT value FROM UNNEST(labels) WHERE key = 'entorno')      AS entorno,
  (SELECT value FROM UNNEST(labels) WHERE key = 'equipo')       AS equipo,
  (SELECT value FROM UNNEST(labels) WHERE key = 'centro-coste') AS centro_coste,
  (SELECT value FROM UNNEST(labels) WHERE key = 'aplicacion')   AS aplicacion,
  ROUND(SUM(cost), 2) AS coste
FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY entorno, equipo, centro_coste, aplicacion
ORDER BY coste DESC

4. Recursos concretos más caros (requiere exportación detallada).

SELECT
  resource.name       AS recurso,
  service.description AS servicio,
  ROUND(SUM(cost), 2) AS coste
FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
  AND resource.name IS NOT NULL
GROUP BY recurso, servicio
ORDER BY coste DESC
LIMIT 30

5. Evolución diaria: para ver el escalón.

SELECT
  DATE(usage_start_time) AS dia,
  service.description    AS servicio,
  ROUND(SUM(cost), 2)    AS coste
FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 60 DAY)
GROUP BY dia, servicio
HAVING coste > 0.5
ORDER BY dia DESC, coste DESC

6. Recursos SIN etiquetar: la deuda de gobierno hecha número.

SELECT
  project.id          AS proyecto,
  service.description AS servicio,
  ROUND(SUM(cost), 2) AS coste_sin_atribuir
FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
  AND (SELECT COUNT(*) FROM UNNEST(labels) WHERE key = 'centro-coste') = 0
GROUP BY proyecto, servicio
HAVING coste_sin_atribuir > 1
ORDER BY coste_sin_atribuir DESC

La consulta 6 es la más incómoda y la más útil. Todo lo que aparece es gasto que no se puede atribuir a nadie. En AlpinaShop, la primera vez que se ejecutó, salieron 380 € de 1.058 €: el 36 % de la factura no tenía dueño.

  1. FinOps para una pyme: informar, optimizar, operar

FinOps es la disciplina de gestionar el coste de la nube como una responsabilidad compartida entre finanzas, tecnología y negocio. Tiene tres fases que se recorren en ciclo:

flowchart LR
    I["INFORMAR<br/>visibilidad, asignacion,<br/>previsiones"] --> O["OPTIMIZAR<br/>dimensionar, descuentos,<br/>eliminar desperdicio"]
    O --> P["OPERAR<br/>politicas, automatismos,<br/>revision continua"]
    P --> I
Fase Qué se hace En AlpinaShop
Informar Exportación, etiquetas, paneles, presupuestos Apartados 3, 4, 6, 10
Optimizar Dimensionar, apagar, descuentos, arquitectura Apartado 8
Operar Políticas que impiden el desperdicio, automatismos Apartados 6, 7, 10, 11

El error clásico es empezar por optimizar. Sin visibilidad, se optimiza lo que se ve —que suele ser lo menos importante— y se ignora el entorno de Composer de 280 € que nadie sabía que existía. Primero se mide, luego se corta.

¿Quién es el dueño del coste?

En una empresa grande hay un equipo de FinOps. En una de cuarenta personas, la pregunta es real y tiene una respuesta incómoda:

Modelo Cómo funciona Problema
Nadie Se mira la factura cuando asusta El de AlpinaShop hasta hoy
Finanzas Contabilidad vigila el total No entiende qué es un SKU ni puede actuar
Solo infraestructura Marta lo lleva todo No controla lo que gastan las consultas de Lucía
Responsabilidad distribuida con un coordinador Cada equipo ve y responde de su gasto; alguien coordina El correcto

Para AlpinaShop:

  • Marta coordina: mantiene el panel, revisa mensualmente y propone.
  • Cada persona ve su gasto: Lucía el de BigQuery y Dataflow, Dani el de Cloud Run y Cloud Build.
  • Dirección aprueba los compromisos plurianuales.
  • Una reunión de 30 minutos al mes con el panel abierto.

Esa reunión mensual, que parece burocracia, es la práctica de FinOps con mejor relación coste/beneficio que existe. Lo que se mira en grupo una vez al mes no se descontrola.

  1. Presupuestos, alertas y acciones automáticas

Un presupuesto en Google Cloud no limita el gasto: avisa. Esto sorprende a todo el mundo y hay que interiorizarlo:

No existe un botón de "no gastes más de X". El presupuesto envía notificaciones; detener el gasto lo tienes que automatizar tú.

Presupuesto básico

gcloud billing budgets create \
  --billing-account=BILLING_ACCOUNT_ID \
  --display-name="AlpinaShop total mensual" \
  --budget-amount=1200EUR \
  --threshold-rule=percent=0.5 \
  --threshold-rule=percent=0.8 \
  --threshold-rule=percent=1.0 \
  --threshold-rule=percent=1.2 \
  --threshold-rule=percent=0.8,basis=forecasted-spend \
  --notifications-rule-monitoring-notification-channels=CHANNEL_ID

La última regla es la más valiosa y la que menos se usa: basis=forecasted-spend avisa cuando Google prevé que se superará el umbral a fin de mes. Avisa el día 8, no el día 26. Es la única alerta que llega a tiempo de hacer algo.

Presupuestos que conviene tener, en lugar de uno solo:

Presupuesto Importe Por qué
Total de la organización 1.200 € Visión global
alpinashop-prod 700 € El que importa
alpinashop-dev 100 € Aquí se descontrola todo
alpinashop-datos 250 € BigQuery y Dataflow son variables
Solo BigQuery, por etiqueta 150 € Una consulta puede disparar esto sola

La acción automática

Aquí es donde el presupuesto deja de ser un aviso y pasa a ser un control. El flujo:

flowchart LR
    B["Presupuesto<br/>umbral superado"] --> PS["Pub/Sub<br/>alertas-presupuesto"]
    PS --> F["Cloud Function<br/>reaccionar-presupuesto"]
    F -->|"100% en dev"| A1["Apagar VM de desarrollo"]
    F -->|"120% en dev"| A2["Desvincular facturacion"]
    F -->|"cualquier umbral"| A3["Avisar en el canal"]
# main.py — funcion reaccionar-presupuesto (Cloud Functions 2a gen, 06-03)
import base64, json, os, logging
from google.cloud import compute_v1
from googleapiclient import discovery

PROYECTO_DEV = "alpinashop-dev"
ZONA = "europe-west1-b"

def reaccionar_presupuesto(evento, contexto):
    datos = json.loads(base64.b64decode(evento["data"]).decode("utf-8"))

    nombre     = datos.get("budgetDisplayName", "")
    gastado    = float(datos.get("costAmount", 0))
    presupuesto= float(datos.get("budgetAmount", 1))
    umbral     = float(datos.get("alertThresholdExceeded", 0))
    ratio      = gastado / presupuesto if presupuesto else 0

    logging.info(json.dumps({
        "severity": "WARNING", "message": "Umbral de presupuesto superado",
        "presupuesto": nombre, "gastado": gastado, "ratio": round(ratio, 2),
    }))

    # Solo se actua sobre DESARROLLO. Produccion NUNCA se apaga sola.
    if "dev" not in nombre.lower():
        return

    if ratio >= 1.0:
        apagar_vms_desarrollo()

    if ratio >= 1.2:
        desvincular_facturacion(PROYECTO_DEV)

def apagar_vms_desarrollo():
    cliente = compute_v1.InstancesClient()
    for inst in cliente.list(project=PROYECTO_DEV, zone=ZONA):
        if inst.status == "RUNNING":
            cliente.stop(project=PROYECTO_DEV, zone=ZONA, instance=inst.name)
            logging.warning(f"VM detenida por presupuesto: {inst.name}")

def desvincular_facturacion(proyecto):
    # MEDIDA EXTREMA: detiene practicamente todo el proyecto
    svc = discovery.build("cloudbilling", "v1")
    svc.projects().updateBillingInfo(
        name=f"projects/{proyecto}",
        body={"billingAccountName": ""}
    ).execute()
    logging.critical(f"FACTURACION DESVINCULADA de {proyecto}")

Tres advertencias que hay que leer dos veces:

  1. Nunca automatices el apagado de producción. Un pico de ventas en campaña dispara el presupuesto; apagar la tienda en el mejor día del año es peor que cualquier factura.
  2. Desvincular la facturación es destructivo y en parte irreversible. Los recursos se detienen y algunos se eliminan pasado un plazo. Solo tiene sentido en desarrollo, y aun así conviene pensarlo.
  3. Los mensajes de presupuesto pueden llegar duplicados. La función tiene que ser idempotente (06-03): apagar una VM ya apagada no debe fallar.

  1. Cuotas como límite duro

Donde el presupuesto avisa, la cuota impide. Es el único mecanismo que realmente frena el gasto.

Tipo Qué limita Uso para coste
De tasa Peticiones por minuto a una API Contener bucles descontrolados
De asignación Recursos simultáneos: vCPU, IP, discos El más eficaz
# Ver las cuotas de CPU de una region
gcloud compute regions describe europe-west1 --format="table(quotas.metric, quotas.limit, quotas.usage)"

Limitar la cuota de vCPU en desarrollo es el freno más eficaz que existe, porque es imposible saltárselo: si la cuota son 8 vCPU, nadie puede crear una VM de 32 aunque quiera.

Las cuotas de coste de BigQuery

BigQuery bajo demanda cobra por bytes leídos, y una sola consulta mal escrita puede costar más que un mes de servidores. Hay dos frenos, y ambos deberían estar puestos siempre:

# 1. Limite de bytes procesados por dia y por usuario
gcloud alpha services quota update \
  --service=bigquery.googleapis.com \
  --consumer=projects/alpinashop-datos \
  --metric=bigquery.googleapis.com/quota/query/usage \
  --unit=1/d/{project}/{user} \
  --value=1099511627776        # 1 TiB por usuario y dia
-- 2. Limite por consulta individual, en la sesion o en el script
SET @@maximum_bytes_billed = 107374182400;   -- 100 GiB

Y en la herramienta de línea de comandos:

bq query --maximum_bytes_billed=107374182400 --use_legacy_sql=false 'SELECT ...'

El mensaje que devuelve una consulta bloqueada es explícito —"Query exceeded limit for bytes billed"— y produce el efecto pedagógico correcto: quien la escribió aprende a filtrar por partición antes de reescribirla.

  1. Palancas de ahorro ordenadas por retorno

Esta es la sección práctica. Ordenadas por euros ahorrados por hora de trabajo invertida, que es el criterio que importa cuando el equipo es pequeño.

Palanca 1 — Apagar lo que no se usa (retorno inmediato)

Lo más rentable no es optimizar: es eliminar. Y siempre hay algo.

# Entornos de desarrollo fuera de horario, con Cloud Scheduler + Workflows
gcloud scheduler jobs create http apagar-desarrollo \
  --location=europe-west1 \
  --schedule="0 20 * * 1-5" \
  --time-zone="Europe/Madrid" \
  --uri="https://workflowexecutions.googleapis.com/v1/projects/alpinashop-dev/locations/europe-west1/workflows/apagar-entorno/executions" \
  --http-method=POST \
  --oauth-service-account-email=sa-scheduler@alpinashop-dev.iam.gserviceaccount.com

gcloud scheduler jobs create http encender-desarrollo \
  --location=europe-west1 \
  --schedule="0 8 * * 1-5" \
  --time-zone="Europe/Madrid" \
  --uri="https://workflowexecutions.googleapis.com/v1/projects/alpinashop-dev/locations/europe-west1/workflows/encender-entorno/executions" \
  --http-method=POST \
  --oauth-service-account-email=sa-scheduler@alpinashop-dev.iam.gserviceaccount.com

La aritmética que convence a cualquiera: un entorno encendido de 8:00 a 20:00 de lunes a viernes son 260 horas de las 730 del mes. Un 64 % de ahorro por dos trabajos programados.

Con dos matices honestos: los discos se siguen pagando aunque la VM esté apagada, y se pierde parte del descuento por uso sostenido. El ahorro real ronda el 50-55 %, no el 64 %.

Palanca 2 — Dimensionar correctamente

Google observa el uso real durante 8 días y recomienda:

gcloud recommender recommendations list \
  --project=alpinashop-prod --location=europe-west1-b \
  --recommender=google.compute.instance.MachineTypeRecommender \
  --format="table(description, primaryImpact.costProjection.cost.units)"

Existen recomendadores para VM, discos persistentes, Cloud SQL, direcciones IP y muchos más. Aplicarlos es de las cosas más rentables por hora invertida.

La advertencia: las recomendaciones se basan en la ventana observada. Si esa ventana es agosto y el pico es en octubre, dimensionar a la baja según agosto es prepararse un problema. Contrasta siempre con el pico anual.

Palanca 3 — Descuentos por compromiso

Uso sostenido (SUD) Uso comprometido (CUD)
Cómo Automático Se contrata
Compromiso Ninguno 1 o 3 años, se paga igual
Ahorro Hasta ~30 % 30-55 %
Flexibilidad Total Los basados en gasto son flexibles; los de recursos, no
Riesgo Ninguno Pagar por lo que ya no usas

Hay dos tipos de compromiso, y elegir mal es caro:

Tipo Se compromete a Riesgo
Por recursos X vCPU y Y GiB en una región y familia concretas Alto: si migras o cambias de familia, sigues pagando
Por gasto (flexible) Gastar al menos X €/hora en un servicio Bajo: se aplica a lo que uses

Regla práctica: comprométete solo con la parte estable de tu consumo, típicamente el 50-70 % del mínimo histórico. Nunca con el total.

Y la lección que AlpinaShop aprendió a su costa: si Marta hubiera contratado en enero un compromiso a un año sobre las vCPU del MIG, hoy —tras migrar a Cloud Run en 07-02— seguiría pagando por unas VM que ya no existen. La optimización arquitectónica invalida los compromisos. Primero se estabiliza la arquitectura, después se compromete.

Palanca 4 — VMs Spot

Instancias con descuento del 60-91 % que Google puede reclamar con 30 segundos de aviso.

Sirven para No sirven para
Procesamiento por lotes con reintentos Bases de datos
Entrenamiento de modelos con puntos de control Servicios web de cara al cliente
Trabajadores de Dataflow y Dataproc Nada con estado local
CI/CD Nada que no tolere reinicios
gcloud dataproc clusters create cluster-analitica \
  --region=europe-west1 \
  --num-workers=2 \
  --num-secondary-workers=8 \
  --secondary-worker-type=spot          # 8 de 10 trabajadores al 30% del precio

El patrón correcto es el de esa orden: núcleo estable pequeño + mayoría spot. Si Google reclama los spot, el trabajo se ralentiza pero no muere.

Palanca 5 — Clases de almacenamiento y ciclo de vida

Clase Coste relativo Recuperación mínima Uso
Standard 1× — Acceso frecuente
Nearline ~0,5× 30 días Menos de una vez al mes
Coldline ~0,3× 90 días Trimestral
Archive ~0,12× 365 días Copias legales
resource "google_storage_bucket" "datalake" {
  name     = "alpinashop-datalake"
  location = "EUROPE-WEST1"

  lifecycle_rule {
    condition { age = 30 }
    action { type = "SetStorageClass" storage_class = "NEARLINE" }
  }
  lifecycle_rule {
    condition { age = 90 }
    action { type = "SetStorageClass" storage_class = "COLDLINE" }
  }
  lifecycle_rule {
    condition { age = 365 }
    action { type = "SetStorageClass" storage_class = "ARCHIVE" }
  }
  lifecycle_rule {
    condition { num_newer_versions = 3 }
    action { type = "Delete" }        # limitar versiones antiguas
  }
}

La trampa de las clases frías, que hay que conocer antes de configurarlas: tienen cargos por recuperación anticipada. Un objeto en Coldline borrado o leído antes de 90 días factura como si hubiera estado los 90. Poner Archive a datos que se consultan cada dos meses sale más caro que Standard.

La última regla, sobre versiones, resuelve un problema real: con versionado activado (y debe estarlo, por 07-04), cada modificación deja una copia. Un bucket con versionado y sin límite de versiones crece indefinidamente y nadie lo mira.

Palanca 6 — Particionado y clustering en BigQuery

BigQuery bajo demanda cobra por bytes leídos, no por tiempo. Una tabla de 500 GB sin particionar cuesta lo mismo consultar un día que un año.

CREATE OR REPLACE TABLE `alpinashop-datos.alpinashop_analitica.pedidos_opt`
PARTITION BY DATE(fecha_pedido)
CLUSTER BY categoria_producto, provincia
OPTIONS (
  partition_expiration_days = 1095,
  require_partition_filter  = TRUE      -- <<< la opcion que salva la factura
)
AS SELECT * FROM `alpinashop-datos.alpinashop_analitica.pedidos`;

require_partition_filter = TRUE es la opción más rentable de todo BigQuery. Rechaza cualquier consulta que no filtre por partición:

Cannot query over table 'pedidos_opt' without a filter over column(s)
'fecha_pedido' that can be used for partition elimination

Un mensaje de error a cambio de evitar escaneos completos de 500 GB.

Efecto medido sobre la consulta habitual de Lucía:

Versión Bytes leídos Coste por ejecución
Sin particionar, SELECT * 480 GB ~2,70 €
Particionada, filtro de 30 días 41 GB ~0,23 €
+ clustering por categoría 6 GB ~0,03 €

Noventa veces más barata. Y la misma consulta ejecutada 40 veces al mes en un cuadro de mando pasa de 108 € a 1,20 €.

Palanca 7 — Bajo demanda frente a ediciones de BigQuery

Modelo Cómo se paga Cuándo conviene
Bajo demanda Por TB leído Uso irregular o bajo. AlpinaShop
Standard / Enterprise (slots) Por capacidad de cómputo reservada, con autoescalado Uso alto y sostenido

El punto de equilibrio ronda un consumo mensual constante equivalente a varios cientos de euros bajo demanda. Por debajo, bajo demanda gana casi siempre. Por encima, las reservas dan además coste predecible, que a veces vale más que el ahorro.

Palanca 8 — CDN y caché

Cada respuesta servida desde la caché de Cloud CDN (03-03) es tráfico que no sale del origen y cómputo que no se ejecuta.

Métrica Sin CDN Con CDN al 85 % de aciertos
Egress desde el origen 300 GB 45 GB
Coste de egress ~36 € ~5,40 € + coste de CDN
Instancias de Cloud Run Más Menos

Y la palanca gratuita que casi nadie activa: comprimir. Con gzip o brotli, una página HTML de 200 KB viaja en 30 KB. Un 85 % menos de bytes facturados, y además carga más rápido.

Palanca 9 — Escalado a cero

Ya cuantificada en 07-02: el catálogo pasó de ~50 € a ~7 € al mes. La regla general:

Todo lo que no reciba tráfico constante debería escalar a cero. Cloud Run, Cloud Functions y los trabajos por lotes lo hacen; las VM y los clústeres, no.

Resumen ordenado por retorno

# Palanca Ahorro típico Esfuerzo €/hora invertida
1 Eliminar lo no usado 10-40 % Muy bajo Altísimo
2 Apagar desarrollo fuera de horario 50 % de dev Bajo Muy alto
3 require_partition_filter + clustering 50-95 % de BigQuery Bajo Muy alto
4 Dimensionar con Recommender 20-40 % de cómputo Bajo Alto
5 Ciclo de vida de almacenamiento 40-70 % de storage Bajo Alto
6 CDN y compresión 60-85 % de egress Medio Alto
7 Escalado a cero 60-90 % del servicio Medio Alto
8 VMs Spot en lotes 60-90 % de esos lotes Medio Medio
9 Compromisos de uso 30-55 % de lo comprometido Bajo, con riesgo Medio
10 Ediciones de BigQuery Variable Alto Bajo si no hay escala

  1. El coste oculto del tráfico

Se introdujo en 07-03 y aquí se cierra con los cuatro escondites y su remedio.

Trayecto Coste orientativo
Entrada desde internet Gratis
Dentro de una zona, IP interna Gratis
Entre zonas de la misma región ~0,01 $/GB
Entre regiones de la UE ~0,02 $/GB
A otro continente 0,05-0,15 $/GB
Salida a internet, Premium ~0,12 $/GB
Balanceador: reglas + datos procesados Cargo fijo + por GB
Escondite Cómo aparece Remedio
Réplica HA de Cloud SQL Replicación continua entre zonas Es el precio de la disponibilidad. Se acepta y se conoce
Aplicación y BD en zonas distintas Cada consulta cruza zona Colocar deliberadamente
Logs y copias hacia otra región Por cada línea de log Buckets de logs en la misma región
Balanceadores olvidados Cargo fijo por regla de reenvío, haya o no tráfico Auditoría periódica (apartado 11)

  1. Etiquetado y asignación de costes

La disciplina de 01-04, ahora explotada. Las cuatro etiquetas de AlpinaShop:

Etiqueta Valores Responde a
entorno prod, dev, pruebas ¿Cuánto cuesta desarrollo?
equipo infra, desarrollo, datos ¿Quién gasta?
centro-coste tienda, analitica, plataforma ¿A qué línea de negocio se imputa?
aplicacion catalogo, informes, ml ¿Qué producto cuesta qué?

Reglas que las hacen útiles:

  1. Valores de un conjunto cerrado. equipo: varios destruye el análisis.
  2. En todos los recursos que facturen. Un recurso sin etiqueta es gasto sin dueño.
  3. Aplicadas por Terraform, nunca a mano.
  4. Ojo: no todos los recursos admiten etiquetas. El egress de red, algunos SKU de balanceador y ciertos cargos de soporte no las llevan. Siempre habrá un residuo sin atribuir; lo importante es que sea pequeño y conocido.

La política que impide crear recursos sin etiqueta

Se aplica en Terraform, que es donde se crea todo:

variable "etiquetas" {
  type        = map(string)
  description = "Etiquetas obligatorias de AlpinaShop"

  validation {
    condition = alltrue([
      for k in ["entorno", "equipo", "centro-coste", "aplicacion"] :
      contains(keys(var.etiquetas), k)
    ])
    error_message = "Faltan etiquetas obligatorias: entorno, equipo, centro-coste, aplicacion."
  }

  validation {
    condition     = contains(["prod", "dev", "pruebas"], lookup(var.etiquetas, "entorno", ""))
    error_message = "entorno debe ser prod, dev o pruebas."
  }

  validation {
    condition     = contains(["infra", "desarrollo", "datos"], lookup(var.etiquetas, "equipo", ""))
    error_message = "equipo debe ser infra, desarrollo o datos."
  }
}

El plan falla si faltan etiquetas o si sus valores no son válidos. Es prevención en el punto de creación, que es infinitamente más eficaz que perseguir recursos huérfanos después. Y se complementa con las etiquetas de recurso (tags) de la organización y las políticas de 07-07, que actúan incluso sobre lo que se cree fuera de Terraform.

  1. Detección de anomalías y gastos fantasma

Detección de anomalías

Google Cloud incluye detección automática de anomalías de coste en la consola de facturación: compara el gasto con el patrón histórico y avisa de desviaciones. Es gratuita, no hay que configurarla, y conviene revisarla mensualmente.

La versión propia, más ajustable:

-- Servicios cuyo gasto de ayer se desvia mas de 3 sigmas de su media de 30 dias
WITH diario AS (
  SELECT service.description AS servicio, DATE(usage_start_time) AS dia, SUM(cost) AS coste
  FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
  WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 45 DAY)
  GROUP BY servicio, dia
),
estad AS (
  SELECT servicio,
         AVG(coste)    AS media,
         STDDEV(coste) AS desviacion
  FROM diario
  WHERE dia BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 31 DAY)
                AND DATE_SUB(CURRENT_DATE(), INTERVAL 2 DAY)
  GROUP BY servicio
)
SELECT d.servicio, d.dia, ROUND(d.coste,2) AS coste_ayer,
       ROUND(e.media,2) AS media_30d,
       ROUND(SAFE_DIVIDE(d.coste - e.media, e.desviacion), 1) AS sigmas
FROM diario d JOIN estad e USING (servicio)
WHERE d.dia = DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
  AND e.desviacion > 0
  AND d.coste > e.media + 3 * e.desviacion
  AND d.coste > 2
ORDER BY sigmas DESC

El filtro d.coste > 2 evita el ruido: un servicio que pasa de 0,02 € a 0,20 € son 9 sigmas y no le importa a nadie.

Los gastos fantasma

Recursos que cuestan dinero y no dan valor a nadie. Siempre hay. En todas las empresas.

Fantasma Por qué aparece Coste típico
IP estáticas reservadas sin usar Se reserva, se cambia el diseño, no se libera ~7 €/mes cada una. Una IP sin usar cuesta más que una en uso
Discos persistentes huérfanos Se borra la VM sin marcar el disco para borrado 0,04-0,17 €/GB/mes
Instantáneas antiguas Se hacen y no caducan Acumulativo
Imágenes de contenedor antiguas Cada compilación deja una Crece con el pipeline
Endpoints de Vertex AI olvidados Se despliega un modelo para probar Alto: la máquina corre 24×7
Clústeres de desarrollo encendidos Se crean para una prueba Muy alto
Entornos de Composer No escalan a cero ~280 €/mes
Balanceadores sin backends Se prueba y se abandona ~18 €/mes cada uno
Buckets de datos temporales alpinashop-dataflow acumula ficheros de staging Crece indefinidamente
Suscripciones de Pub/Sub sin consumidor Retienen mensajes hasta caducar Almacenamiento

El script de auditoría de fantasmas

#!/usr/bin/env bash
# auditoria-fantasmas.sh — busca recursos que cuestan y no sirven
set -uo pipefail
PROYECTOS=(alpinashop-prod alpinashop-dev alpinashop-datos alpinashop-cicd alpinashop-red)

for P in "${PROYECTOS[@]}"; do
  echo "############ $P ############"

  echo "--- IP estaticas reservadas y NO usadas (~7 EUR/mes cada una) ---"
  gcloud compute addresses list --project="$P" \
    --filter="status:RESERVED" --format="table(name, region, address)"

  echo "--- Discos persistentes sin adjuntar ---"
  gcloud compute disks list --project="$P" \
    --filter="-users:*" --format="table(name, zone, sizeGb, type)"

  echo "--- Instantaneas de mas de 180 dias ---"
  gcloud compute snapshots list --project="$P" \
    --filter="creationTimestamp<$(date -u -d '180 days ago' +%Y-%m-%d)" \
    --format="table(name, diskSizeGb, creationTimestamp)"

  echo "--- Endpoints de Vertex AI (comprobar si se usan) ---"
  gcloud ai endpoints list --project="$P" --region=europe-west1 \
    --format="table(displayName, createTime)" 2>/dev/null

  echo "--- Reglas de reenvio: balanceadores, ~18 EUR/mes cada uno ---"
  gcloud compute forwarding-rules list --project="$P" \
    --format="table(name, region, IPAddress, target)"

  echo "--- Clusteres de GKE ---"
  gcloud container clusters list --project="$P" \
    --format="table(name, location, status, currentNodeCount)" 2>/dev/null

  echo "--- Entornos de Cloud Composer (~280 EUR/mes cada uno) ---"
  gcloud composer environments list --project="$P" --locations=europe-west1 \
    --format="table(name, state)" 2>/dev/null

  echo "--- Instancias de Cloud SQL detenidas (siguen pagando disco) ---"
  gcloud sql instances list --project="$P" \
    --filter="state!=RUNNABLE" --format="table(name, state, settings.tier)"

  echo "--- Imagenes de contenedor de mas de 90 dias ---"
  gcloud artifacts docker images list \
    "europe-west1-docker.pkg.dev/$P/alpinashop" --include-tags \
    --filter="createTime<$(date -u -d '90 days ago' +%Y-%m-%d)" \
    --format="table(package, version, createTime)" 2>/dev/null
  echo
done

Ejecútalo el primer lunes de cada mes. Es media hora que en AlpinaShop encontró, la primera vez, 430 € mensuales.

Y para que los fantasmas no vuelvan, políticas automáticas:

# Caducidad automatica de imagenes antiguas en Artifact Registry
gcloud artifacts repositories update alpinashop \
  --location=europe-west1 \
  --update-labels=limpieza=activa

# Politica de limpieza: borrar sin etiqueta y con mas de 30 dias,
# conservando siempre las 10 versiones mas recientes
gcloud artifacts repositories set-cleanup-policies alpinashop \
  --location=europe-west1 --policy=politica-limpieza.json
[
  {
    "name": "borrar-antiguas-sin-etiqueta",
    "action": {"type": "Delete"},
    "condition": {"tagState": "UNTAGGED", "olderThan": "30d"}
  },
  {
    "name": "conservar-recientes",
    "action": {"type": "Keep"},
    "mostRecentVersions": {"keepCount": 10}
  }
]

  1. El caso completo de AlpinaShop: antes y después

La factura de julio: 1.058 €

Servicio € Comentario
Cloud Composer 280 🔴 Entorno de evaluación de 04-06, nunca borrado
Cloud SQL (alpinashop-pedidos HA + réplica) 150 HA regional + réplica de informes
Vertex AI (endpoint) 140 🔴 Endpoint del recomendador desplegado "para probar"
GKE Autopilot 120 Clúster del catálogo
Dataflow 90 Trabajo de pedidos en streaming
BigQuery 75 15 almacenamiento + 60 consultas
Compute Engine (MIG) 50 2 × e2-medium 24×7
Cloud Logging / Monitoring 45 Retención por defecto, sin exclusiones
Egress de red 35 Salida a internet y entre zonas
Cloud Storage 25 4 buckets, sin ciclo de vida
Balanceador + IP 20
Cloud Build 15
Pub/Sub 5
Secret Manager + KMS 5
Cloud Functions 3 procesar-imagen-producto
Total 1.058 €

Los dos hallazgos que Marta no supo explicar son las dos primeras filas en rojo: 420 € al mes, el 40 % de la factura, en dos recursos que no daban valor a nadie. El de Composer llevaba cuatro meses; el endpoint de Vertex AI, tres. Coste acumulado del despiste: más de 1.500 €.

Y el detalle que lo hace doloroso: DA-002 decidió expresamente que las recomendaciones se harían por lotes y no con un endpoint 24×7. La decisión estaba escrita y era correcta. Lo que faltó fue borrar el endpoint de la prueba.

Una decisión de arquitectura solo ahorra dinero si alguien ejecuta la parte de "y ahora apaga lo otro".

La factura después: 425 €

Servicio Antes Después Qué se hizo
Cloud Composer 280 0 🗑️ Borrado. Se usa Workflows (04-06)
Vertex AI 140 8 🗑️ Endpoint eliminado; predicciones por lotes (DA-002)
GKE Autopilot 120 0 🗑️ Retirado tras 07-02
Cloud SQL 150 105 Dimensionado + compromiso a 1 año sobre la parte estable
Dataflow 90 55 Trabajadores secundarios spot + dimensionado
BigQuery 75 38 Particionado + clustering + require_partition_filter
Compute Engine 50 0 🗑️ MIG apagado (07-02)
Cloud Run 0 7 ✨ Sustituye al MIG
Logging / Monitoring 45 28 Exclusiones de ruido y retención ajustada (06-06)
Egress 35 22 CDN + compresión
Cloud Storage 25 14 Ciclo de vida y límite de versiones
Balanceador + IP 20 20 Sin cambio
Cloud Build 15 12 Limpieza de imágenes antiguas
Pub/Sub 5 5
Secret Manager + KMS 5 5
Cloud Functions 3 3
Cloud VPN HA (2 túneles) 0 60 ✨ Nuevo: enlace a Sabadell (07-03)
VPC Flow Logs 0 8 ✨ Nuevo: visibilidad de red
Logs de acceso a datos 0 35 ✨ Nuevo: requisito de 07-04
Total 1.058 € 425 € −60 %

La lectura honesta

Lo que se ahorró de verdad, por orden de impacto:

  1. Borrar dos recursos olvidados: 412 €. El 65 % del ahorro total salió de media hora ejecutando un script de auditoría. Ninguna optimización técnica se acerca a ese retorno.
  2. Decisiones de arquitectura: 163 €. GKE y el MIG retirados a favor de Cloud Run. Pero cuidado con atribuirlo todo aquí: Cloud Run no se eligió para ahorrar, se eligió por trabajo operativo y portabilidad (DA-001). El ahorro fue una consecuencia agradable, no el objetivo.
  3. Optimizaciones de configuración: 76 €. Particionado, ciclo de vida, spot, exclusiones de logs. Trabajo real de ingeniería para un tercio del ahorro que dio borrar dos cosas.
  4. Compromiso de uso: 20 €. Modesto a propósito. Solo sobre Cloud SQL, que es lo único que no va a cambiar de arquitectura en un año.

Lo que NO se ahorró, y por qué:

Concepto € Por qué se mantiene
Balanceador global + IP 20 Es el precio de tener CDN, WAF, TLS gestionado e IP anycast. Ahora es el 5 % de la factura y el 74 % del coste del catálogo: no se toca
Cloud SQL HA 105 La alta disponibilidad cuesta el doble. Se paga a propósito (07-06)
Réplica de informes Incluida Evita que las consultas de Lucía afecten a los pedidos. Vale su precio
Egress 22 Servir la tienda tiene un coste. Ya está optimizado con CDN

Lo que se decidió gastar de MÁS, a propósito — y esta es la parte que casi nunca aparece en un caso de estudio:

Concepto € Por qué
Cloud VPN HA, dos túneles 60 Uno solo costaría la mitad y sería un punto único de fallo. Se paga la redundancia
VPC Flow Logs 8 Sin ellos no se puede investigar un incidente de red
Logs de acceso a datos 35 Deuda #5 de 07-04. Es un requisito legal de facto: sin ellos no se puede acotar una brecha

103 € al mes de gasto nuevo y deliberado en fiabilidad y seguridad. Sin ese matiz, el titular "hemos bajado un 60 % la factura" sería una media verdad. La verdad completa es: se eliminaron 736 € de desperdicio, se optimizaron 260 € y se reinvirtieron 103 € en cosas que hacían falta.

Optimizar coste no es recortar. Es dejar de pagar lo que no aporta para poder pagar lo que sí.

Coste unitario: la métrica que de verdad importa

El número absoluto engaña. Lo que hay que seguir es el coste por unidad de negocio:

Métrica Julio Después
Factura mensual 1.058 € 425 €
Pedidos al mes 1.200 1.200
Coste por pedido 0,88 € 0,35 €
Coste como % del ingreso (ticket ~65 €) 1,4 % 0,55 %

Esta métrica es la que hay que presentar a dirección, por dos motivos. Primero, porque hace comparables meses distintos: si en octubre la factura sube a 600 € pero se procesan 3.000 pedidos, el coste por pedido baja a 0,20 € y la subida es una buena noticia. Y segundo, porque convierte la infraestructura de "un gasto que hay que recortar" en "un coste variable del negocio", que es lo que realmente es.

  1. Herramientas: Recommender, Active Assist y la calculadora

Herramienta Para qué
Recommender Recomendaciones por tipo de recurso: máquinas, discos, IP, IAM, Cloud SQL
Active Assist El paraguas que agrupa Recommender, Insights y las predicciones
Pricing Calculator Estimar antes de crear
Detección de anomalías Incluida en la consola de facturación, gratuita
Informes de facturación Agrupación y filtrado sin SQL
Cloud Billing API Automatizar consultas y precios
# Todas las recomendaciones de coste de un proyecto, de un vistazo
for R in google.compute.instance.MachineTypeRecommender \
         google.compute.disk.IdleResourceRecommender \
         google.compute.address.IdleResourceRecommender \
         google.compute.image.IdleResourceRecommender \
         google.cloudsql.instance.IdleRecommender \
         google.cloudsql.instance.OverprovisionedRecommender; do
  echo "=== $R ==="
  gcloud recommender recommendations list \
    --project=alpinashop-prod --location=europe-west1-b --recommender="$R" \
    --format="table(description, primaryImpact.costProjection.cost.units)" 2>/dev/null
done

Consejo sobre la calculadora: úsala antes de crear, no después de la factura. Diez minutos estimando el coste de una arquitectura evitan la sorpresa del mes siguiente, y sobre todo permiten comparar dos diseños en euros y no en intuiciones.

Errores Comunes y Consejos

  • Optimizar antes de medir. Se acaba afinando lo visible mientras 280 € al mes se van en algo que nadie sabía que existía.
  • Olvidar el array credits en las consultas de facturación. Las cifras salen infladas y nadie entiende por qué no cuadran con la consola.
  • Consultar la tabla de facturación sin filtro de partición. La consulta que analiza tu coste te cuesta dinero.
  • Creer que un presupuesto limita el gasto. Solo avisa. Frenar hay que automatizarlo, y con cuidado.
  • Automatizar el apagado de producción por presupuesto. Un pico de ventas apagaría la tienda el mejor día del año.
  • Comprometerse a 1 o 3 años antes de estabilizar la arquitectura. Si migras después, pagas por lo que ya no usas.
  • Comprometer el 100 % del consumo. Solo la parte estable, un 50-70 % del mínimo histórico.
  • Poner Archive a datos que se consultan cada dos meses. Los cargos por recuperación anticipada lo hacen más caro que Standard.
  • Tablas de BigQuery sin require_partition_filter. Un SELECT * sin filtro puede costar más que un mes de servidores.
  • Versionado de buckets sin límite de versiones. El bucket crece indefinidamente y nadie lo mira.
  • Etiquetas con valores libres. equipo: varios destruye el análisis. Conjunto cerrado y validación en Terraform.
  • Reservar IP estáticas "por si acaso". Una IP reservada sin usar cuesta más que una en uso.
  • Borrar una VM sin marcar el disco para borrado automático. El disco huérfano sigue facturando.
  • Consejo: ejecuta el script de fantasmas el primer lunes de cada mes. Es la actividad de mayor retorno por hora de toda la lección.
  • Consejo: sigue el coste por unidad de negocio, no el absoluto. Es lo único que hace comparables meses distintos.
  • Consejo: la reunión mensual de 30 minutos con el panel abierto es la práctica de FinOps más rentable que existe.
  • Consejo: pon --max-instances en todo lo que escale. Es un cortafuegos de coste (07-02).
  • Consejo: usa la calculadora antes de crear. Comparar dos diseños en euros es mejor que discutirlos en intuiciones.

Ejercicios

Ejercicio 1 — Investigar una subida de factura

La factura de AlpinaShop ha pasado de 425 € a 690 € de un mes a otro. No ha habido campaña ni despliegues grandes. Dirección pregunta qué ha pasado.

Escribe la secuencia completa de consultas SQL sobre la exportación de facturación que usarías para llegar a la causa raíz, explicando qué buscas en cada paso y cómo interpretas el resultado. Indica al menos cuatro causas plausibles y cómo distinguirías entre ellas con datos.

Ejercicio 2 — Optimizar el entorno de datos

El proyecto alpinashop-datos cuesta 250 € al mes:

  • BigQuery consultas: 95 € (bajo demanda)
  • BigQuery almacenamiento: 40 €
  • Dataflow: 70 €
  • Cloud Storage (alpinashop-datalake): 45 €

Datos adicionales: la tabla eventos_web tiene 1,2 TB, no está particionada y se consulta unas 200 veces al día desde un cuadro de mando. El trabajo de Dataflow procesa pedidos en streaming las 24 horas, aunque los pedidos llegan casi todos entre las 9:00 y las 23:00. El data lake tiene 3 TB, de los cuales el 80 % son ficheros de más de un año que solo se consultan para auditorías anuales.

Propón un plan de optimización con el ahorro estimado de cada medida, el esfuerzo, y el riesgo asociado. Ordénalo por retorno.

Ejercicio 3 — Presentar el caso a dirección

Prepara la presentación de Marta al consejo de AlpinaShop sobre la gestión de costes del último trimestre. Debe incluir: qué se ha conseguido, cómo se ha conseguido, qué se gasta de más a propósito, cuál es la métrica que se va a seguir a partir de ahora, y qué se pide para el trimestre siguiente.

Escríbelo como se lo diría a un consejo que no es técnico. Máximo una página. Y explica después qué tres decisiones de comunicación has tomado y por qué.

Soluciones

Solución 1 — Investigar una subida de factura

Paso 1 — Localizar el servicio y el momento. Nunca se empieza por el detalle: primero se acota.

SELECT
  service.description AS servicio,
  ROUND(SUM(CASE WHEN DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
                 THEN cost END), 2) AS mes_actual,
  ROUND(SUM(CASE WHEN DATE(usage_start_time) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 60 DAY)
                                                 AND DATE_SUB(CURRENT_DATE(), INTERVAL 31 DAY)
                 THEN cost END), 2) AS mes_anterior
FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 60 DAY)
GROUP BY servicio
HAVING mes_actual > 1
ORDER BY (mes_actual - IFNULL(mes_anterior, 0)) DESC

La primera fila da el servicio responsable de la mayor parte de los 265 €.

Paso 2 — Averiguar la forma temporal. Es el paso que más información aporta y el que casi nadie hace:

SELECT
  DATE(usage_start_time) AS dia,
  ROUND(SUM(cost), 2)    AS coste
FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 60 DAY)
  AND service.description = 'SERVICIO_SOSPECHOSO'
GROUP BY dia
ORDER BY dia

La forma de la curva es el diagnóstico:

Forma Significado
Escalón que empieza un día y se mantiene Se creó un recurso permanente. Fantasma o despliegue
Pico de uno o dos días Un trabajo puntual, una consulta, una prueba
Rampa creciente Algo que acumula: almacenamiento, logs, instantáneas
Dientes de sierra más altos Aumento de tráfico o de frecuencia de un proceso

Paso 3 — Bajar a SKU y a recurso.

SELECT
  sku.description     AS sku,
  resource.name       AS recurso,
  ROUND(SUM(cost), 2) AS coste,
  ROUND(SUM(usage.amount), 2) AS uso,
  ANY_VALUE(usage.unit) AS unidad
FROM `alpinashop-datos.facturacion.gcp_billing_export_resource_v1_XXXXXX`
WHERE DATE(_PARTITIONTIME) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
  AND service.description = 'SERVICIO_SOSPECHOSO'
GROUP BY sku, recurso
ORDER BY coste DESC LIMIT 20

Paso 4 — Correlacionar con los cambios. Con la fecha del escalón:

FECHA="2026-09-14"
gcloud logging read "
  protoPayload.methodName=~'(create|insert|Create|Insert)'
  AND timestamp>=\"${FECHA}T00:00:00Z\"
  AND timestamp<=\"${FECHA}T23:59:59Z\"" \
  --project=alpinashop-datos --limit=100 \
  --format="table(timestamp, protoPayload.methodName,
                  protoPayload.authenticationInfo.principalEmail,
                  protoPayload.resourceName)"

Las cuatro causas plausibles y cómo distinguirlas:

Causa Firma en los datos Confirmación
Recurso nuevo olvidado Escalón limpio, mismo importe cada día, un solo resource.name Logs de auditoría del día del escalón; script de fantasmas
Consulta de BigQuery desbocada Pico en el SKU "Analysis"; usage.amount en TB dispar INFORMATION_SCHEMA.JOBS_BY_PROJECT ordenado por total_bytes_billed
Crecimiento de almacenamiento o logs Rampa suave, sin escalón; SKU de "Storage" Tamaño del bucket o volumen de ingesta de Logging
Aumento de tráfico real Sube egress y Cloud Run y peticiones del balanceador a la vez Métricas de negocio: si suben los pedidos, es buena noticia

La cuarta merece el matiz que da criterio: una subida proporcional a la actividad de negocio no es un problema de coste. Se verifica con la métrica unitaria del apartado 12: si el coste por pedido se mantiene, el negocio ha crecido; si se dispara, hay ineficiencia.

Para el caso de BigQuery, la consulta definitiva:

SELECT
  user_email,
  DATE(creation_time) AS dia,
  COUNT(*) AS consultas,
  ROUND(SUM(total_bytes_billed)/POW(1024,4), 2) AS tib_facturados,
  ROUND(SUM(total_bytes_billed)/POW(1024,4) * 5.5, 2) AS coste_estimado_eur
FROM `alpinashop-datos.region-eu.INFORMATION_SCHEMA.JOBS_BY_PROJECT`
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
  AND job_type = 'QUERY' AND state = 'DONE'
GROUP BY user_email, dia
ORDER BY tib_facturados DESC LIMIT 20

Y en cuanto se identifique la causa, la acción no es solo corregir: es poner el control que impide que se repita — require_partition_filter si fue una consulta, el script mensual si fue un fantasma, una alerta de anomalía si fue una rampa.

Solución 2 — Optimizar el entorno de datos

Análisis previo. Los cuatro conceptos tienen causas muy distintas y el orden de ataque no es el orden de la lista.


Medida 1 — Particionar y agrupar eventos_web. (Ahorro: ~75 €/mes · Esfuerzo: 4 h · Riesgo: bajo)

Una tabla de 1,2 TB sin particionar, consultada 200 veces al día, es la definición del problema. Cada consulta lee 1,2 TB salvo que BigQuery pueda podar, y sin partición no puede.

CREATE OR REPLACE TABLE `alpinashop-datos.alpinashop_analitica.eventos_web_opt`
PARTITION BY DATE(momento)
CLUSTER BY tipo_evento, pagina
OPTIONS (
  partition_expiration_days = 400,
  require_partition_filter  = TRUE
)
AS SELECT * FROM `alpinashop-datos.alpinashop_analitica.eventos_web`;

Estimación: el cuadro de mando consulta típicamente los últimos 30 días → de 1,2 TB a unos 30 GB por consulta, y el clustering recorta más. Reducción realista del 80-85 % del coste de consultas: 95 € → ~20 €.

Riesgo y mitigación: require_partition_filter romperá las consultas existentes sin filtro. Se despliega en dos fases: crear la tabla optimizada, migrar el cuadro de mando y las consultas guardadas, verificar, y solo entonces sustituir la original. Nunca al revés.

Bonus gratuito: partition_expiration_days = 400 elimina solo los eventos de más de 400 días, lo que además reduce el almacenamiento con el tiempo.


Medida 2 — Ciclo de vida del data lake. (Ahorro: ~28 €/mes · Esfuerzo: 1 h · Riesgo: muy bajo)

2,4 TB (el 80 % de 3 TB) son de más de un año y solo se leen en auditorías anuales. Están en Standard.

lifecycle_rule {
  condition { age = 90 }
  action { type = "SetStorageClass" storage_class = "NEARLINE" }
}
lifecycle_rule {
  condition { age = 365 }
  action { type = "SetStorageClass" storage_class = "COLDLINE" }
}
lifecycle_rule {
  condition { age = 1095 }
  action { type = "SetStorageClass" storage_class = "ARCHIVE" }
}

Cálculo: 2,4 TB pasando de Standard a Coldline (~0,3×) ahorra en torno al 70 % de su parte. 45 € → ~17 €.

Por qué Coldline y no Archive para el tramo de un año: con recuperación mínima de 365 días, Archive castiga cualquier lectura anticipada. Una auditoría anual está justo en el límite y podría dispararse el cargo. Coldline (90 días) es el punto seguro; Archive se reserva para lo de más de tres años, que ya nadie va a tocar.


Medida 3 — Dataflow: repensar el streaming. (Ahorro: ~35 €/mes · Esfuerzo: 1-2 días · Riesgo: medio)

Un trabajo de streaming 24×7 cuando los pedidos llegan entre las 9:00 y las 23:00. Tres opciones, y la elección depende de un requisito de negocio, no técnico:

Opción Ahorro Impacto
(a) Trabajadores secundarios spot + dimensionar ~25 € Ninguno. Empezar por aquí
(b) Streaming solo de 8:00 a 24:00; Pub/Sub retiene por la noche ~25 € Los pedidos nocturnos aparecen por la mañana
(c) Sustituir por proceso por lotes cada 15 min ~50 € Latencia de 15 min en la analítica

La pregunta que decide: ¿alguien mira los datos de pedidos en tiempo real? En AlpinaShop, la respuesta honesta es no: Lucía mira el cuadro de mando por la mañana. La opción (c) es la correcta y es la que más ahorra, pero exige reescribir el pipeline y por eso se plantea después de (a), que es gratis.

Plan realista: aplicar (a) esta semana (~25 €), y evaluar (c) el trimestre siguiente.


Medida 4 — Almacenamiento de BigQuery a largo plazo. (Ahorro: ~8 €/mes · Esfuerzo: 30 min · Riesgo: nulo)

BigQuery aplica automáticamente un precio de almacenamiento a largo plazo (~50 % menos) a las particiones no modificadas en 90 días. La trampa: cualquier UPDATE sobre una partición reinicia el contador.

SELECT table_name, partition_id,
       ROUND(total_logical_bytes/POW(1024,3), 2) AS gib,
       last_modified_time
FROM `alpinashop-datos.alpinashop_analitica.INFORMATION_SCHEMA.PARTITIONS`
WHERE total_logical_bytes > 0
ORDER BY total_logical_bytes DESC

Si aparecen particiones antiguas con last_modified_time reciente, hay un proceso reescribiéndolas innecesariamente. Corregirlo es gratis y el descuento se aplica solo.

También conviene evaluar el modelo de facturación físico frente al lógico: con datos muy comprimibles puede salir más barato, aunque el descuento a largo plazo funciona distinto. Se compara con INFORMATION_SCHEMA antes de cambiar.


Plan ordenado por retorno:

Orden Medida Ahorro Esfuerzo €/hora
1 Ciclo de vida del data lake 28 € 1 h 28
2 Particionar y agrupar 75 € 4 h 19
3 Almacenamiento a largo plazo 8 € 0,5 h 16
4 Dataflow spot (a) 25 € 4 h 6
5 Dataflow a lotes (c) +25 € 12 h 2

Resultado: 250 € → ~115 €, un 54 % menos, con las cuatro primeras medidas en unas dos jornadas de trabajo.

Y la medida que no cuesta nada y que hay que aplicar el primer día, antes que ninguna de las anteriores:

gcloud alpha services quota update \
  --service=bigquery.googleapis.com \
  --consumer=projects/alpinashop-datos \
  --metric=bigquery.googleapis.com/quota/query/usage \
  --unit=1/d/{project}/{user} --value=549755813888     # 512 GiB/usuario/dia

Porque todo lo anterior optimiza el consumo actual, pero nada impide que mañana alguien escriba una consulta que cueste 300 € en una tarde. La cuota es el único control que lo impide.

Solución 3 — Presentar el caso a dirección


Gestión del coste de la plataforma tecnológica — Informe del tercer trimestre Marta Ruiz, responsable de infraestructura

Resumen. El coste mensual de nuestra plataforma tecnológica ha pasado de 1.058 € a 425 €, un 60 % menos, sin reducir ninguna capacidad y habiendo añadido medidas de seguridad y de continuidad que antes no teníamos. En términos de negocio, el coste tecnológico por pedido ha bajado de 0,88 € a 0,35 €: de un 1,4 % a un 0,55 % del importe medio de un pedido.

De dónde viene el ahorro. Dos terceras partes no vienen de negociar mejor ni de recortar: vienen de haber encontrado dos servicios encendidos que nadie usaba —una herramienta que evaluamos en primavera y un modelo de recomendaciones que probamos en verano— y que juntos costaban 420 € al mes. Ninguna persona decidió gastarlo; simplemente nadie decidió apagarlo. Ya está corregido, y desde este mes ejecutamos una revisión automática el primer lunes de cada mes para que no vuelva a ocurrir. La lección es que en la nube el gasto crece por omisión, no por decisión, y eso exige una rutina.

El tercio restante son mejoras técnicas: hemos trasladado la tienda a una tecnología que solo cobra cuando hay visitas —de madrugada no pagamos nada—, hemos reorganizado los datos para que las consultas del cuadro de mando lean lo justo, y hemos movido los archivos antiguos a un almacenamiento más barato.

Lo que gastamos de más, a propósito. Quiero que conste explícitamente que 103 € al mes son gasto nuevo y deliberado: la conexión con la tienda de Sabadell está duplicada para que un corte no nos deje sin inventario, y hemos activado el registro de quién accede a los datos de clientes, que es lo que nos permitiría responder ante la Agencia de Protección de Datos si algún día hubiera un incidente. Sin ese registro no podríamos acotar qué pasó, y eso es una exposición legal, no solo técnica. Optimizar el coste no significa recortar en fiabilidad ni en cumplimiento: significa dejar de pagar lo que no aporta para poder pagar lo que sí.

La métrica que seguiremos. A partir de ahora informaremos del coste por pedido, no del total. Es importante entender por qué: en la campaña de otoño la factura va a subir, y eso será una buena noticia. Si vendemos el triple, el coste por pedido bajará aunque el importe total crezca. El número absoluto no dice nada por sí solo; el unitario sí.

Lo que pedimos para el próximo trimestre. Nada de presupuesto adicional. Solo dos decisiones:

  1. Autorización para un compromiso a un año sobre la base de datos, que es lo único que sabemos con certeza que no va a cambiar. Ahorra en torno a un 30 % de esa partida a cambio de comprometernos.
  2. Media hora al mes de dirección para revisar el panel de costes con nosotros. Es la medida más barata y más eficaz de todas: lo que se mira en grupo no se descontrola.

Las tres decisiones de comunicación, y por qué:

1. Empezar por el número de negocio, no por el técnico. El titular no es "hemos migrado a Cloud Run": es "el coste por pedido ha bajado de 0,88 € a 0,35 €". Un consejo no puede evaluar una decisión de arquitectura, pero sí sabe perfectamente qué significa que cada pedido cueste la mitad. Traducir a la unidad del negocio no es simplificar: es hablar en la moneda en la que ellos deciden.

2. Contar el fallo antes de que lo pregunten. Se podría haber presentado el ahorro como mérito técnico y callar que 420 € se iban en dos recursos olvidados. Decirlo tiene tres efectos que compensan de sobra la incomodidad: da credibilidad a todo lo demás del informe, justifica la rutina mensual —que sin la anécdota parecería burocracia innecesaria— y protege de la pregunta incómoda dentro de seis meses. Un informe que solo cuenta aciertos se cree a medias.

3. Anticipar la mala noticia y reencuadrarla. La factura va a subir en campaña. Si eso se explica después, parece una excusa; explicado antes, es previsión — y además enseña al consejo a leer la métrica correcta. Es la misma razón por la que el gasto nuevo de 103 € se declara de forma destacada en lugar de esconderlo en el neto: si dirección descubre por su cuenta un gasto que no se le contó, pierde la confianza en todo el informe, por muy bueno que sea el resultado.

Y una cuarta que no se ve pero está: no se pide dinero, se piden decisiones. Un informe que termina pidiendo presupuesto se lee con desconfianza; uno que termina pidiendo media hora al mes y una autorización que ahorra, se lee como gestión.

Conclusión

La factura de AlpinaShop ha dejado de ser una sorpresa mensual para convertirse en un dato que se entiende, se atribuye y se gobierna.

Sabes por qué la nube sorprende: coste variable, decisiones técnicas con efecto económico, ausencia de freno natural y desfase temporal — con la primera regla grabada: el problema casi nunca es que algo sea caro, sino que nadie sabía que estaba encendido.

Sabes leer una factura: servicio, SKU y uso, con la constatación de que una VM no es un coste sino cinco SKU distintos. Distingues los descuentos que se aplican solos —uso sostenido— de los que hay que contratar, y sabes que el sostenido explica por qué apagar por las noches ahorra menos de lo que la aritmética promete.

Tienes la exportación a BigQuery configurada con coste detallado —sin el cual sabes que Storage cuesta 25 € pero no qué bucket— y las seis consultas que responden las preguntas reales, con los dos errores clásicos evitados: olvidar el array credits y consultar sin filtro de partición. Incluida la consulta más incómoda, la del gasto sin etiquetar, que en AlpinaShop reveló que el 36 % de la factura no tenía dueño.

Entiendes FinOps —informar, optimizar, operar— con el orden que importa: primero medir, luego cortar. Y el modelo de responsabilidad para una empresa pequeña, con la práctica de mayor retorno de toda la disciplina: una reunión de treinta minutos al mes con el panel abierto.

Sabes montar presupuestos con la regla de previsión que avisa el día 8 y no el 26, varios en lugar de uno solo, y la acción automática por Pub/Sub y función — con las tres advertencias: nunca apagar producción, desvincular la facturación es destructivo, y los mensajes llegan duplicados. Y sabes que el único freno duro son las cuotas, incluidas las de bytes de BigQuery, que hay que poner siempre.

Tienes las nueve palancas ordenadas por retorno, con la primera muy por delante de todas: eliminar lo que no se usa. Y las demás con sus trampas dichas — dimensionar según agosto cuando el pico es en octubre, comprometerse antes de estabilizar la arquitectura, poner Archive a datos que se leen cada dos meses, versionado sin límite de versiones. Con require_partition_filter señalada como la opción más rentable de todo BigQuery: noventa veces más barata la misma consulta.

Sabes dónde se esconde el coste del tráfico y tienes la disciplina de etiquetado convertida en control real, con validaciones de Terraform que hacen fallar el plan si falta una etiqueta o su valor no está en el conjunto cerrado — prevención en el punto de creación, que vence siempre a perseguir huérfanos después.

Cazas anomalías con estadística y fantasmas con un script que hay que ejecutar el primer lunes de cada mes: IP reservadas que cuestan más que las usadas, discos huérfanos, instantáneas eternas, endpoints de Vertex AI, clústeres de pruebas, entornos de Composer y balanceadores sin backends.

Y tienes el caso completo: 1.058 € desglosados con dos partidas en rojo que sumaban el 40 % de la factura y llevaban meses ahí, frente a 425 € después. Con la lectura honesta que es lo más valioso del apartado: el 65 % del ahorro salió de media hora ejecutando un script, la arquitectura aportó menos de lo que parece —y además Cloud Run no se eligió para ahorrar—, las optimizaciones técnicas dieron un tercio de lo que dio borrar dos cosas, y 103 € al mes son gasto nuevo y deliberado en redundancia, visibilidad de red y logs de acceso a datos. Más la métrica que hay que presentar de ahora en adelante: el coste por pedido, que convierte la infraestructura de gasto a recortar en coste variable del negocio.

De todo ello queda una frase que resume la lección entera y que enlaza con lo que viene:

Optimizar coste no es recortar. Es dejar de pagar lo que no aporta para poder pagar lo que sí.

Y esa frase tiene una contrapartida que hay que decir con la misma claridad. En esta lección se ha decidido pagar la alta disponibilidad de Cloud SQL sin discutirla, se han duplicado los túneles VPN sabiendo que uno costaría la mitad, y se ha aceptado que el balanceador global —que ahora es el 74 % del coste del catálogo— no se toca. Tres decisiones de fiabilidad tomadas por intuición, sin un objetivo que las justifique con números.

Porque AlpinaShop todavía no ha respondido a la pregunta más básica de todas: ¿qué significa exactamente que la tienda "funcione bien"? Hay alertas desde 06-04, pero una alerta dice que algo pasó, no si el servicio está cumpliendo lo que promete. No hay un objetivo declarado, no hay forma de saber si 43 minutos de caída al mes son aceptables o inadmisibles, no hay criterio para decidir si se puede desplegar un viernes, y —lo más grave, señalado ya como deuda crítica en 07-04— nadie ha comprobado nunca que las copias de seguridad se puedan restaurar.

La siguiente lección responde a todo eso con números: SLI, SLO, presupuesto de error, arquitecturas de alta disponibilidad por niveles, modos de fallo y su mitigación, y un plan de recuperación ante desastres con RTO y RPO — incluido el ensayo de restauración de alpinashop-pedidos que lleva demasiado tiempo pendiente.

Curso de Google Cloud Platform (GCP)

Módulo 1: Introducción a Google Cloud Platform

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados