Try   HackMD
tags: ASO-GIT Flask Google Cloud Run

Servicio web con flask en Cloud Run

Creamos una imagen nueva a partir de python:latest.

FROM python:latest COPY requirements.txt /requirements.txt RUN pip install -r requirements.txt COPY app app WORKDIR /app CMD ["python", "main.py"]

El archivo dockerfile hace referencia a requirements.txt que con tiene el nombre de las bibliotecas (dependencias) que necesitamos que el contenedor tenga disponibles.

Creamos el programa que responderá a las peticiones del servicio web:

HTTP GET /hello
HTTP GET /bye

Para contruir ese programa haremos uso de Flask, y para ello lo añadimos como dependencia al archivo requirements.txt.

Flask==1.1.2

y el programa correspondiente en python, app/main.py

from flask import Flask app = Flask(__name__) @app.route('/hello') def hello_world(): return ("Hola Mundo, soy Python!") @app.route('/bye') def bye_world(): return ("Adios mundo cruel") if __name__ == '__main__': app.run(debug=True, host='0.0.0.0')

Con esto, ya podemos construir la imagen:

$ docker build . -t myapp:latest
Sending build context to Docker daemon  57.34kB
Step 1/6 : FROM python:latest
 ---> a3fe352c5377
Step 2/6 : COPY requirements.txt /requirements.txt
 ---> Using cache
 ---> 8da49270fac3
Step 3/6 : RUN pip install -r requirements.txt
 ---> Using cache
 ---> 9e7a504f594c
Step 4/6 : COPY app app
 ---> 75df8d3119b8
Step 5/6 : WORKDIR /app
 ---> Running in 3e0426929dc5
Removing intermediate container 3e0426929dc5
 ---> cebb15d43e7c
Step 6/6 : CMD ["python", "main.py"]
 ---> Running in 627eddde26e5
Removing intermediate container 627eddde26e5
 ---> 91e0a87586e6
Successfully built 91e0a87586e6
Successfully tagged mynginx:latest

Y por último podemos ejecutarlo en nuestra máquina local haciendo:

docker run -p80:5000 myapp
 * Serving Flask app "main" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 307-969-969

Mapeamos el puerto 80 al 5000 porque es en este puerto en el que escucha el servidor de pruebas de Flask. Como sugiere el mensaje de este servidor de prueba, este es un servidor solo para desarrollo. Para producción sería necesario utilizar un conector compatible con WSGI, como gunicorn o uwsgi.

Para verificar su funcionamiento, lo invocamos con un navegador web, o con curl

$ curl http://localhost/hello
Hola Mundo, soy Python!

El código, hasta este punto, se puede consultar en el siguiente repositorio de Github: https://github.com/uah-sol/sample-app/tree/feature/greetings

Ejecutando una imagen docker en Cloud Run

Cloud Run es un servicio que permite ejecutar contenedores Docker sin tener que montar un servidor con su correspondiente sistema operativo y software. Pertenece al tipo de servicio conocido como PaaS, Platform as a Service. PaaS es un modelo de computación en la nube que proporciona un entorno completo ejecutar y gestionar aplicaciones sin tener que preocuparse por la infraestructura subyacente

Para poder usar este servicio necesitamos subir la imagen a Google Cloud Registry, que es un registor de imágenes, similar al del dockerhub.

Para que docker reconozca al servicio de registro de google, tenemos que hacer:

gcloud auth configure-docker

También es importante habilitar el permiso de la máquina virtual en la que estamos trabajando para que pueda tener permiso de escritura en GCR.

Para subir una imagen a GCR tenemos que etiquetarla siguiendo un convenio:

gcr.io/<project-name>/<nombre-img>:tag

Por ejemplo

gcr.io/aso-git-290916/myapp

Para esto haremos:

docker tag opobla/myapp gcr.io/aso-git-290916/myapp
docker push gcr.io/aso-git-290916/myapp

Ahora ya podemos configurar un servicio de ClourRun para que ejecute la imagen.

En este apartado vamos a añadir un nuevo microservicio que denominaremos catalog. A través de él vamos a gestionar un pequeño catálogo de productos a través de los siguientes endpoints:

GET /product
GET /product/<:product_id>
POST /product
PUT /product/<:product_id>

