Introducción a Cloud Run Sidecars

Paradigma Digital
7 min readApr 1, 2024

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.

¿Qué es Cloud Run?

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.

¿Cuál es la novedad?

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.

Fuente: Google

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:
Fuente: Google
  • Usar un proxy, como pueden ser Nginx, Envoy o Apache2, delante de nuestro contenedor:
Fuente: Google

. Añadir filtro de autorización o autenticación:

Fuente: Google

Caso de ejemplo

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.

Punto de partida

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:

Para cerrar

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.

Originally published at https://www.paradigmadigital.com.

--

--