Referencia Técnica
Listo para implementar
Nivel: Avanzado

Optimización de Builds Bazel
para DataMesh Intelligence

Guía de producción: remote caching en GCP, ejecución distribuida, regla custom ML y migración desde Webpack. Objetivo: 25 min → 5 min en CI.

Bazel 7.0 GCP Artifact Registry GitHub Actions React 18 · TypeScript Python 3.11 · PyTorch 2.1 Remote Cache
Build completo CI
25m
6m
Con remote cache activo
-76% tiempo
Build incremental CI
18m
90s
Sólo targets afectados
-92% tiempo
Tests locales (1 lib)
12m
45s
Con disk cache + análisis impacto
-94% tiempo
Ahorro GCP mensual
×4 runners
De 4 runners a 2 en builds incrementales
~€420/mes
1
Diagnóstico: por qué tarda 25 minutos
Análisis del estado actual
Problema raíz: Sin --disk_cache ni --remote_cache, Bazel recalcula todo desde cero en cada build CI. Con 40 librerías y 3 apps, el grafo de dependencias acumula miles de acciones que se re-ejecutan innecesariamente.
Causa Tiempo perdido Impacto Solución
Sin remote cache en CI ~14 min GCP Remote Cache
Targets demasiado grandes (1 BUILD por app) ~5 min Granularidad fina
Sin --jobs=auto optimizado ~3 min .bazelrc CI config
Webpack aún en el pipeline (pre-Bazel) ~2 min Migración a rules_js
Sin análisis de targets impactados ~1 min bazel query + rdeps
$ bazel analyze-profile — Diagnóstico antes de optimizar
BASH
# 1. Genera perfil de build para análisis
bazel build //... --profile=~/datamesh-profile.json \
                  --noenable_bzlmod

# 2. Analiza bottlenecks
bazel analyze-profile ~/datamesh-profile.json

# 3. Log detallado de acciones lentas (>2s)
bazel build //... --execution_log_json_file=exec_log.json
jq 'select(.actualOutputs != null) | {target: .targetLabel, dur: .elapsedNs}' \
   exec_log.json | sort -k2 -n | tail -20
2
Remote Cache en GCP (Artifact Registry)
Mayor impacto individual: -14 min en CI
GCP Artifact Registry soporta el protocolo de Remote Cache de Bazel de forma nativa. DataMesh ya tiene cuenta GCP con Cloud Build, por lo que esta integración no requiere infraestructura adicional.
setup-gcp-cache.sh — Configuración inicial (una sola vez)
BASH
#!/bin/bash
# Ejecutar como admin GCP — solo la primera vez

PROJECT_ID="datamesh-prod"
REGION="europe-west1"
REPO_NAME="bazel-cache"
SA_NAME="bazel-ci-sa"

# 1. Crear repositorio en Artifact Registry
gcloud artifacts repositories create $REPO_NAME \
  --repository-format=apt \
  --location=$REGION \
  --project=$PROJECT_ID \
  --description="Bazel Remote Cache — DataMesh monorepo"

# 2. Service Account para CI (GitHub Actions)
gcloud iam service-accounts create $SA_NAME \
  --display-name="Bazel CI Service Account" \
  --project=$PROJECT_ID

# 3. Permisos mínimos
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/artifactregistry.writer"

# 4. Crear clave y exportar como Secret en GitHub
gcloud iam service-accounts keys create bazel-sa-key.json \
  --iam-account="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

echo "Añade bazel-sa-key.json como secret GCP_BAZEL_SA_KEY en GitHub"
3
Configuración .bazelrc para DataMesh
Archivo central de rendimiento
.bazelrc
BAZEL
# ================================================
# DataMesh Intelligence — .bazelrc
# Optimizado para monorepo TypeScript + Python/ML
# ================================================

# --- Flags base ---
build --enable_platform_specific_config
build --incompatible_enable_cc_toolchain_resolution
build --experimental_strict_conflict_checks

# --- Rendimiento local ---
build --jobs=auto
build --local_cpu_resources=HOST_CPUS*.80
build --local_ram_resources=HOST_RAM*.70

# --- Caché local en disco (siempre activo) ---
build --disk_cache=~/.cache/bazel-datamesh
build --repository_cache=~/.cache/bazel-repo-datamesh

# --- Remote Cache GCP (config:remote-cache) ---
build:remote-cache --remote_cache=https://storage.googleapis.com/datamesh-bazel-cache
build:remote-cache --google_default_credentials
build:remote-cache --remote_upload_local_results=true
build:remote-cache --remote_timeout=3600
build:remote-cache --remote_max_connections=200

