###### tags: `ASO-GIT` `Flask` `Google Cloud Run` # Servicio web con flask en Cloud Run Creamos una imagen nueva a partir de `python:latest`. ```dockerfile= 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` ```python= 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. ## :one: Creamos un conjunto de endpoints para gestionar un catálogo 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: ```curl! 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: ```bash! $ cp -r greetings catalog ``` Ahora la estructura de nuestro repo será: ```bash! $ 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: ```bash! $ 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: ```jsonld! [ { "category": "tonight", "description": "Join front age room four board themselves.", "name": "anything", "price": 839.70863, "sku": "1cc35940-6b57-4418-9d50-5d2431b6320e" }, ... ] ``` ## :two: 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: ```yaml! 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 ```bash! 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: ```sql= \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. ```sql 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. ```python= 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. ```python= 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) ``` ```python= 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. ![](https://i.imgur.com/x9aqjxj.png) ## Base de datos como servicio gestionado. El servicio gestionado de bases de datos relacionales en GCP es Cloud SQL: ![](https://i.imgur.com/EQmtYk4.png) 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` :::warning También hay que habilitar el Cloud SQL Admin API ::: :::info 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. ![](https://i.imgur.com/yjEWcm8.png) 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`: ```python= 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: ```diff= @@ -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: ```python= 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` ![](https://i.imgur.com/CSO2VCE.png) 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 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: ![](https://i.imgur.com/fIGae2Z.png) 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". ``` ```sql= \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'); ``` ```sql= 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: ![](https://i.imgur.com/fB6AJSc.png) - Crear instancia de MemoryStore - Creamos la conexión VPC en Cloud Run ![](https://i.imgur.com/jfsm2a1.png) - Creamos la nuevas variable de entorno para apuntar a nuestro servidor REDIS ![](https://i.imgur.com/auvkejW.png) Vamos a hacer un despliegue parcial: ![](https://i.imgur.com/lqUyN95.png) Para ello desmarcamos la opción que dice _serve this revision inmediately_ y damos a desplegar.