1. Introducción
Descripción general
En este codelab, configurarás Cloud Run para compilar e implementar automáticamente versiones nuevas de tu aplicación cada vez que envíes cambios de código fuente a un repositorio de GitHub.
Esta aplicación de demostración guarda datos del usuario en Firestore, pero solo se guarda correctamente una parte parcial de los datos. Configurarás implementaciones continuas de modo que, cuando envíes una corrección de errores a tu repositorio de GitHub, verás automáticamente que la corrección está disponible en una revisión nueva.
Qué aprenderás
- Escribe una aplicación web de Express con el editor de Cloud Shell
- Conecta tu cuenta de GitHub a Google Cloud para obtener implementaciones continuas
- Implementa automáticamente tu aplicación en Cloud Run
- Aprende a usar HTMX y TailwindCSS
2. Configuración y requisitos
Requisitos previos
- Tienes una cuenta de GitHub y estás familiarizado con la creación y el envío de código a los repositorios.
- Accediste a la consola de Cloud.
- Ya implementaste un servicio de Cloud Run. Por ejemplo, puedes seguir la guía de inicio rápido para implementar un servicio web desde el código fuente para comenzar.
Activar Cloud Shell
- En la consola de Cloud, haz clic en Activar Cloud Shell.
Si es la primera vez que inicias Cloud Shell, verás una pantalla intermedia que describe en qué consiste. Si apareció una pantalla intermedia, haz clic en Continuar.
El aprovisionamiento y la conexión a Cloud Shell solo tomará unos minutos.
Esta máquina virtual está cargada con todas las herramientas de desarrollo necesarias. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que mejora considerablemente el rendimiento de la red y la autenticación. Gran parte de tu trabajo en este codelab, si no todo, se puede hacer con un navegador.
Una vez que te conectes a Cloud Shell, deberías ver que estás autenticado y que el proyecto está configurado con tu ID del proyecto.
- En Cloud Shell, ejecuta el siguiente comando para confirmar que tienes la autenticación:
gcloud auth list
Resultado del comando
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com> To set the active account, run: $ gcloud config set account `ACCOUNT`
- Ejecuta el siguiente comando en Cloud Shell para confirmar que el comando de gcloud conoce tu proyecto:
gcloud config list project
Resultado del comando
[core] project = <PROJECT_ID>
De lo contrario, puedes configurarlo con el siguiente comando:
gcloud config set project <PROJECT_ID>
Resultado del comando
Updated property [core/project].
3. Habilita las APIs y configura las variables de entorno
Habilita las APIs
Este codelab requiere el uso de las siguientes APIs. Para habilitar esas APIs, ejecuta el siguiente comando:
gcloud services enable run.googleapis.com \ cloudbuild.googleapis.com \ firestore.googleapis.com \ iamcredentials.googleapis.com
Configura variables de entorno
Puedes establecer variables de entorno que se usarán en este codelab.
REGION=<YOUR-REGION> PROJECT_ID=<YOUR-PROJECT-ID> PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') SERVICE_ACCOUNT="firestore-accessor" SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
4. Crea una cuenta de servicio
Cloud Run usará esta cuenta de servicio para llamar a la API de Gemini en Vertex AI. Esta cuenta de servicio también tendrá permisos de lectura y escritura en Firestore, y de lectura de secretos desde Secret Manager.
Primero, ejecuta este comando para crear la cuenta de servicio:
gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run access to Firestore"
Ahora, otorga a la cuenta de servicio acceso de lectura y escritura a Firestore.
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/datastore.user
5. Crea y configura un proyecto de Firebase
- En Firebase console, haz clic en Agregar proyecto.
- Ingresa <YOUR_PROJECT_ID> para agregar Firebase a uno de tus proyectos de Google Cloud existentes
- Si se te solicita, revisa y acepta las condiciones de Firebase.
- Haz clic en Continuar.
- Haz clic en Confirmar plan para confirmar el plan de facturación de Firebase.
- Habilitar Google Analytics para este codelab es opcional.
- Haz clic en Agregar Firebase:
- Cuando se haya creado el proyecto, haz clic en Continuar.
- En el menú Compilar, haz clic en Base de datos de Firestore.
- Haz clic en Crear base de datos.
- Elige tu región en el menú desplegable Ubicación y haz clic en Siguiente.
- Usa la opción predeterminada Comenzar en modo de producción y, luego, haz clic en Crear.
6. Escribe la aplicación
Primero, crea un directorio para el código fuente y desplázate a ese directorio con el comando cd.
mkdir cloud-run-github-cd-demo && cd $_
Luego, crea un archivo package.json
con el siguiente contenido:
{ "name": "cloud-run-github-cd-demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "node app.js", "nodemon": "nodemon app.js", "tailwind-dev": "npx tailwindcss -i ./input.css -o ./public/output.css --watch", "tailwind": "npx tailwindcss -i ./input.css -o ./public/output.css", "dev": "npm run tailwind && npm run nodemon" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@google-cloud/firestore": "^7.3.1", "axios": "^1.6.7", "express": "^4.18.2", "htmx.org": "^1.9.10" }, "devDependencies": { "nodemon": "^3.1.0", "tailwindcss": "^3.4.1" } }
Primero, crea un archivo fuente app.js
con el siguiente contenido. Este archivo contiene el punto de entrada del servicio y la lógica principal de la app.
const express = require("express"); const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); const path = require("path"); const { get } = require("axios"); const { Firestore } = require("@google-cloud/firestore"); const firestoreDb = new Firestore(); const fs = require("fs"); const util = require("util"); const { spinnerSvg } = require("./spinnerSvg.js"); const service = process.env.K_SERVICE; const revision = process.env.K_REVISION; app.use(express.static("public")); app.get("/edit", async (req, res) => { res.send(`<form hx-post="/update" hx-target="this" hx-swap="outerHTML"> <div> <p> <label>Name</label> <input class="border-2" type="text" name="name" value="Cloud"> </p><p> <label>Town</label> <input class="border-2" type="text" name="town" value="Nibelheim"> </p> </div> <div class="flex items-center mr-[10px] mt-[10px]"> <button class="btn bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium mr-[10px]">Submit</button> <button class="btn bg-gray-200 text-gray-800 px-4 py-2 rounded-lg text-center text-sm font-medium mr-[10px]" hx-get="cancel">Cancel</button> ${spinnerSvg} </div> </form>`); }); app.post("/update", async function (req, res) { let name = req.body.name; let town = req.body.town; const doc = firestoreDb.doc(`demo/${name}`); //TODO: fix this bug await doc.set({ name: name /* town: town */ }); res.send(`<div hx-target="this" hx-swap="outerHTML" hx-indicator="spinner"> <p> <div><label>Name</label>: ${name}</div> </p><p> <div><label>Town</label>: ${town}</div> </p> <button hx-get="/edit" class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]" > Click to update </button> </div>`); }); app.get("/cancel", (req, res) => { res.send(`<div hx-target="this" hx-swap="outerHTML"> <p> <div><label>Name</label>: Cloud</div> </p><p> <div><label>Town</label>: Nibelheim</div> </p> <div> <button hx-get="/edit" class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]" > Click to update </button> </div> </div>`); }); const port = parseInt(process.env.PORT) || 8080; app.listen(port, async () => { console.log(`booth demo: listening on port ${port}`); //serviceMetadata = helper(); }); app.get("/helper", async (req, res) => { let region = ""; let projectId = ""; let div = ""; try { // Fetch the token to make a GCF to GCF call const response1 = await get( "http://metadata.google.internal/computeMetadata/v1/project/project-id", { headers: { "Metadata-Flavor": "Google" } } ); // Fetch the token to make a GCF to GCF call const response2 = await get( "http://metadata.google.internal/computeMetadata/v1/instance/region", { headers: { "Metadata-Flavor": "Google" } } ); projectId = response1.data; let regionFull = response2.data; const index = regionFull.lastIndexOf("/"); region = regionFull.substring(index + 1); div = ` <div> This created the revision <code>${revision}</code> of the Cloud Run service <code>${service}</code> in <code>${region}</code> for project <code>${projectId}</code>. </div>`; } catch (ex) { // running locally div = `<div> This is running locally.</div>`; } res.send(div); });
Crea un archivo llamado spinnerSvg.js
.
module.exports.spinnerSvg = `<svg id="spinner" alt="Loading..." class="htmx-indicator animate-spin -ml-1 mr-3 h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" > <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" ></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" ></path> </svg>`;
Cómo crear un archivo input.css
para tailwindCSS
@tailwind base; @tailwind components; @tailwind utilities;
Y crea el archivo tailwind.config.js
para tailwindCSS
/** @type {import('tailwindcss').Config} */ module.exports = { content: ["./**/*.{html,js}"], theme: { extend: {} }, plugins: [] };
Y crea un archivo .gitignore
.
node_modules/ npm-debug.log coverage/ package-lock.json .DS_Store
Ahora, crea un directorio public
nuevo.
mkdir public cd public
En ese directorio público, crea el archivo index.html
para el frontend, que usará htmx.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous" ></script> <link href="./output.css" rel="stylesheet" /> <title>Demo 1</title> </head> <body class="font-sans bg-body-image bg-cover bg-center leading-relaxed" > <div class="container max-w-[700px] mt-[50px] ml-auto mr-auto"> <div class="hero flex items-center"> <div class="message text-base text-center mb-[24px]"> <h1 class="text-2xl font-bold mb-[10px]"> It's running! </h1> <div class="congrats text-base font-normal"> Congratulations, you successfully deployed your service to Cloud Run. </div> </div> </div> <div class="details mb-[20px]"> <p> <div hx-trigger="load" hx-get="/helper" hx-swap="innerHTML" hx-target="this">Hello</div> </p> </div> <p class="callout text-sm text-blue-700 font-bold pt-4 pr-6 pb-4 pl-10 leading-tight" > You can deploy any container to Cloud Run that listens for HTTP requests on the port defined by the <code>PORT</code> environment variable. Cloud Run will scale automatically based on requests and you never have to worry about infrastructure. </p> <h1 class="text-2xl font-bold mt-[40px] mb-[20px]"> Persistent Storage Example using Firestore </h1> <div hx-target="this" hx-swap="outerHTML"> <p> <div><label>Name</label>: Cloud</div> </p><p> <div><label>Town</label>: Nibelheim</div> </p> <div> <button hx-get="/edit" class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]" > Click to update </button> </div> </div> <h1 class="text-2xl font-bold mt-[40px] mb-[20px]"> What's next </h1> <p class="next text-base mt-4 mb-[20px]"> You can build this demo yourself! </p> <p class="cta"> <button class="bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium" > VIEW CODELAB </button> </p> </div> </body> </html>
7. Ejecución de la aplicación de manera local
En esta sección, ejecutarás la aplicación de manera local para confirmar que hay un error en ella cuando el usuario intente guardar datos.
En primer lugar, deberás tener el rol de usuario de Datastore para acceder a Firestore (si usas tu identidad para la autenticación, p.ej., si ejecutas Cloud Shell) o puedes usar la identidad de la cuenta de usuario creada anteriormente.
Usa ADC en la ejecución local
Si estás ejecutando en Cloud Shell, ya estás ejecutando en una máquina virtual de Google Compute Engine. Las credenciales asociadas con esta máquina virtual (como se muestra cuando se ejecuta gcloud auth list
) se utilizarán automáticamente en Credenciales predeterminadas de la aplicación (ADC), por lo que no es necesario utilizar el comando gcloud auth application-default login
. Sin embargo, tu identidad aún necesitará el rol de usuario de Datastore. Puedes pasar a la sección Cómo ejecutar la app de manera local.
Sin embargo, si estás ejecutando en tu terminal local (es decir, no en Cloud Shell), deberás usar las credenciales predeterminadas de la aplicación para autenticarte en las APIs de Google. Puedes 1) acceder con tus credenciales (siempre que tengas el rol Usuario de Datastore) o 2) acceder usando la identidad de la cuenta de servicio que se usa en este codelab.
Opción 1) Usa tus credenciales para ADC
Si quieres usar tus credenciales, primero puedes ejecutar gcloud auth list
para verificar cómo te autenticas en gcloud. A continuación, es posible que debas otorgar a tu identidad el rol de usuario de Vertex AI. Si tu identidad tiene el rol de propietario, ya tienes este rol de usuario de Datastore. De lo contrario, puedes ejecutar este comando para otorgar a tu identidad el rol de usuario de Vertex AI y el rol de usuario de Datastore.
USER=<YOUR_PRINCIPAL_EMAIL> gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/datastore.user
Luego, ejecuta el siguiente comando:
gcloud auth application-default login
Opción 2: Suplantación de identidad de una cuenta de servicio para ADC
Si deseas usar la cuenta de servicio que se creó en este codelab, tu cuenta de usuario deberá tener el rol de creador de tokens de cuentas de servicio. Para obtener este rol, ejecuta el siguiente comando:
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/iam.serviceAccountTokenCreator
Luego, ejecutarás el siguiente comando para usar ADC con la cuenta de servicio
gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS
Ejecuta la app de forma local
A continuación, asegúrate de estar en el directorio raíz cloud-run-github-cd-demo
de tu codelab.
cd .. && pwd
Ahora, instalarás las dependencias.
npm install
Por último, puedes iniciar la app ejecutando la siguiente secuencia de comandos. Esta secuencia de comandos también generará el archivo output.css desde tailwindCSS.
npm run dev
Ahora abre tu navegador web en http://localhost:8080. Si estás en Cloud Shell, puedes abrir el botón Vista previa en la Web y seleccionar Puerto de vista previa 8080 para abrir el sitio web.
Escribe el texto en los campos de entrada Nombre y Ciudad, y presiona Guardar. Luego, actualiza la página. Notarás que el campo de ciudad no persistió. Solucionarás este error en la siguiente sección.
Detén la ejecución de la app Express de manera local (p.ej., Ctrl^c en MacOS).
8. Crea un repositorio de GitHub
En tu directorio local, crea un repositorio nuevo con main como nombre predeterminado de rama.
git init git branch -M main
Confirma la base de código actual que contiene el error. Corregirás el error después de que se configure la implementación continua.
git add . git commit -m "first commit for express application"
Ve a GitHub y crea un repositorio vacío que sea privado o público. En este codelab, se recomienda asignarle el nombre cloud-run-auto-deploy-codelab
a tu repositorio. Para crear un repositorio vacío, dejarás todos los parámetros de configuración predeterminados sin marcar o establecidos en ninguno para que no haya contenido en el repositorio de forma predeterminada cuando lo crees, p. ej.,
Si completaste este paso correctamente, verás las siguientes instrucciones en la página del repositorio vacío:
Para seguir las instrucciones para enviar un repositorio existente desde la línea de comandos, ejecuta los siguientes comandos:
Primero, agrega el repositorio remoto ejecutando
git remote add origin <YOUR-REPO-URL-PER-GITHUB-INSTRUCTIONS>
y, luego, enviar la rama principal al repositorio upstream.
git push -u origin main
9. Configura la implementación continua
Ahora que tienes código en un GitHub, puedes configurar la implementación continua. Ve a la consola de Cloud para Cloud Run.
- Haz clic en Crear un servicio.
- Haz clic en Continuously deploy from a repository.
- Haz clic en CONFIGURAR CLOUD Build.
- En Repositorio de código fuente
- Selecciona GitHub como el proveedor del repositorio
- Haz clic en Administrar repositorios conectados para configurar el acceso de Cloud Build al repositorio.
- Selecciona tu repositorio y haz clic en Siguiente.
- En Configuración de compilación,
- Dejar la rama como ^main$
- En Tipo de compilación, selecciona Go, Node.js, Python, Java, .NET Core, Ruby o PHP a través de los paquetes de compilación de Google Cloud.
- Deja el directorio de contexto de compilación como
/
- Haga clic en Guardar.
- En Autenticación
- Haz clic en Permitir invocaciones no autenticadas.
- En Contenedores, Volúmenes, Herramientas de redes y Seguridad
- En la pestaña Seguridad, selecciona la cuenta de servicio que creaste en un paso anterior, p.ej.,
Cloud Run access to Firestore
- En la pestaña Seguridad, selecciona la cuenta de servicio que creaste en un paso anterior, p.ej.,
- Haz clic en CREAR.
Esto implementará el servicio de Cloud Run que contiene el error que corregirás en la siguiente sección.
10. Corregir el error
Corrige el error en el código
En el editor de Cloud Shell, abre el archivo app.js
y ve al comentario que dice //TODO: fix this bug
.
cambia la siguiente línea de
//TODO: fix this bug await doc.set({ name: name });
para
//fixed town bug await doc.set({ name: name, town: town });
Ejecuta este comando para verificar la solución
npm run start
y abre tu navegador web. Vuelve a guardar datos para la ciudad y actualiza la página. Verás que los datos de la ciudad recién ingresados persisten correctamente al actualizar.
Ahora que verificaste la solución, tienes todo listo para implementarla. Primero, confirma la corrección.
git add . git commit -m "fixed town bug"
y, luego, enviarla al repositorio upstream en GitHub.
git push origin main
Cloud Build implementará automáticamente los cambios. Puedes ir a la consola de Cloud para que tu servicio de Cloud Run supervise los cambios en la implementación.
Verifica la corrección en producción
Una vez que la consola de Cloud para tu servicio de Cloud Run muestre que una segunda revisión está entregando el 100% del tráfico, p.ej., https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions, puedes abrir la URL del servicio de Cloud Run en tu navegador y verificar que los datos de la ciudad recién ingresados se mantengan después de actualizar la página.
11. ¡Felicitaciones!
¡Felicitaciones por completar el codelab!
Te recomendamos que revises la documentación de Cloud Run y la implementación continua desde git.
Temas abordados
- Escribe una aplicación web de Express con el editor de Cloud Shell
- Conecta tu cuenta de GitHub a Google Cloud para obtener implementaciones continuas
- Implementa automáticamente tu aplicación en Cloud Run
- Aprende a usar HTMX y TailwindCSS
12. Limpia
Para evitar cargos involuntarios (por ejemplo, si los servicios de Cloud Run se invocan de forma involuntaria más veces que tu asignación mensual de invocación de Cloud Run en el nivel gratuito), puedes borrar Cloud Run o el proyecto que creaste en el paso 2.
Para borrar el servicio de Cloud Run, ve a la consola de Cloud Run en https://console.cloud.google.com/run y borra el servicio que creaste en este codelab; p.ej., borra el servicio cloud-run-auto-deploy-codelab
.
Si decides borrar el proyecto completo, puedes ir a https://console.cloud.google.com/cloud-resource-manager, seleccionar el proyecto que creaste en el paso 2 y elegir Borrar. Si borras el proyecto, deberás cambiar los proyectos en tu SDK de Cloud. Para ver la lista de todos los proyectos disponibles, ejecuta gcloud projects list
.