# --- CI: activar remote cache automáticamente ---
build:ci --config=remote-cache
build:ci --build_metadata=ROLE=CI
build:ci --noshow_progress
build:ci --show_result=0
build:ci --jobs=32   # 4 runners × 8 CPUs

# --- Plataformas ---
build:linux  --platforms=//platforms:linux_x86_64
build:macos  --platforms=//platforms:macos_arm64

# --- Tests ---
test --test_output=errors
test --test_summary=terse
test --cache_test_results=yes

# --- Modos de compilación ---
build:opt  --compilation_mode=opt
build:dbg  --compilation_mode=dbg

# Importar config local del dev (git-ignored)
try-import %workspace%/user.bazelrc
4
Builds TypeScript — apps/dashboard
React 18 con rules_js + aspect_rules_ts
💡
Clave: Un único BUILD.bazel con glob para toda la app es el anti-patrón más común. Dividir por feature module permite caché granular: cambiar charts/ no invalida auth/.
apps/dashboard/BUILD.bazel
STARLARK
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("@aspect_rules_js//js:defs.bzl", "js_library", "js_binary")
load("@npm//:defs.bzl", "npm_link_all_packages")
load("@aspect_rules_jest//jest:defs.bzl", "jest_test")

npm_link_all_packages(name = "node_modules")

# --- Feature: Charts (analytics visualizations) ---
ts_project(
    name = "charts_ts",
    srcs = glob(["src/charts/**/*.tsx", "src/charts/**/*.ts"]),
    declaration = True,
    tsconfig = "//:tsconfig.json",
    deps = [
        ":node_modules/react",
        ":node_modules/recharts",
        "//libs/analytics-utils:utils",  # lib compartida
    ],
    visibility = ["//visibility:public"],
)

# --- Feature: Auth ---
ts_project(
    name = "auth_ts",
    srcs = glob(["src/auth/**/*.tsx", "src/auth/**/*.ts"]),
    declaration = True,
    tsconfig = "//:tsconfig.json",
    deps = [
        ":node_modules/react",
        ":node_modules/@auth0/auth0-react",
        "//libs/api-client:client",
    ],
    visibility = ["//visibility:public"],
)

# --- App bundle principal ---
js_binary(
    name = "dashboard",
    entry_point = "src/main.tsx",
    data = [
        ":charts_ts",
        ":auth_ts",
        ":node_modules/react-dom",
    ],
    visibility = ["//visibility:public"],
)

# --- Tests ---
jest_test(
    name = "dashboard_test",
    config = "//:jest.config.js",
    data = [
        ":charts_ts",
        ":auth_ts",
    ],
    node_modules = "//:node_modules",
)
5
Builds Python/ML — libs/demand-forecast
PyTorch 2.1 con rules_python
libs/demand-forecast/BUILD.bazel
STARLARK
load("@rules_python//python:defs.bzl", "py_library", "py_test", "py_binary")
load("@pip//:requirements.bzl", "requirement")

py_library(
    name = "demand_forecast",
    srcs = glob(["src/**/*.py"]),
    deps = [
        requirement("torch"),
        requirement("numpy"),
        requirement("pandas"),
        "//libs/data-pipeline:pipeline",
    ],
    visibility = ["//visibility:public"],
)

py_test(
    name = "demand_forecast_test",
    srcs = glob(["tests/**/*.py"]),
    deps = [
        ":demand_forecast",
        requirement("pytest"),
        requirement("pytest-mock"),
    ],
    size = "medium",
    timeout = "moderate",
    tags = ["requires-gpu"],  # se omite en runners sin GPU
)

py_binary(
    name = "train_demand",
    srcs = ["train.py"],
    deps = [":demand_forecast"],
    data = ["//data:retail_training_data"],
)
6
Regla Custom: ml_model_package
Empaquetado de modelos PyTorch para producción
🔧
Caso de uso: DataMesh exporta modelos .pt + metadatos + requirements al Artifact Registry para despliegue en Cloud Run. Esta regla custom unifica el proceso en un único target Bazel reproducible.
tools/bazel/rules/ml_model.bzl
STARLARK
"""Regla custom para empaquetar modelos ML de DataMesh.

Produce un .tar.gz con:
  model.pt          — pesos del modelo PyTorch
  metadata.json     — versión, métricas, fecha
  requirements.txt  — dependencias de inferencia
"""