Creamos una nueva carpeta en nuestro repositorio para el nuevo microservicio. Podemos reutilizar mucho código del microservicio de greetings que acabamos de hacer. Desde la raíz del repositorio haremos:

$ cp -r greetings catalog

Ahora la estructura de nuestro repo será:

$ tree
.
├── README.md
├── catalog
│   ├── Dockerfile
│   ├── app
│   │   └── main.py
│   └── requirements.txt
└── greetings
    ├── Dockerfile
    ├── app
    │   └── main.py
    └── requirements.txt

Modificamos el archivo catalog/app/main.py para incluir los nuevos endpoints requeridos.

Además, hemos añadido un nuevo módulo que permite generar datos de prueba (fake data). Esto es muy útil para hacer pruebas cuando no se tienen datos reales.

El código resultante se puede consultar en la rama feature/catalog-step-1

Llegados a este punto podemos construir la imagen y ejecutar un contenedor haciendo:

$ cd catalog
$ docker run  -v ./app:/app -p 80:5000 -e PORT=5000 catalog

Ahora se puede comprobar el sistema haciendo una llamada con cualquier cliente HTTP, por ejemplo curl:

curl http://localhost/products | jq .

La respuesta será un JSON similar a la siguiente:

[
  {
    "category": "tonight",
    "description": "Join front age room four board themselves.",
    "name": "anything",
    "price": 839.70863,
    "sku": "1cc35940-6b57-4418-9d50-5d2431b6320e"
  },
	...
]

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Guardar los productos en una BBDD

En este paso vamos a sustituir las respuestas falsas de prueba (fake) por datos procedentes de una BBDD real. Primero lo haremos en un entorno local y finalmente utilizaremos una instancia de Cloud SQL en Google Cloud.

Base de datos Postgres en un contedor docker

Usaremos Postgres como base de datos relacional SQL. Concretamente una imagen docker obtenida de dockerhub https://hub.docker.com/_/postgres

De las instrucciones obtenemos que necesitamos crear una serie de variables de entorno cuando se crea el contenedor:

  • POSTGRES_USER que indica el nombre del usuario que operará la base de datos.
  • POSTGRES_PASSWORD, que indica la contraseña del usuario anterior.
  • POSTGRES_DB, que indica el nombre de la BD que se quiere utilizar, y que se creará de no existir.

Crearemos una carpeta para los elementos relacionados con la base de datos, otra con la aplicación tal y como está en este momento, y un archivo docker-compose que nos facilite la labor de arrancar todos los contenedores necesarios. El aspecto de nuestro proyecto ahora será el siguiente:

.
├── .gitignore
├── README.md
├── catalog
│   ├── Dockerfile
│   ├── app
│   │   ├── main.py
│   │   └── model
│   │       └── fake_products.py
│   └── requirements.txt
└── greetings
    ├── Dockerfile
    ├── app
    │   └── main.py
    └── requirements.txt

El archivo docker-compose.yml será el siguiente por ahora:

services:

  redis:
    image: redis:latest
    networks: 
      - web_network
    expose: 
      - 6379

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: mysecretpassword
      POSTGRES_USER: myuser
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  greetings:
    build: ./greetings
    image: europe-west1-docker.pkg.dev/aso-git/webs/greetings:latest
    volumes:
      - ./greetings/app:/app
    networks: 
      - web_network
    environment:
      - NAME=oscar
      - PORT=8080
      - REDIS_LOCATION=redis
      - REDIS_PORT=6379
    ports:
      - 80:8080

  catalog:
    build: ./catalog
    image: europe-west1-docker.pkg.dev/aso-git/webs/catalog:latest
    volumes:
      - ./catalog/app:/app
    networks: 
      - web_network
    environment:
      - PORT=8080
      - REDIS_LOCATION=redis
      - REDIS_PORT=6379
    ports:
      - 80:8080

networks:
  web_network:
volumes:
  postgres_data:


Con esto ya podemos contruir y ejecutar la nueva imagen de Postgres haciendo

docker compose build db
docker compose up -d db

Para conectar a la BBDD necesitaremos un cliente compatible con el protocolo que utiliza Posgress. Una opción habitual es usar psql, que es una aplicación que proporciona el fabricante para este fin, y además viene en la propia imagen de postgres, así es que lo podemos ejecutar desde el mismo contenedor que acabamos de poner en marcha.

