A pocos sorprenderá saber que dentro del equipo de Goodly, Cloud Run es uno de nuestros productos favoritos presentes en GCP. Para Google también parece ser muy importante porque las novedades se suceden continuamente.
Solo en 2023 se publicaron más de 70 novedades. Una de estas últimas novedades fue presentada en preview el pasado mes de mayo y liberada para uso general a mediados de noviembre del mismo.
Pese a que deben quedar pocas personas que no hayan oído hablar de Cloud Run, Cloud Run es una plataforma de computación totalmente gestionada basada en Knative que nos permite desplegar cargas de una manera sencilla, delegando las tareas relacionadas con infraestructura como pueden ser el aprovisionamiento, el escalado y la configuración.
Google anunció el pasado mayo que tenemos en preview una nueva funcionalidad por la que podremos correr un contenedor sidecar junto al contenedor principal. Hasta ahora un Cloud Run solo podía correr un único contenedor.
Esto nos abre un abanico importante de posibilidades para extender las capacidades de nuestros Runs de una manera simple y modular, como por ejemplo:
- Ejecutar aplicaciones de monitoring, logging and tracing:
- Usar un proxy, como pueden ser Nginx, Envoy o Apache2, delante de nuestro contenedor:
- Añadir filtro de autorización o autenticación:
Uno de los casos de uso que a mí particularmente me parece más interesante de esta nueva funcionalidad presentada, es usar los sidecars para añadir un filtro de autorización a nuestros Cloud Runs.
Además, en nuestro caso en particular nos vino como anillo al dedo para abordar un proyectillo en el que andábamos trabajando. Partíamos de un Cloud Run muy sencillo que tenía un check para comprobar en cada llamada si esta era legítima o no. En caso de no serlo, se devolvía un error; y en caso de pasar la validación, la petición era procesada.
Este caso en particular es algo que la tecnología ya permitía abordar de diferentes maneras, pero además es uno de los ejemplos en los que el uso de Cloud Run con sidecars nos puede ayudar y es con el que vamos a trabajar durante este post.
Vamos a suponer un ejemplo muy sencillo, un API que además incluye la validación de la request para saber si tiene que servir la respuesta o no. Para simplificar el ejemplo vamos a suponer que la única comprobación que se va a hacer es comprobar la presencia de una cabecera en la request. Esta cabecera es “token” y el valor debería ser “RXN0b05vRXNOYWRhSW1wb3J0YW50ZQ==”.
El código sería algo así:
from fastapi import Request, FastAPI, status
from fastapi.responses import JSONResponse
import uvicorn
import os
SUCCESS_TOKEN = "RXN0b05vRXNOYWRhSW1wb3J0YW50ZQ=="
class NoAuthException(Exception):
pass
app = FastAPI()
def checkRequest(req):
print("Checking request...")
token = req.headers.get('token')
if (token!=SUCCESS_TOKEN):
print("Checking KO")
raise NoAuthException("Authorize error")
print("Checking OK")
@app.get("/")
def proxy(req: Request):
try:
checkRequest(req)
data = {
"foo": "bar"
}
return JSONResponse(content=data)
except Exception:
data = {}
return JSONResponse(content=data, status_code=status.HTTP_403_FORBIDDEN)
if __name__ == "__main__":
uvicorn.run(app, host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
Partiendo de esto vamos a ver cómo separarlo en dos contenedores para lanzarlos en un Cloud Run con sidecar.
En primer lugar, vamos a eliminar la comprobación del API, dejando lo que vendría a ser puramente el API:
from fastapi import Request, FastAPI
from fastapi.responses import JSONResponse
import uvicorn
import os
app = FastAPI()
@app.get("/")
def proxy(req: Request):
print("API Request")
data = {
"foo": "bar"
}
return JSONResponse(content=data)
if __name__ == "__main__":
uvicorn.run(app, host='0.0.0.0', port=int(os.environ.get('API_PORT', 8888)))
Por otro lado, vamos a crear un nuevo contenedor. Este se va a encargar de ejecutar las validaciones que anteriormente hemos comentado para autorizar o no las llamadas finales al API. En caso de que estas validaciones se cumplan reenviará la petición a nuestro API:
from fastapi import Request, Body, FastAPI, status
from fastapi.responses import HTMLResponse
import os
import json
from urllib import request, parse
import uvicorn
SUCCESS_TOKEN = "RXN0b05vRXNOYWRhSW1wb3J0YW50ZQ=="
class NoAuthException(Exception):
pass
BASE_URL = os.environ.get("BASE_URL", "http://127.0.0.1:8888")
app = FastAPI()
def checkRequest(req):
print("Checking request...")
token = req.headers.get('token')
if (token!=SUCCESS_TOKEN):
print("Checking KO")
raise NoAuthException("Authorize error")
print("Checking OK")
@app.get("/{path:path}")
async def proxy(req: Request, path: str):
try:
checkRequest(req)
print("Processing request...")
url = '{}/{}'.format(BASE_URL, path)
print(f"url: {url}")
dest_req = request.Request(url)
dest_req.add_header('Content-Type', 'application/json')
print(f"Trying to connect to {url}")
with request.urlopen(dest_req) as dest_rsp:
print(f"Connection established successfully")
return HTMLResponse(content=dest_rsp.read(), status_code=dest_rsp.status)
except NoAuthException as error:
print("Authorize error")
return HTMLResponse(content=None, status_code=status.HTTP_403_FORBIDDEN)
except Exception as error:
print(f"Error when try to connect to {url}: {error}")
return HTMLResponse(content=None, status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
if __name__ == "__main__":
uvicorn.run(app, host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
Una vez tenemos separado el código, necesitamos subir al Artifact Registry las imágenes de ambos. Para ello añadiendo el siguiente Dockerfile las creamos y las subimos:
# Use the official lightweight Python image.
# https://hub.docker.com/_/python
FROM python:3.11-slim
# Allow statements and log messages to immediately appear in the logs
ENV PYTHONUNBUFFERED True
ENV HNSWLIB_NO_NATIVE=1
# Copy local code to the container image.
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . ./
# Install production dependencies.
RUN pip install --no-cache-dir -r requirements.txt
CMD exec uvicorn main:app --host 0.0.0.0 --port $PORT --reload
Nuestro ArtifactRegistry lo hemos creado en europe-west3. Por lo que tras crearlo nos autenticamos, creamos las imágenes y las subimos desde la consola:
> docker buildx build --platform linux/amd64 -t api-mock .
> docker tag api-mock europe-west3-docker.pkg.dev/sandbox-jrberenguer/artifact-registry/api-mock
> docker push europe-west3-docker.pkg.dev/sandbox-jrberenguer/artifact-registry/api-mock
> docker buildx build --platform linux/amd64 -t proxy .
> docker tag proxy europe-west3-docker.pkg.dev/sandbox-jrberenguer/artifact-registry/proxy
> docker push europe-west3-docker.pkg.dev/sandbox-jrberenguer/artifact-registry/proxy
¿Y cómo juntamos todo esto para desplegar la solución completa? Simplemente, tenemos que definir un service en un fichero yaml:
— — —
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: cloudrun-sidecar-test
labels:
cloud.googleapis.com/location: europe-west3
annotations:
run.googleapis.com/launch-stage: BETA
run.googleapis.com/description: sample tutorial service
run.googleapis.com/ingress: all
spec:
template:
metadata:
annotations:
run.googleapis.com/container-dependencies: "{proxy: [api]}"
spec:
containers:
- image: europe-west3-docker.pkg.dev/sandbox-jrberenguer/artifact-registry/proxy:latest
name: proxy
env:
- name: BASE_URL
value: "http://127.0.0.1:8888"
ports:
- name: http1
containerPort: 80
resources:
limits:
cpu: 500m
memory: 256Mi
- image: europe-west3-docker.pkg.dev/sandbox-jrberenguer/artifact-registry/api-mock:latest
name: api
env:
- name: API_PORT
value: '8888'
- name: PROJECT_ID
value: 'sandbox-jrberenguer'
resources:
limits:
cpu: 500m
memory: 256Mi
En este fichero vamos a definir los contenedores (en nuestro caso dos) y las dependencias entre ellos. Analizado por partes sería:
Creamos el servicio que en este caso llamaremos cloudrun-sidecar-test:
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: cloudrun-sidecar-test
labels:
cloud.googleapis.com/location: europe-west3
annotations:
run.googleapis.com/launch-stage: BETA
run.googleapis.com/description: sample tutorial service
run.googleapis.com/ingress: all
Definimos las dependencias entre los contenedores, en este caso solo hay una…
metadata:
annotations:
run.googleapis.com/container-dependencies: "{proxy: [api]}"
Por otro lado, tenemos los dos contenedores que hemos definido más arriba.
Proxy:
- image: europe-west3-docker.pkg.dev/sandbox-jrberenguer/artifact-registry/proxy:latest
name: proxy
env:
- name: BASE_URL
value: "http://127.0.0.1:8888"
ports:
- name: http1
containerPort: 80
resources:
limits:
cpu: 500m
memory: 256Mi
API:
- image: europe-west3-docker.pkg.dev/sandbox-jrberenguer/artifact-registry/api-mock:latest
name: api
env:
- name: PORT
value: '8888'
- name: PROJECT_ID
value: 'sandbox-jrberenguer'
resources:
limits:
cpu: 500m
memory: 256Mi
Una vez definido el fichero service, solo nos queda desplegarlo:
Una vez desplegado el servicio, ahora toca probarlo. Para esto simplemente vamos a lanzar algunas peticiones al servicio para ver si realmente está aplicando el filtro o no.
En el proxy el check que vamos a hacer es simplemente que el valor de la cabecera token sea uno en concreto:
SUCCESS_TOKEN = "RXN0b05vRXNOYWRhSW1wb3J0YW50ZQ=="
def checkRequest(req):
print("Checking request...")
token = req.headers.get('token')
if (token!=SUCCESS_TOKEN):
print("Checking KO")
raise NoAuthException("Authorize error")
print("Checking OK")
Si lanzamos un curl con un token incorrecto, obtenemos un 403:
Si, por el contrario, añadimos el token válido obtenemos un 200 y la respuesta correcta:
En este post hemos podido ver cómo resolver uno de los casos de uso que esta nueva actualización de Cloud Run viene a solventar: añadir filtros de autorización a nuestro contenedor principal. Pero como hemos mencionado anteriormente, este no es el único caso de uso que viene a cubrir esta nueva feature. Prometheus o incluso OpenTelemetry con Cloud Run ahora es posible.
Cloud Run sigue siendo para muchos uno de los productos estrella de GCP y Google lo sabe, ya que no deja de evolucionarlo y hacerlo cada vez más potente y más usable para incorporarlo a nuestras soluciones tecnológicas en la nube, posicionándolo en el top de los productos serverless.
Tell us what you think.