Cómo llevar un script Python a Cloud Run
1. Qué es
Cloud Run es un servicio de Google Cloud que toma un script local, lo empaqueta y lo sube a la nube. A cambio te da un URL. Cada vez que alguien llama ese URL, tu script se ejecuta.
El único requisito es que el script "hable HTTP" — o sea, que tenga un servidor que esté escuchando y sepa recibir peticiones.
2. El script base (solo lógica, sin HTTP)
Punto de partida: un script Python que hace algo y devuelve un resultado. En la práctica usamos contar_palabras.py — recibe un texto y devuelve cuántas palabras tiene.
def contar_palabras(texto: str) -> int:
return len(texto.split())
if __name__ == "__main__":
texto = input("Ingresa un texto: ")
resultado = contar_palabras(texto)
print(f"El texto tiene {resultado} palabras")
Se puede probar directamente desde la terminal:
cd ~/scripts-datablog/google-cloud/script-python-a-cloud-run
python3 -m venv venv && source venv/bin/activate
python contar_palabras.py
Esto funciona perfecto en local. El problema es que Cloud Run no puede activarlo porque no tiene forma de llamarlo desde afuera.
3. La diferencia entre local y la nube
En local y en la nube el script hace lo mismo, pero el canal para pasarle los datos es completamente distinto:
En local:
- Tú arrancas el script manualmente:
python contar_palabras.py - El script te pregunta:
Ingresa un texto: - Tú escribes en la terminal y el script responde
- El canal es la terminal
En la nube:
- Nadie arranca el script manualmente — Cloud Run lo despierta cuando llega una palmadita HTTP
- No hay terminal donde escribir
- El usuario manda los datos dentro del URL
- El canal es la petición HTTP
# en local
python contar_palabras.py
→ Ingresa un texto: hola mundo
# en la nube
GET https://mi-script.run.app/contar-palabras?texto=hola mundo
El input() desaparece. El texto ya no lo escribe nadie en una terminal — viaja dentro del URL. Para que el script sepa recibir eso, necesita hablar HTTP. Eso es lo que resuelve FastAPI.
4. Hacer que el script hable HTTP en local
palmadita HTTP → uvicorn (servidor local) → FastAPI (intermediario) → tu función Python
La palmadita no le llega al script directamente — le llega al servidor. El servidor la recibe y llama al script. El script solo ejecuta lógica, no sabe nada de HTTP.
- uvicorn — abre el canal y recibe la palmadita
- FastAPI — lee la palmadita y decide qué función ejecutar. Sin FastAPI, uvicorn recibiría la palmadita y no sabría qué hacer con ella. FastAPI es el que tiene las instrucciones: a qué ruta llegó, qué método es, qué datos trae y qué función ejecutar con eso
- tu función — ejecuta la lógica y devuelve el resultado
Las opciones más usadas de framework en Python son Flask y FastAPI. En este tema usamos FastAPI.
4.1 Levantar un servidor en local
Antes de subir a Cloud Run, se prueba todo en local levantando un servidor — es decir, abrir un canal en tu Mac que quede esperando palmaditas. Es el equivalente a npm run dev en JavaScript.
uvicorn no se instala por separado — viene incluido en requirements.txt. Al correr pip install -r requirements.txt se instalan tanto fastapi como uvicorn juntos.
# primera vez
source venv/bin/activate
pip install -r requirements.txt
# siguientes veces
source venv/bin/activate
uvicorn main:app --reload
Mientras ese comando está corriendo en la terminal, tu Mac escucha en http://localhost:8000. Esa es la base — el host más el puerto. Los endpoints son rutas que se agregan encima:
http://localhost:8000 → la base (no es un endpoint en sí)
http://localhost:8000/ → endpoint raíz, definido con @app.get("/")
http://localhost:8000/contar-palabras → otro endpoint, definido con @app.get("/contar-palabras")
Cuando escribes http://localhost:8000 en el navegador, estás llamando al endpoint / — la barra final está implícita. Cada endpoint responde de forma independiente.
5. Modificar el script para que hable HTTP
El script base contar_palabras.py solo tiene lógica — no sabe nada de HTTP. Para exponerlo como endpoint creamos un segundo archivo main.py que actúa como puente: importa la función y le dice a FastAPI qué hacer cuando llega una palmadita.
La convención para nombres de endpoints es kebab-case: palabras en minúsculas separadas por guiones (/contar-palabras). El endpoint / se reserva para el health check — confirma que el servicio está vivo sin ejecutar lógica de negocio.
from fastapi import FastAPI
from contar_palabras import contar_palabras
app = FastAPI()
@app.get("/")
def root():
return {"status": "ok"}
@app.get("/contar-palabras")
def contar(texto: str):
resultado = contar_palabras(texto)
return {"texto": texto, "palabras": resultado}
Lo que hace cada línea:
from contar_palabras import contar_palabras— trae la función del otro archivoFastAPI()— crea el intermediario que define qué hacer con cada palmadita@app.get("/")— health check: responde{"status": "ok"}sin parámetros ni lógica@app.get("/contar-palabras")— el endpoint real, nombrado en kebab-casetexto: str— recibe el texto desde el URL como parámetro (?texto=hola mundo)return {...}— lo que devuelve se convierte automáticamente en JSON
5.1 Qué responde cada endpoint
/ — health check:
curl "http://localhost:8000/"
# {"status":"ok"}
/contar-palabras sin parámetro:
curl -i "http://localhost:8000/contar-palabras"
# HTTP/1.1 422 Unprocessable Entity
# {"detail":[{"type":"missing","loc":["query","texto"],"msg":"Field required","input":null}]}
FastAPI valida que todos los parámetros requeridos lleguen antes de ejecutar la función. Como texto: str es obligatorio y no llegó, responde 422 sin ejecutar la lógica.
/contar-palabras con parámetro:
Desde el navegador — los espacios se codifican con %20:
http://localhost:8000/contar-palabras?texto=esto%20es%20una%20prueba
→ {"texto":"esto es una prueba","palabras":4}
Desde curl — dos formas equivalentes:
# curl rechaza espacios directos en el URL — usar --data-urlencode para que los codifique solo
curl -G "http://localhost:8000/contar-palabras" --data-urlencode "texto=hola mundo esto es una prueba magica"
# {"texto":"hola mundo esto es una prueba magica","palabras":7}
# alternativa: codificar los espacios manualmente con %20
curl "http://localhost:8000/contar-palabras?texto=hola%20mundo%20esto%20es%20una%20prueba%20magica"
# {"texto":"hola mundo esto es una prueba magica","palabras":7}
Endpoint que no existe:
curl "http://localhost:8000/ruta-inventada"
# {"detail":"Not Found"}
FastAPI responde 404 automáticamente para cualquier ruta que no esté definida en main.py.
Resumen — hasta aquí
Con los pasos 1 al 5 ya se tiene un script Python funcionando como API REST en local:
- Script base (
contar_palabras.py) — contiene solo la lógica, sin saber nada de HTTP main.py— actúa como puente: importa la función y define los endpoints con FastAPI- uvicorn — levanta el servidor y abre el canal en
http://localhost:8000 - FastAPI — recibe cada palmadita y la dirige al endpoint correcto según la ruta
Los endpoints definidos y sus respuestas:
| Llamada | Respuesta | Por qué |
|---|---|---|
GET / |
{"status":"ok"} |
Health check — confirma que el servicio está vivo |
GET /contar-palabras?texto=hola |
{"texto":"hola","palabras":1} |
Ejecuta la lógica |
GET /contar-palabras (sin parámetro) |
422 Unprocessable Entity |
FastAPI detecta que falta texto |
GET /ruta-inventada |
404 Not Found |
La ruta no está definida en main.py |
Limitación actual: el servidor solo recibe palmaditas desde la misma Mac. Para que cualquiera en internet pueda llamarlo hay que empaquetarlo y subirlo a Cloud Run — eso es lo que hacen los pasos siguientes.
6. Empaquetarlo en Docker
El problema sin empaquetar es este: tu script funciona en tu Mac porque tienes Python instalado, las dependencias instaladas y el sistema operativo correcto. Pero Cloud Run no sabe nada de eso — es una máquina en blanco.
Empaquetar significa meter el script junto con todo lo que necesita para funcionar — Python, dependencias, archivos — en una "caja" estandarizada llamada contenedor Docker. Cloud Run recibe esa caja y la ejecuta sin necesidad de instalar nada.
El contenedor resuelve eso:
tu Mac contenedor Docker
────────────────── ──────────────────────────
Python 3.12 instalado → Python 3.12 incluido adentro
fastapi instalado → fastapi incluido adentro
uvicorn instalado → uvicorn incluido adentro
contar_palabras.py → contar_palabras.py adentro
main.py → main.py adentro
El Dockerfile es la receta que le dice a Docker cómo construir esa caja. Independientemente de cómo se suba el código a Cloud Run — por CLI o por interfaz — Google Cloud lee el Dockerfile y construye el contenedor en sus propios servidores. No hace falta tener Docker instalado en local.
sin Docker local: escribes la receta → Google Cloud cocina → Cloud Run sirve
con Docker local: escribes la receta → tú cocinas y pruebas → Google Cloud cocina → Cloud Run sirve
Docker local sirve para detectar errores en el contenedor antes de subirlo. Para aprendizaje, con dejar que Google Cloud cocine es suficiente.
El Dockerfile no reemplaza al requirements.txt — los dos cumplen roles distintos:
requirements.txt— lista qué dependencias instalar (fastapi,uvicorn)Dockerfile— orquesta todo: qué sistema base usar, dónde meter los archivos, cómo instalar las dependencias y cómo arrancar el servidor
El Dockerfile en realidad usa el requirements.txt en uno de sus pasos:
RUN pip install -r requirements.txt ← aquí lee el requirements.txt
Cloud Run no recibe los archivos .py directamente — necesita el contenedor completo. El Dockerfile le dice cómo construirlo.
El Dockerfile usa su propio lenguaje — no es Python ni bash. Sus instrucciones son específicas de Docker y cada una representa un paso en la construcción del contenedor:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
Lo que hace cada línea:
FROM python:3.12-slim— parte de una caja base que ya tiene Python 3.12 instalado. Docker tiene una biblioteca de cajas base, no hay que instalar Python desde ceroWORKDIR /app— dentro de la caja, trabaja en la carpeta/app. Es como hacercd /app— todos los comandos siguientes se ejecutan desde ahíCOPY requirements.txt .— copia elrequirements.txtde tu Mac a la carpeta/appde la cajaRUN pip install -r requirements.txt— instala las dependencias dentro de la caja. Aquí es donde fastapi y uvicorn quedan instalados adentro del contenedorCOPY . .— copia todo lo demás de tu carpeta a la caja:contar_palabras.py,main.py, etc.CMD [...]— cuando alguien encienda esta caja, ejecuta este comando. Es el equivalente a escribiruvicorn main:appen la terminal, pero se ejecuta automáticamente al arrancar el contenedor en Cloud Run
El CMD usa flags distintos a los del servidor local porque en Cloud Run las palmaditas vienen de afuera del contenedor:
- --host 0.0.0.0 — en local uvicorn escucha solo en 127.0.0.1 (tu Mac). Con 0.0.0.0 escucha en todas las interfaces
- --port 8080 — en local uvicorn usa el puerto 8000. Cloud Run espera que el contenedor escuche en el 8080
7. Evolución de archivos del proyecto
El proyecto fue creciendo en tres etapas según lo que necesitábamos:
Etapa 1 — script local puro (solo lógica, se ejecuta desde la terminal):
script-python-a-cloud-run/
├── contar_palabras.py → la lógica
└── requirements.txt → las dependencias
Etapa 2 — script que habla HTTP (funciona como API en local):
script-python-a-cloud-run/
├── contar_palabras.py → la lógica
├── main.py → el puente HTTP (endpoints, FastAPI)
└── requirements.txt → las dependencias
Etapa 3 — listo para Cloud Run (empaquetado, deployable a la nube):
script-python-a-cloud-run/
├── contar_palabras.py → la lógica
├── main.py → el puente HTTP (endpoints, FastAPI)
├── requirements.txt → las dependencias
└── Dockerfile → la receta del contenedor
8. Subirlo a Cloud Run
gcloud run deploy mi-script \
--source . \
--region us-central1 \
--allow-unauthenticated
--source . le dice a gcloud que construya la imagen Docker desde la carpeta actual y la suba directo. Al terminar, devuelve un URL.
Para poder ejecutar este comando hay que conocer primero los siguientes temas:
- Google Cloud CLI — LINK PENDIENTE
- Proyectos en Google Cloud — LINK PENDIENTE
- APIs en Google Cloud (Cloud Run y Cloud Build ) — LINK PENDIENTE
9. ¿Cómo quedaría en Cloud Run?
tú → GET https://mi-script-abc123-uc.a.run.app
↓
Cloud Run despierta el contenedor
↓
FastAPI recibe la petición
↓
Se ejecuta tu función Python
↓
Devuelve JSON como respuesta
Diferencia con local: El flujo es idéntico — cambia solo que ahora cualquier persona en internet puede hacer palmaditas, no solo tu Mac.
Riesgos:
- Cualquiera puede hacer una palmadita a tu endpoint siempre — con o sin restricciones. Lo que cambia con
--allow-unauthenticatedes que cualquiera recibe la respuesta completa sin necesidad de identificarse - Si alguien encuentra el URL y lo llama miles de veces, tu script se ejecuta miles de veces — esto puede generar costos inesperados o saturar el servicio. Esto se controla con rate limiting (máximo N llamadas por minuto), autenticación (token requerido) o Cloudflare como intermediario. El rate limiting por IP solo no es suficiente — se puede evadir con proxies
Costos:
- Cloud Run cobra por tiempo de ejecución — cada vez que llega una palmadita y el contenedor se activa
- Tiene una capa gratuita generosa (2 millones de solicitudes al mes), suficiente para aprendizaje
- Si nadie llama el URL, no hay costo — el contenedor está apagado
10. Hasta dónde puedes llegar con Cloud Run
Cloud Run no es solo para scripts simples. La palmadita (petición HTTP) puede desencadenar cosas cada vez más complejas:
| Nivel | Qué hace | Método |
|---|---|---|
| 1 | Script simple — un URL, una función, una respuesta | GET |
| 2 | Varios endpoints en un mismo Cloud Run | GET |
| 3 | Recibe datos más complejos en el body | POST |
| 4 | Conecta con otra API externa (OpenAI, Gemini, etc.) | GET / POST |
| 5 | Lee o escribe en una base de datos | GET / POST |
| 6 | Aplicación web completa — devuelve HTML en vez de JSON | GET, POST, DELETE |
Estamos en el nivel 1. Cada nivel siguiente agrega una capa de complejidad pero el principio es siempre el mismo: palmadita → Cloud Run despierta → hace algo → devuelve respuesta.
Gotchas
- Cloud Function vs Cloud Run — las dos ejecutan código en respuesta a palmaditas HTTP, pero la diferencia es quién resuelve el HTTP. En Cloud Function lo resuelve Google con su propio framework (
@functions_framework.http) — solo necesitasmain.pyyrequirements.txt. En Cloud Run lo resuelves tú con FastAPI + uvicorn + Dockerfile. Más trabajo, más control. Para una función simple, Cloud Function es más rápido; Cloud Run vale cuando tienes varios endpoints o necesitas controlar el entorno. - Cloud Run apaga el contenedor cuando no hay tráfico — no es un servidor permanente, se "despierta" con cada petición
- Los espacios en el URL se convierten automáticamente a
%20— es la forma estándar de representar espacios en HTTP. FastAPI los decodifica solo, la función recibe el texto normal: - Abuso de endpoints públicos: Si alguien encuentra el URL y lo llama miles de veces, el script se ejecuta miles de veces — generando costos o saturando el servicio. El rate limiting por IP no es suficiente porque se puede evadir con proxies. La primera línea de defensa real es la autenticación. Para protección adicional sin depender del proveedor de nube, Cloudflare actúa como intermediario (
usuario → Cloudflare → Cloud Run) y filtra tráfico abusivo antes de que llegue al servicio.