docker compose exec db psql -U myuser mydb

Podemos listar las distintas BBDD haciendo \l y comprobamos que mydb ya está presente.

Ahora vamos a crear la tabla para contener a nuestros productos:

\connect postgres; DROP DATABASE if exists mydb; CREATE DATABASE mydb WITH TEMPLATE template0 ENCODING = 'UTF8' LC_COLLATE = 'es_ES.utf8' LC_CTYPE = 'es_ES.utf8'; \connect mydb; CREATE EXTENSION "uuid-ossp"; CREATE TABLE productos ( category varchar(80), description text, name varchar(80), price decimal, sku uuid PRIMARY KEY DEFAULT uuid_generate_v4() ); INSERT INTO productos (category, description, name, price) VALUES ('watch', 'Super training watch GPS', 'GPS-1000', 200), ('watch', 'Super training watch GPS', 'GPS-2000', 300), ('food', 'Megaburger T1', 'Burger T1', 10), ('food', 'Megaburger T2', 'Burger T2', 12), ('food', 'Megaburger T3', 'Burger T3', 14)

También podemos poner este contenido en un archivo, por ejemplo dentro de una nueva carpeta database, llamarlo db_productos.sql y hacer:

cat database/db_productos.sql | docker compose exec -T db psql -U myuser mydb

Comprobamos que todo ha ido bien haciendo una consulta. Importante acordarse de seleccionar la base de datos db, que es donde hemos creado la tabla.

echo 'select * from productos;' | docker compose exec -T db psql -U myuser mydb

Conectamos python con Postgres

Para ello necesitamos elegir un conector adecuado: psycopg2 concretamente fijaremos la versión a igual o mayor que 2.8.6, para lo cual editamos requirements.txt y añadiremos:

...
psycopg2>=2.8

En un módulo aparte crearemos un par de funciones que nos ayudarán a conectar con la base de datos. Crearemos una carpeta db para que contenga un nuevo paquete python. Para eso añadiremos en el ella el archivo vacío __init__.py, haciendo, desde el directorio principal (el que tiene el archivo docker-compose.yml):

mkdir catalog/app/db
touch catalog/app/db/__init__.py

A continuación añadiremos el siguiente archivo database.py al paquete.

import os import psycopg2 from flask import g DB_USER = os.environ["DB_USER"] DB_PASSWORD = os.environ["DB_PASSWORD"] DB_NAME = os.environ["DB_NAME"] DB_HOST = os.environ["DB_HOST"] DB_PORT = os.environ["DB_PORT"] def get_db(): if 'db' not in g: g.db = psycopg2.connect( user=DB_USER, password=DB_PASSWORD, database=DB_NAME, host=DB_HOST, port=DB_PORT ) return g.db def close_db(e=None): db = g.pop('db', None) if db is not None: db.close()

Como se puede deducir del código, estas dos funciones nos ayudarán a abrir y cerrar respectivamente la conexión a la BBDD. Utilizaremos el paquete de flask g para colocar objeto en el área global de la aplicación: el objeto db que nos proporciona accesso a la BD en concreto.

Modificamos ahora catalog.py para que obtenga los productos desde la BD en lugar de devolver una lista falsa.

