# Cuando montar un GitHub interno para modelos de IA te cuesta menos de US$200 al mes: arquitectura completa con Google Cloud y TensorFlow

*Photo: [Igor Omilaev](https://unsplash.com/@omilaev) on Unsplash*
Hace dos semanas, una startup de Barcelona con la que trabajo me compartió su problema: tenían seis data scientists entrenando variaciones del mismo modelo de clasificación de texto. Sin saber qué había probado el otro, duplicaban experimentos y perdían días en configuraciones que otros ya habían descartado. El CTO quería contratar MLflow Enterprise. Sin embargo, le armé una plataforma colaborativa completa en Google Cloud por menos de lo que cuesta un desarrollador junior al mes.
Esta no es una guía sobre herramientas enterprise ni sobre adoptar plataformas cerradas. Es la arquitectura exacta que implementé: desde el almacenamiento de datasets compartidos hasta el versionado de modelos, pasando por notebooks colaborativos y pipelines de CI/CD para TensorFlow. Todo esto, con un presupuesto de seed stage.
## Por qué Google Cloud (y no AWS ni Azure) para colaboración en IA
La decisión de plataforma no es teológica, sino práctica. He probado las tres grandes clouds construyendo sistemas similares y, en mi experiencia, Google Cloud tiene tres ventajas concretas para equipos pequeños que trabajan con TensorFlow:
**Vertex AI Workbench es gratuito hasta cierto punto**. Puedes tener notebooks compartidos, con control de versiones integrado, y realmente solo pagas por el compute. En AWS, necesitas SageMaker Studio, que suma US$50-100 al mes antes de tocar una GPU. En Azure necesitas ML Studio, con costos similares.
**Cloud Storage tiene integraciones nativas con TensorFlow**. Puedes leer datasets directamente desde buckets sin descargar localmente usando `tf.data.Dataset` con prefijos `gs://`. Lo curioso es que esto suena trivial hasta que tienes datasets de 50GB y tres personas intentando acceder simultáneamente.
**El IAM de Google es el más granular**. Puedes dar acceso de lectura a datasets, escritura solo a modelos entrenados y ejecución de notebooks sin dar acceso root al proyecto. En AWS, esto requiere configurar múltiples roles y policies que se vuelven inmanejables.
El stack completo que vamos a montar incluye:
- Cloud Storage para datasets y modelos versionados
- Vertex AI Workbench para notebooks colaborativos
- Cloud Build para CI/CD de entrenamientos
- Artifact Registry para imágenes Docker personalizadas
- Cloud Run para servir modelos como APIs (opcional pero recomendado)
El costo mensual real con uso moderado (3-4 personas, entrenamientos diarios) se sitúa entre US$150-200. ¿Y con AWS/Azure? Uff, entre US$400-600.
## Arquitectura de storage: cómo versionar datasets y modelos sin volverse loco

*Photo: [Steve A Johnson](https://unsplash.com/@steve_j) on Unsplash*
El error número uno que veo en equipos empezando con ML colaborativo es tratar los modelos como código y los datasets como archivos estáticos. No funcionan igual, y eso es clave.
**Estructura de buckets que realmente escala:**
ml-platform-datasets/ ├── raw/ │ ├── 2026-01-15_customer_reviews_v1.parquet │ └── 2026-01-22_customer_reviews_v2.parquet ├── processed/ │ ├── sentiment_train_v1/ │ └── sentiment_train_v2/ └── metadata/ └── dataset_versions.json
ml-platform-models/ ├── experiments/ │ ├── user_john/ │ │ └── sentiment_lstm_2026_01_20/ │ └── user_maria/ │ └── sentiment_transformer_2026_01_21/ ├── staging/ │ └── sentiment_model_v1.2_candidate/ └── production/ └── sentiment_model_v1.1/
**Reglas que mantenemos religiosamente:**
1. **Raw nunca se modifica**. Cada versión de dataset crudo es append-only con timestamp.
2. **Processed tiene versionado explícito**. Si cambias el preprocesamiento, es una versión nueva.
3. **Experiments es tierra de nadie**. Cada usuario tiene su carpeta y hace lo que quiera.
4. **Staging y production requieren metadata obligatoria**: qué dataset usó, hiperparámetros, métricas, quién lo subió.
**Implementación del metadata tracking:**
```python
# dataset_versioning.py
from google.cloud import storage
import json
from datetime import datetime
class DatasetVersioner:
def __init__(self, bucket_name):
self.client = storage.Client()
self.bucket = self.client.bucket(bucket_name)
self.metadata_blob = self.bucket.blob('metadata/dataset_versions.json')
def register_dataset(self, path, description, author, schema_changes=None):
"""Registra una nueva versión de dataset con metadata"""
metadata = self._load_metadata()
version_id = f"v{len(metadata) + 1}"
metadata[version_id] = {
'path': path,
'description': description,
'author': author,
'timestamp': datetime.now().isoformat(),
'schema_changes': schema_changes or [],
'used_by_models': []
}
self._save_metadata(metadata)
return version_id
def link_model_to_dataset(self, model_path, dataset_version):
"""Vincula un modelo entrenado con su dataset"""
metadata = self._load_metadata()
if dataset_version in metadata:
metadata[dataset_version]['used_by_models'].append({
'model_path': model_path,
'timestamp': datetime.now().isoformat()
})
self._save_metadata(metadata)
def _load_metadata(self):
try:
content = self.metadata_blob.download_as_string()
return json.loads(content)
except:
return {}
def _save_metadata(self, metadata):
self.metadata_blob.upload_from_string(
json.dumps(metadata, indent=2),
content_type='application/json'
)
# Uso en tu notebook o script
versioner = DatasetVersioner('ml-platform-datasets')
dataset_v = versioner.register_dataset(
path='processed/sentiment_train_v3/',
description='Añadido filtrado de spam, balanceo de clases',
author='maria@startup.com',
schema_changes=['removed column: ip_address', 'added: sentiment_score_normalized']
)
Este sistema simple nos salvó cuando un modelo en producción empezó a fallar y necesitábamos rastrear exactamente qué datos usó para entrenarse. Sin ello, hubiéramos tardado días en reconstruir el pipeline.
Notebooks colaborativos que no terminan en merge hell
Vertex AI Workbench te da notebooks Jupyter con Git integrado, pero ojo, la integración por defecto es básica. Necesitas convenciones claras o terminas con conflictos imposibles de resolver.
Setup inicial del Workbench:
- Crea una instancia en Vertex AI Workbench (no uses Managed Notebooks, usa User-Managed para tener control completo).
- Tamaño mínimo: n1-standard-4 (4 vCPUs, 15GB RAM) - US$120/mes si está prendida 24/7, pero la apagas cuando no trabajas.
- Agrega GPU solo cuando realmente necesites entrenar modelos grandes. Para exploración y experimentación, CPU es suficiente.
- Conecta un repositorio Git desde el launcher.
Estructura de repositorio que funciona:
ml-experiments/
├── notebooks/
│ ├── exploration/
│ │ ├── 01_data_analysis_john.ipynb
│ │ └── 02_feature_engineering_maria.ipynb
│ ├── training/
│ │ └── sentiment_model_v1.ipynb # Este se versiona y revisa
│ └── evaluation/
│ └── model_comparison.ipynb
├── src/
│ ├── preprocessing/
│ ├── models/
│ └── utils/
├── tests/
└── configs/
└── training_config.yaml
Regla de oro: el código de producción vive en /src/, mientras que la exploración está en /notebooks/.
Los notebooks en exploration/ pueden ser personales y caóticos. Aquellos de training/ y evaluation/ se revisan en PR como código normal.
Ejemplo de notebook de entrenamiento colaborativo amigable:
# notebooks/training/sentiment_classifier_v2.ipynb
# Celda 1: Configuración (siempre la misma estructura)
import sys
sys.path.append('../../src')
from preprocessing import load_and_preprocess
from models import SentimentClassifier
from utils import upload_model_to_gcs
import yaml
with open('../../configs/training_config.yaml') as f:
config = yaml.safe_load(f)
# Celda 2: Carga de datos (referencia a versión específica)
dataset_version = 'v3' # Explícito, nunca 'latest'
train_data, val_data = load_and_preprocess(
bucket='ml-platform-datasets',
path=f'processed/sentiment_train_{dataset_version}/',
config=config['preprocessing']
)
# Celda 3: Construcción del modelo
model = SentimentClassifier(
vocab_size=config['model']['vocab_size'],
embedding_dim=config['model']['embedding_dim'],
lstm_units=config['model']['lstm_units']
)
# Celda 4: Entrenamiento
history = model.fit(
train_data,
validation_data=val_data,
epochs=config['training']['epochs'],
callbacks=[...]
)
# Celda 5: Evaluación y subida
metrics = model.evaluate(val_data)
print(f"Validation accuracy: {metrics['accuracy']:.4f}")
if metrics['accuracy'] > config['promotion_threshold']:
model_path = upload_model_to_gcs(
model=model,
bucket='ml-platform-models',
path=f'staging/sentiment_v2_{datetime.now().strftime("%Y%m%d")}/',
metadata={
'dataset_version': dataset_version,
'config': config,
'metrics': metrics
}
)
print(f"Model uploaded to: {model_path}")
La clave aquí es que todo lo configurable está en YAML, todo lo reutilizable en /src/, y el notebook es simplemente el "pegamento" que orquesta. Así, cuando alguien hace un PR del notebook, revisas lógica de entrenamiento, no implementación de capas LSTM.
CI/CD para modelos: automatizando entrenamientos sin morir en el intento
Aquí es donde el 80% de los equipos pequeños se rinde y vuelve a entrenar manualmente. Cloud Build y Cloud Run hacen que valga la pena el setup.
Dockerfile para entrenamiento reproducible:
# training.Dockerfile
FROM tensorflow/tensorflow:2.15.0-gpu
WORKDIR /app
# Dependencias
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Código fuente
COPY src/ ./src/
COPY configs/ ./configs/
# Script de entrenamiento
COPY scripts/train.py .
# Credentials de GCP (se inyectan en runtime)
ENV GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-key.json
ENTRYPOINT ["python", "train.py"]
Cloud Build config que dispara entrenamientos:
# cloudbuild.yaml
steps:
# Build de la imagen de entrenamiento
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'-t', 'gcr.io/$PROJECT_ID/sentiment-trainer:$SHORT_SHA',
'-f', 'training.Dockerfile',
'.'
]
# Push al Artifact Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/sentiment-trainer:$SHORT_SHA']
# Ejecutar entrenamiento
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: 'gcloud'
args:
- 'run'
- 'jobs'
- 'create'
- 'sentiment-training-$SHORT_SHA'
- '--image=gcr.io/$PROJECT_ID/sentiment-trainer:$SHORT_SHA'
- '--region=us-central1'
- '--task-timeout=3h'
- '--memory=8Gi'
- '--cpu=4'
- '--execute-now'
timeout: 4h
options:
machineType: 'N1_HIGHCPU_8'
Trigger automático en Git push:
En Cloud Build, puedes configurar un trigger que escucha cambios en main del repositorio. Cuando alguien mergea un PR que modifica configs/training_config.yaml o código en src/models/, esto dispara automáticamente un nuevo entrenamiento.
Lo crítico es que el script train.py debe ser idempotente y guardar checkpoints con timestamp único. Si falla a mitad de camino, puede resumir.
Script de entrenamiento production-ready:
# scripts/train.py
import argparse
import tensorflow as tf
from google.cloud import storage
import yaml
import json
from datetime import datetime
def train_model(config_path, dataset_version, output_bucket):
# Carga config