def _ml_model_package_impl(ctx):
    model_file    = ctx.file.model
    metadata      = ctx.file.metadata
    requirements  = ctx.file.requirements
    output        = ctx.actions.declare_file(ctx.attr.name + ".tar.gz")

    # Script de empaquetado generado dinámicamente
    pack_script = ctx.actions.declare_file(ctx.attr.name + "_pack.sh")
    ctx.actions.write(
        output = pack_script,
        content = """#!/bin/bash
set -euo pipefail
TMPDIR=$(mktemp -d)
cp {model} $TMPDIR/model.pt
cp {metadata} $TMPDIR/metadata.json
cp {requirements} $TMPDIR/requirements.txt
tar -czf {output} -C $TMPDIR .
rm -rf $TMPDIR
""".format(
            model        = model_file.path,
            metadata     = metadata.path,
            requirements = requirements.path,
            output       = output.path,
        ),
        is_executable = True,
    )

    ctx.actions.run(
        inputs      = [model_file, metadata, requirements],
        outputs     = [output],
        executable  = pack_script,
        mnemonic    = "MLModelPackage",
        progress_message = "Packaging ML model %s" % ctx.label,
    )

    return [DefaultInfo(
        files = depset([output]),
        runfiles = ctx.runfiles(files = [output]),
    )]

ml_model_package = rule(
    implementation = _ml_model_package_impl,
    attrs = {
        "model": attr.label(
            allow_single_file = [".pt", ".pth"],
            mandatory = True,
            doc = "Archivo de pesos PyTorch (.pt/.pth)",
        ),
        "metadata": attr.label(
            allow_single_file = [".json"],
            mandatory = True,
            doc = "metadata.json con versión y métricas",
        ),
        "requirements": attr.label(
            allow_single_file = [".txt"],
            mandatory = True,
            doc = "requirements de inferencia (sin extras dev)",
        ),
    },
    doc = "Empaqueta un modelo PyTorch para despliegue en Cloud Run.",
)

# ---- Macro de conveniencia ----
def ml_model(name, model, metadata, requirements, **kwargs):
    """Atajo que combina py_binary (train) + ml_model_package (export)."""
    ml_model_package(
        name         = name + "_package",
        model        = model,
        metadata     = metadata,
        requirements = requirements,
        **kwargs,
    )
models/demand-v3/BUILD.bazel — Uso de la regla custom
STARLARK
load("//tools/bazel/rules:ml_model.bzl", "ml_model_package")

ml_model_package(
    name         = "demand_v3_release",
    model        = "demand_v3.pt",
    metadata     = "metadata.json",
    requirements = "requirements-inference.txt",
    visibility   = ["//visibility:public"],
)

# Uso: bazel build //models/demand-v3:demand_v3_release
# Output: bazel-bin/models/demand-v3/demand_v3_release.tar.gz
7
Queries de Dependencias para DataMesh
Análisis de impacto y auditoría del grafo
¿Qué afecta si cambio data-pipeline?
bazel query \
  "rdeps(//..., //libs/data-pipeline:pipeline)" \
  --output=label

# Muestra todos los targets que dependen
# de data-pipeline (para tests inteligentes)
Targets impactados por git diff
CHANGED=$(git diff --name-only origin/main \
  | sed 's|^|//|; s|/[^/]*$||' \
  | sort -u | tr '\n' '+')

bazel query \
  "rdeps(//..., set($CHANGED))"
Grafo de dependencias visual
bazel query \
  "deps(//apps/dashboard:dashboard)" \
  --output=graph \
  | dot -Tpng -o deps_dashboard.png

# Requiere: apt install graphviz
Tests de integración únicamente
bazel query \
  "attr(tags, 'integration', //...)"

# Luego ejecutar solo esos:
# bazel test $(bazel query ...)
Tamaño del grafo de build
bazel query "deps(//...)" \
  --output=package \
  | wc -l

# Referencia DataMesh: ~340 paquetes
Dependencias circulares (detectar)
bazel query \
  "somepath(//libs/api-client:client,
            //libs/analytics-utils:utils)"

# Si devuelve path → hay ciclo potencial
8
GitHub Actions con Remote Cache
Pipeline CI optimizado para DataMesh
.github/workflows/bazel-ci.yml
YAML
name: DataMesh CI — Bazel Build & Test

on:
  push:
    branches: [main, "release/**"]
  pull_request:
    branches: [main]