diff --git a/app/catalog.py b/app/catalog.py index fb84a42..5fe42b6 100644 --- a/app/catalog.py +++ b/app/catalog.py @@ -1,29 +1,28 @@ import sys import uuid +from db.database import get_db + + def get_products(): - fake_response = [{ - "sku": uuid.uuid4(), - "title": "Vanilla icecream", - "long_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum volutpat semper metus, ac blandit risus viverra eu. Nullam commodo posuere velit, efficitur fringilla turpis posuere a. Vestibulum tempus scelerisque nunc, a efficitur justo hendrerit a. Fusce eu porttitor diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam posuere nec risus nec aliquam. Nulla interdum arcu at sodales vestibulum. Suspendisse velit risus, aliquet vel tristique quis, suscipit sit amet nibh. Vivamus viverra eros odio, a aliquet sapien mollis vel. Duis scelerisque leo nulla, sit amet laoreet mi sodales vitae. Suspendisse vitae consectetur metus, vel imperdiet nulla. Fusce consectetur varius neque, sit amet ornare erat porta ac. Sed ligula lectus, sollicitudin ut pretium quis, maximus eu sapien. Curabitur vitae lacinia urna, sit amet iaculis tellus.", - "price_euro": 1.5 - },{ - "sku": uuid.uuid4(), - "title": "Cola icecream", - "long_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum volutpat semper metus, ac blandit risus viverra eu. Nullam commodo posuere velit, efficitur fringilla turpis posuere a. Vestibulum tempus scelerisque nunc, a efficitur justo hendrerit a. Fusce eu porttitor diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam posuere nec risus nec aliquam. Nulla interdum arcu at sodales vestibulum. Suspendisse velit risus, aliquet vel tristique quis, suscipit sit amet nibh. Vivamus viverra eros odio, a aliquet sapien mollis vel. Duis scelerisque leo nulla, sit amet laoreet mi sodales vitae. Suspendisse vitae consectetur metus, vel imperdiet nulla. Fusce consectetur varius neque, sit amet ornare erat porta ac. Sed ligula lectus, sollicitudin ut pretium quis, maximus eu sapien. Curabitur vitae lacinia urna, sit amet iaculis tellus.", - "price_euro": 1.5 - },{ - "sku": uuid.uuid4(), - "title": "Vanilla icecream", - "long_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum volutpat semper metus, ac blandit risus viverra eu. Nullam commodo posuere velit, efficitur fringilla turpis posuere a. Vestibulum tempus scelerisque nunc, a efficitur justo hendrerit a. Fusce eu porttitor diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam posuere nec risus nec aliquam. Nulla interdum arcu at sodales vestibulum. Suspendisse velit risus, aliquet vel tristique quis, suscipit sit amet nibh. Vivamus viverra eros odio, a aliquet sapien mollis vel. Duis scelerisque leo nulla, sit amet laoreet mi sodales vitae. Suspendisse vitae consectetur metus, vel imperdiet nulla. Fusce consectetur varius neque, sit amet ornare erat porta ac. Sed ligula lectus, sollicitudin ut pretium quis, maximus eu sapien. Curabitur vitae lacinia urna, sit amet iaculis tellus.", - "price_euro": 2.5 - },{ - "sku": uuid.uuid4(), - "title": "Lemon icecream", - "long_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum volutpat semper metus, ac blandit risus viverra eu. Nullam commodo posuere velit, efficitur fringilla turpis posuere a. Vestibulum tempus scelerisque nunc, a efficitur justo hendrerit a. Fusce eu porttitor diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam posuere nec risus nec aliquam. Nulla interdum arcu at sodales vestibulum. Suspendisse velit risus, aliquet vel tristique quis, suscipit sit amet nibh. Vivamus viverra eros odio, a aliquet sapien mollis vel. Duis scelerisque leo nulla, sit amet laoreet mi sodales vitae. Suspendisse vitae consectetur metus, vel imperdiet nulla. Fusce consectetur varius neque, sit amet ornare erat porta ac. Sed ligula lectus, sollicitudin ut pretium quis, maximus eu sapien. Curabitur vitae lacinia urna, sit amet iaculis tellus.", - "price_euro": 0.5 - }] - return fake_response + db = get_db() + + cursor = db.cursor() + postgreSQL_select_Query = "select sku, title, long_description, price from productos" + + cursor.execute(postgreSQL_select_Query) + products = cursor.fetchall() + + response = [] + for product in products: + response.append({ + "sku": product[0], + "title": product[1], + "long_description": product[2], + "price": product[3], + }) + return response + def create_product(sku, title, long_description, price_euro): - ''' Insertar todo esto en una bbdd ''' - print(f"Crear sku={sku} y title={title}", file=sys.stderr) + ''' Insertar todo esto en una bbdd ''' + print(f"Crear sku={sku} y title={title}", file=sys.stderr)
diff --git a/app/main.py b/app/main.py index c1def0f..870882b 100644 --- a/app/main.py +++ b/app/main.py @@ -3,37 +3,31 @@ from catalog import get_products, create_product app = Flask(__name__) + @app.route('/product', methods=['GET', 'POST']) def list_all_products(): - '''This view manages the CRUD of products''' - print("Hola mundo") - if request.method == 'GET': - response = get_products() - return jsonify(response) - - if request.method == 'POST': - data = request.get_json() - create_product( - data['sku'], - data['title'], - data['long_description'], - data['price_euro']) - return jsonify({ "status": "ok"}) + print("Hola mundo") + if request.method == 'GET': + response = get_products() + return jsonify(response) + + if request.method == 'POST': + data = request.get_json() + create_product(data['sku'], data['title'], data['long_description'], data['price_euro']) + return jsonify({"status": "ok"}) @app.route('/hello') def hello_world(): - message = "Hola Mundo, soy Python! Ahora con CloudBuild y hablando JSON" - response = { - "message": message, - "length": len(message) - } - return jsonify(response) + message = "Hola Mundo, soy Python! Ahora con CloudBuild y hablando JSON" + response = {"message": message, "length": len(message)} + return jsonify(response) + @app.route('/bye') def bye_world(): - return ("Adios mundo cruel") + return "Adios mundo cruel" + if __name__ == '__main__': app.run(debug=True, host='0.0.0.0') -

