La historia de cómo conecté Cloud Run con Cloud SQL usando IP privada sin pagar el conector VPC, y cómo recuperé mi infraestructura cuando Terraform "olvidó" todo.
El sueño del MVP barato
Cuando empecé a construir Geo-Engine, mi SaaS de procesamiento geoespacial con Go, tenía una meta clara: quería una arquitectura profesional, pero no quería pagar cientos de dólares al mes mientras buscaba mis primeros usuarios.
La elección obvia fue Google Cloud Platform (GCP):
- Cloud Run: Para escalar a cero (pagar solo por uso).
- Cloud SQL (Postgres): Para tener una base de datos gestionada y sólida.
- Terraform: Para tener todo como código (IaC).
Sonaba perfecto en papel, pero la realidad me golpeó con dos problemas que casi descarrilan el proyecto: Terraform queriendo borrar mi producción y una factura potencial innecesaria por conectividad de red. Aquí te cuento cómo solucioné ambos.
Problema 1: La Amnesia de Terraform (State Drift)
Desarrollo desde mi laptop (Fedora) y a veces desde la nube (Project IDX). Un día, al intentar desplegar desde el entorno nuevo, Terraform me dio el susto de mi vida:
Terraform will perform the following actions:
# google_cloud_run_service.api will be created
# google_sql_database_instance.master will be created
...
Plan: 5 to add, 0 to change, 0 to destroy.
¿Crear? ¡Pero si ya existían! Terraform quería duplicar todo (o borrar y recrear).
Lo que aprendí: El archivo terraform.tfvars (donde guardo mi project_id y variables sensibles) estaba en mi .gitignore. Al cambiar de entorno, Terraform usó valores por defecto, apuntó a un "limbo" y decidió que no había nada creado.
La Solución:
- Recrear variables: Asegurarme de que el project_id en el nuevo entorno fuera idéntico al real.
- Importar, no recrear: Usé el comando import para decirle a Terraform: "Oye, este recurso ya existe, mételo en tu memoria".
terraform import google_cloud_run_v2_service.default projects/MI_PROYECTO/locations/us-central1/services/geo-api
Esto sincronizó mi estado local con la realidad de la nube y el plan volvió a decir "No changes". Paz mental restaurada.
Problema 2: Conectar Cloud Run a SQL (La trampa de los $17 USD)
Por seguridad, mi base de datos solo tiene IP Privada. Pero Cloud Run vive "fuera" de mi VPC. La documentación oficial recomienda usar un Serverless VPC Access Connector. El problema: Ese conector cuesta mínimo ~$17 USD/mes porque mantiene instancias VM encendidas 24/7. Para un MVP sin ingresos, eso es un "impuesto" doloroso.
La Solución: Direct VPC Egress.
Descubrí que Cloud Run ahora soporta una conexión directa a la VPC sin intermediarios costosos. Solo necesitaba configurar correctamente el bloque de red en Terraform.
Aquí está el fragmento de código que me ahorró esos $200 dólares al año:
resource "google_cloud_run_v2_service" "default" {
name = "geo-api"
location = "us-central1"
template {
# ... configuración del contenedor ...
vpc_access {
network_interfaces {
network = "default"
subnetwork = "default"
}
# ESTA ES LA CLAVE:
egress = "PRIVATE_RANGES_ONLY"
}
}
}
Con PRIVATE_RANGES_ONLY, Cloud Run enruta el tráfico hacia mi base de datos (IP 10.x.x.x) a través de la red interna de Google, y el resto del tráfico sale a internet normalmente. Costo extra: $0.
Conclusión
Construir Geo-Engine me enseñó que la infraestructura como código es poderosa, pero requiere disciplina. Hoy tengo un stack corriendo Go, Postgres y Cloud Run que es:
- Seguro: Todo en red privada.
- Económico: Escala a cero y no paga conectores ociosos.
- Reproducible: Gracias a Terraform (y a saber usar import).
Si te interesa ver el resultado final, puedes probar la beta de mi herramienta aquí: Geo-Engine.
🎁 Bonus: SDK de Node.js Oficial
Mientras ajustaba la infraestructura, me di cuenta de que necesitaba una forma fácil de consumir mi propia API. Así que aproveché para empaquetar y publicar el SDK oficial.
Usando tsup, logré generar un paquete híbrido que soporta tanto ES Modules (moderno) como CommonJS (legacy) automáticamente.
La estructura final quedó así de limpia:
geo-engine-node/
├── dist/
│ ├── index.js (ES Modules - import)
│ ├── index.cjs (CommonJS - require)
│ └── index.d.ts (TypeScript Definitions)
└── package.json

Top comments (1)
Muy buen artículo!! Enhorabuena