jobs:
  bazel-build-test:
    runs-on: ubuntu-22.04
    permissions:
      contents: read
      id-token: write   # Para Workload Identity GCP

    steps:
      - uses: actions/checkout@v4

      - name: Autenticar en GCP (Workload Identity)
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_BAZEL_SA_KEY }}

      - name: Instalar Bazelisk
        run: |
          curl -Lo /usr/local/bin/bazel \
            https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64
          chmod +x /usr/local/bin/bazel

      - name: Cache — Repositorios externos
        uses: actions/cache@v4
        with:
          path: ~/.cache/bazel-repo-datamesh
          key: bazel-repo-${{ hashFiles('WORKSPACE.bazel', 'requirements*.txt') }}

      - name: Detectar targets impactados
        id: targets
        run: |
          BASE=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
          CHANGED=$(git diff --name-only $BASE HEAD | \
            grep -E '\.(py|ts|tsx|bzl)$' | \
            sed 's|/[^/]*$||' | sort -u)
          if [ -z "$CHANGED" ]; then
            echo "targets=//..." >> $GITHUB_OUTPUT
          else
            QUERY="rdeps(//..., set($(echo $CHANGED | tr ' ' '\n' | sed 's|^|//|' | tr '\n' ' ')))"
            TARGETS=$(bazel query "$QUERY" --output=label 2>/dev/null | tr '\n' ' ')
            echo "targets=$TARGETS" >> $GITHUB_OUTPUT
          fi

      - name: Bazel Build
        run: |
          bazel build --config=ci ${{ steps.targets.outputs.targets }}

      - name: Bazel Test
        run: |
          bazel test --config=ci \
            --test_tag_filters=-requires-gpu \
            ${{ steps.targets.outputs.targets }}

      - name: Resumen de cache
        if: always()
        run: |
          bazel info --config=ci | grep remote
9
Migración Webpack → rules_js
Plan de transición sin romper CI
Estado actual Antes
  • webpack.config.js en apps/dashboard — fuera de Bazel
  • npm scripts: build, test, lint — sin caché
  • 2 pipelines CI: uno Bazel (libs) + uno Node (apps)
  • Imposible hacer análisis de impacto cruzado
Estado objetivo Después
  • apps/dashboard con BUILD.bazel + rules_js/webpack
  • Un único pipeline Bazel con remote cache
  • bazel build //apps/dashboard:bundle reemplaza npm run build
  • Análisis rdeps cross-stack (TS + Python)
1
Wrap Webpack en genrule transitoria
Envuelve el build de Webpack existente en un genrule para que sea un target Bazel. Sin romper nada, CI sigue verde.
⏱ 2h
2
Instalar aspect_rules_js + toolchain Node
Añadir al WORKSPACE.bazel las reglas y nodejs_register_toolchains(node_version = "20.9.0").
⏱ 1h
3
Migrar libs compartidas (una a una)
Empezar por //libs/api-client y //libs/analytics-utils. Verificar que los tests pasan con bazel test.
⏱ 1-2 días por lib
4
Migrar apps/dashboard a BUILD.bazel nativo
Reemplazar la genrule transitoria con targets granulares ts_project por feature module. Eliminar webpack.config.js.
⏱ 3-4 días
5
Eliminar npm scripts del CI
Actualizar .github/workflows para usar solo bazel build / bazel test. Eliminar pasos de npm install en CI.
⏱ 1 día
10
Plan de Acción — 4 semanas
De 25 min a <5 min en CI
Semana 1 — Fundamentos
Remote Cache + .bazelrc optimizado
• Crear bucket GCP y Service Account para CI
• Aplicar nuevo .bazelrc con disk_cache + remote_cache
• Verificar hit rate en el primer CI (objetivo: >60%)
• Reducer de 25 min a ~10 min esperado
Semana 2 — Granularidad
BUILD.bazel por feature en apps/dashboard
• Dividir BUILD.bazel monolítico en 6-8 targets granulares
• Implementar análisis rdeps en el workflow de CI
• Targets afectados: sólo los módulos con cambios
• Reducción adicional de ~4 min en builds incrementales
Semana 3 — Regla ML
ml_model_package + pipeline de modelos
• Implementar regla custom ml_model_package en tools/bazel/rules/
• Migrar los 3 modelos (demand, churn, pricing) al nuevo target
• Integrar bazel build //models/...:*_package en CI de release
• Pipeline de modelos reproducible y versionado
Semana 4 — Migración Webpack
Eliminar Webpack del CI, pipeline unificado
• Wrap Webpack en genrule → migrar libs → migrar dashboard
• Un único pipeline Bazel en GitHub Actions
• Objetivo final: <6 min build limpio, <90s incremental
• Hit rate cache remoto objetivo: >85%
Resultado esperado al finalizar: Build CI limpio de 25 min → 6 min. Build incremental de 18 min → 90 segundos. Los 28 ingenieros de DataMesh recuperan ~2h/día en tiempo de espera colectivo. ROI estimado: €420/mes en compute + productividad.