Usamos la siguiente orden curl para probar:

curl -X POST \
  -H "Content-Type: application/json" \
  -u usuario:contraseña \
  -d '{"category": "Electronics", "description": "A new gadget", "name": "Awesome Gadget", "price": 99.99}' \
  http://localhost:81/products

Operaciones CRUD de products sobre BBDD

Operaciones:

  • GET /product
  • POST /product

E.O: Si insertamos un producto haciendo una llamada CURL (o postman) el producto es visible en el listado general de GET /product

Introducción de CACHE en un entorno con docker-compose

  • Nuevo servicio en docker-compose
  • Pruebas con redis-cli
  • Explicación del uso de cache y de invalidación de cache
  • Nueva dependencia en requirements.txt
  • Endpoint GET /product/<sku>
  • Endpoint con cache

Montar y desplegar infraestructura completa gestionada en GC

  • Montar CloudSQL
  • Montar conector VPC
  • Montar MemoryStore
  • Pruebas de carga con Locust

Montar todo en una PaaS gestionada por Google

El servicio que hemos creado ahora depende de otros dos sistemas externos: una base de datos (Postgress) y de una base de datos en memoria (redis). En nuestro entorno de desarrollo hemos utilizado docker-compose para disponer de ambos. Podríamos replicar exactamente lo mismo en una máquina de Compute Engine (IaaS), pero en su lugar vamos vamos a emplear otra estrategia: utilizar servicios aprovisionados y gestionados por el proveedor (PaaS). Estos servicios gestionados serán una BBDD Postgres y una base de datos en memoria compatible con REDIS.

Base de datos como servicio gestionado.

El servicio gestionado de bases de datos relacionales en GCP es Cloud SQL:

  1. Creamos una instancia de BBDD
    • Elegimos Postgres
    • La denominamos app-database
    • Le ponemos una password, que guardamos convenientemente (xPo2G3Bt7ntrL8Or)
    • En opciones avanzadas verificamos que sea una máquina pequeña
  2. Tomamos nota del connection name, que nos hará falta luego. En mi caso es aso-git-290916:europe-west1:app-database. Usaremos esto para conectar con la BBDD.
  3. Creamos un usuario en nuestra BBDD, que será el usuario que usará la aplicación para conectar. Utilizamos la opción Users de la columna de la izquierda.
    • Username: dbuser
    • Password: supercomplexpasswd
  4. En la misma columna izquierda, creamos una BBDD con la opción Databases
    • Database name: catalog

También hay que habilitar el Cloud SQL Admin API

Si tienes gcloud correctamente configurado, también puedes crear esta máquina haciendo:

gcloud sql instances create myapp2db --database-version=POSTGRES_9_6 --tier=db-f1-micro --zone=europe-west1-b --root-password=password123

Conectar con la BD usando gcloud

Para conectar con la instancia que acabamos de crear podemos utilizar gcloud desde nuestra máquina de Google Compute Engine. Para ello debemos asegurarnos de que dicha máquina tiene permisos para interactuar con Cloud SQL.

También necesitaremos tener el cliente de postgres con el que realizar las operaciones oportunas. En nuestra máquina de compute engine, que tiene la distribución Debian 9 de Linux, podemos instalar ese cliente haciendo:

sudo apt-get install postgresql-client

Ahora ya podemos conectar haciendo:

gcloud sql connect app-database --user=postgres

De la misma forma que hicimos al principio, podemos ejecutar el código SQL para construir la estructura de nuestra BD, así como insertar algunos artículos de ejemplo, haciendo:

cat ddl/db.ddl.sql | gcloud sql connect app-database --user=postgres

Conectar con BD usando Cloud Sql Auth Proxy

https://cloud.google.com/sql/docs/mysql/quickstart-proxy-test

curl https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 --output cloud_sql_proxy
chmod +x cloud_sql_proxy

Para arrancar el proxy necesitamos conocer el nombre de conexión de nuestra instancia (INSTANCE_CONNECTION_NAME)

./cloud_sql_proxy -instances=aso-git-21-22-325916:europe-west1:app-database=tcp:5432

Con el proxy escuchando en el puerto de nuestra máquina local 5432, podemos hacer:

psql -h localhost -U postgres

Conectar nuestra app

Ahora vamos a modificar la aplicación para que use la BBDD gestionada que acabamos de crear. Para esto hay que tener en cuenta que cuando Cloud Run ejecute nuestra aplicación, a nuestro contenedor docker le injectará un socket de tipo UNIX que la pondrá en contacto con la BBDD (ver patrón sidecar en contenedores docker y SQLProxy). Con esto en mente inspeccionamos la parte de nuestro código que tiene que ver con la conexión a la BBDD, que está en app/db/database.py:

import psycopg2 from flask import g def get_db(): if 'db' not in g: g.db = psycopg2.connect( user="dbuser", password="secreto", host="db", port="5432", database="db" ) return g.db def close_db(e=None): db = g.pop('db', None) if db is not None: db.close()

Tarde o temprano tendríamos que corregir una cosa: es una mala idea usar constantes literales, como por ejemplo dbuser, pero mucho peor es poner una contraseña en texto claro. Pero en nuestro caso hay otro motivo adicional, que queremos que nuestra aplicación funcione en nuestro entorno de desarrollo, pero también cuando se ejecute en Cloud Run. Y claro, los valores de user, password, etc. serán diferentes dependiendo del entorno en el que nos encontremos. Este es un problema muy habitual, y una de las técnicas clásicas para resolverlo es utilizar variables de entorno del sistema operativo. Es decir, que el entorno de ejecución, ya sea Cloud Run, docker-compose o el que sea, inyectará unas variables de entorno que nosotros digamos, con los valores apropiados. Con esto en mente, vamos a utilizar las siguiente variables de entorno para cuando ejecutemos nuestro servicio en local:

  • DB_USERNAME con el nombre de usuario de la BBDD que creamos en local, dbuser
  • DB_PASSWORD con la contraseña para ese usuario secreto
  • DB_NAME con el nombre de la BBDD db
  • DB_HOST con el nombre del contenedor que tiene la BBDD db
  • DB_PORT con el puerto TCP en el que escucha el contenedor de la BBDD 5432

Por su parte, cuando se ejecute en Cloud Run, estos valores serán:

  • DB_USERNAME con el nombre de usuario de la BBDD que hemos creado antes, dbuser
  • DB_PASSWORD con la contraseña para ese usuario supercomplexpasswd
  • DB_NAME con el nombre de la BBDD catalog
  • CLOUD_SQL_CONNECTION_NAME con el que nos ha proporcionado Cloud SQL durante la creación de la BBDD aso-git-290916:europe-west1:app-database

Ahora tenemos que hacer llegar esas variables de entorno a nuestro contenedor docker. Para ello vamos a comenzar con docker-compose y luego veremos como hacerlo en Cloud Run.

Vamos a modificar el archivo docker-compose.yml de la siguiente forma:

@@ -24,6 +24,11 @@ services: environment: - REDIS_HOST=redis - REDIS_PORT=6379 + - DB_USERNAME=dbuser + - DB_PASSWORD=secreto + - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=db env_file: - .env.catalogo volumes:

Podemos verificar que esto funciona haciendo:

$ docker-compose 
$ docker-compose up -d
$ docker-compose exec catalogo bash
root@2c8cf0d0c6aa:/local/app# env | grep DB
DB_PASSWORD=secreto
DB_PORT=5432
DB_USERNAME=dbuser
DB_HOST=db
DB_NAME=db
root@2c8cf0d0c6aa:/local/app#

Lo siguiente será modificar nuestra aplicación para que obtenga los valores de esas variables de entorno a la hora de configurar la conexión a la BBDD:

def get_db(): db_user = os.environ.get('DB_USERNAME') db_password = os.environ.get('DB_PASSWORD') db_name = os.environ.get('DB_NAME') db_connection_name = os.environ.get('CLOUD_SQL_CONNECTION_NAME', None) if 'db' not in g and not db_connection_name: # If there is no db_connection_name we assume that we are connecting # using a TCP socket, and therefore we need a HOST and a PORT to # connect to. db_host = os.environ.get('DB_HOST') db_port = os.environ.get('DB_PORT') g.db = psycopg2.connect( user=db_user, password=db_password, host=db_host, port=db_port, database=db_name ) if 'db' not in g and db_connection_name: # If there is a db_connection_name then we assume that we are # connecting to a unix socket, whose name is google-specific. unix_socket = '/cloudsql/{}'.format(db_connection_name) g.db = psycopg2.connect( user=db_user, password=db_password, host=unix_socket, database=db_name ) return g.db

No hay que olvidar import os para poder disponer de la función os.environ.get.

Para definir estas variables en Cloud Run, tenemos que acceder a dicha funcionalidad en la consola de Google Cloud, editar nuestra aplicación y buscar la pestaña VARIABLES

Vamos a aprovechar a crear la conexión a la BBDD. A la derecha de la pestaña de VARIABLES encontraremos otra que dice CONNECTIONS. Allí encontraremos un desplegable para seleccionar la conexión a la base de datos que creamos anteriormente. Con todo esto ya podemos desplegar la nueva aplicación, aunque como este despliegue se hará con el código antiguo, aun no entrará nada de lo que hemos hecho en funcionamiento. De hecho aún no podemos desplegar el nuevo código porque lo hemos hecho dependiente del servicio de REDIS, y no hemos creado este servicio en Google Cloud. Podríamos configurar dicho servicio y desplegar todo junto, pero creo que es mejor ir haciendo cosas de forma incremental, así es que lo primero que haremos será modificar nuestra aplicación para que no sea dependiente de REDIS.

diff --git a/catalogo/app/main.py b/catalogo/app/main.py
index e66b3d5..353d75e 100644
--- a/catalogo/app/main.py
+++ b/catalogo/app/main.py
@@ -8,16 +8,22 @@ import redis
 app = Flask(__name__)
 CORS(app)
 
-redis_client = redis.Redis(
-    host=os.getenv('REDIS_HOST'),
-    port=os.getenv('REDIS_PORT'),
-    db=0, decode_responses=True
-)
+redis_host = os.environ.get('REDIS_HOST', None)
+redis_port = os.environ.get('REDIS_PORT', None)
+
+if redis_host and redis_port:
+    redis_client = redis.Redis(
+        host=os.getenv('REDIS_HOST'),
+        port=os.getenv('REDIS_PORT'),
+        db=0, decode_responses=True
+    )
+else:
+    redis_client = None
 
 
 @app.route('/product/<sku>', methods=['GET', ])
 def get_product_by_sku(sku):
-    product = redis_client.hgetall(sku)
+    product = redis_client.hgetall(sku) if redis_client else None
     if not product:
         product = get_product(sku)
         if not product:
@@ -25,7 +31,8 @@ def get_product_by_sku(sku):
                 "error": f"SKU {sku} not found!"
             }), 404)
         product['cache'] = 'miss'
-        redis_client.hmset(product['sku'], product)
+        if redis_client:
+            redis_client.hmset(product['sku'], product)
     else:
         pass
         product['cache'] = 'hit'

Así, si no declaramos las variables de entorno REDIS_HOST y REDIS_PORT el programa no hará uso de cache.

Con esto ya podemos desplegar nuestra aplicación. Como tenemos configurada la integración continua con el repositorio de github, basta con mergear la rama en la que he estado trabajando a la rama main. Para ello realizamos los commit necesarios, subimos la rama al repositorio, cambiamos a main, mergeamos feature/redis y subimos main al repositorio. Esta última acción provocará el nuevo despliegue:

$ git commit ....
$ git push origin
Enumerating objects: 19, done.
Counting objects: 100% (19/19), done.
Delta compression using up to 8 threads
Compressing objects: 100% (11/11), done.
Writing objects: 100% (12/12), 1.78 KiB | 1.78 MiB/s, done.
Total 12 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), completed with 3 local objects.
To github.com:opobla/myapp2.git
   f225392..d16fb71  feature/redis -> feature/redis

$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git merge feature/redis
Updating aa86b25..d16fb71
Fast-forward
 catalogo/app/catalog.py     | 12 ++++++++----
 catalogo/app/db/database.py | 36 ++++++++++++++++++++++++++++++------
 catalogo/app/main.py        | 82 ++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------
 docker-compose.yml          |  5 +++++
 4 files changed, 91 insertions(+), 44 deletions(-)
$ git push origin main

Si ahora vamos a Cloud Run comprobaremos que es está desplegando la nueva versión. Una vez desplegada, podemos intengar obtener el listado de productos, pero obtendremos el siguiente error:

Efectivamente, no hemos creado las tablas oportunas en la BBDD. Necesitamos conectar a la BD para ejecutar las operaciones DDL que hicimos cuando creamos la BD local. Hay varias formas de conectar con la BD. Una de ellas es usando la utilidad gcloud.

Esta utilidad debería estar configurada para operar con nuestro proyecto. Para verificar esto podemos hacer gcloud config configurations list.SSi necesitamos crear una nueva configuración podemos hacer gcloud init

Ahora, con gcloud configurado correctamente, podemos obtener un listado de todas las instancias de Cloud SQL haciendo:

$ gcloud sql instances list
NAME          DATABASE_VERSION  LOCATION        TIER              PRIMARY_ADDRESS  PRIVATE_ADDRESS  STATUS
app-database  POSTGRES_12       europe-west1-d  db-custom-1-3840  35.195.92.206    -                RUNNABLE
$ gcloud sql connect app-database --user=postgres
Allowlisting your IP for incoming connection for 5 minutes...done.
Connecting to database with SQL user [postgres].Password:
psql (13.1 (Debian 13.1-1.pgdg100+1), server 12.4)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
postgres=> \c catalog
Password:
psql (13.1 (Debian 13.1-1.pgdg100+1), server 12.4)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
You are now connected to database "catalog" as user "postgres".
\connect catalog; CREATE EXTENSION "uuid-ossp"; CREATE TABLE productos ( sku uuid PRIMARY KEY DEFAULT uuid_generate_v4(), price money, title varchar(80), long_description text ); INSERT INTO productos (price, title, long_description) VALUES (12.4, 'Ejemplo 1', 'Producto ejemplar 1'), (34.43, 'Ejemplo 2', 'Producto ejemplar 2');
CREATE ROLE catalogapp; GRANT catalogapp to dbuser; ALTER TABLE productos OWNER TO catalogapp;
catalog=> CREATE EXTENSION "uuid-ossp";
CREATE EXTENSION
catalog=> CREATE TABLE productos (
catalog(>    sku uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
catalog(>    price money,
catalog(>    title varchar(80),
catalog(>    long_description text
catalog(> );
CREATE TABLE
catalog=> INSERT INTO productos (price, title, long_description) VALUES
catalog->     (12.4, 'Ejemplo 1', 'Producto ejemplar 1'),
catalog->     (34.43, 'Ejemplo 2', 'Producto ejemplar 2');
INSERT 0 2
catalog=> CREATE ROLE catalogapp;
CREATE ROLE
catalog=> GRANT catalogapp to dbuser;
GRANT ROLE
catalog=> ALTER TABLE productos OWNER TO catalogapp;
ALTER TABLE
catalog=>

REDIS como servicio gestionado

  • Habilitar Serverless VPC Access API
  • Creamos un conector:
  • Crear instancia de MemoryStore
  • Creamos la conexión VPC en Cloud Run
  • Creamos la nuevas variable de entorno para apuntar a nuestro servidor REDIS

Vamos a hacer un despliegue parcial:


Para ello desmarcamos la opción que dice serve this revision inmediately y damos a desplegar.