Cómo implementar automáticamente los cambios de GitHub en Cloud Run con Cloud Build

1. Introducción

Descripción general

En este codelab, configurarás Cloud Run para compilar y, luego, implementar automáticamente versiones nuevas de tu aplicación cada vez que envíes los cambios del código fuente a un repositorio de GitHub.

Esta aplicación de demostración guarda los datos del usuario en Firestore. Sin embargo, solo se guarda correctamente una cantidad parcial de los datos. Configurarás las 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 estará 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 implementaciones continuas.
  • Implementa automáticamente tu aplicación en Cloud Run.
  • Aprende a usar HTMX y TailwindCSS.

2. Configuración y requisitos

Requisitos previos

Activar Cloud Shell

  1. En la consola de Cloud, haz clic en Activar Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Si es la primera vez que inicias Cloud Shell, verás una pantalla intermedia en la que se describirá qué es. Si viste una pantalla intermedia, haz clic en Continuar.

d95252b003979716.png

El aprovisionamiento y la conexión a Cloud Shell solo tomará unos minutos.

7833d5e1c5d18f54.png

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 permite mejorar 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 ya te autenticaste y que el proyecto ya se configuró con el ID de tu proyecto.

  1. 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`
  1. En Cloud Shell, ejecuta el siguiente comando para confirmar que el comando gcloud conoce tu proyecto:
gcloud config list project

Resultado del comando

[core]
project = <PROJECT_ID>

De lo contrario, puede configurarlo con este comando:

gcloud config set project <PROJECT_ID>

Resultado del comando

Updated property [core/project].

3. Habilita las APIs y establece las variables de entorno

Habilita las APIs

Este codelab requiere el uso de las siguientes APIs. Puedes habilitar esas APIs ejecutando 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 Vertex AI Gemini. Esta cuenta de servicio también tendrá permisos para leer y escribir en Firestore, y leer secretos de Secret Manager.

Primero, crea la cuenta de servicio ejecutando este comando:

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

  1. En el Firebase console, haz clic en Agregar proyecto.
  2. Ingresa <YOUR_PROJECT_ID> para agregar Firebase a uno de tus proyectos de Google Cloud existentes.
  3. Si se te solicita, revisa y acepta las Condiciones de Firebase.
  4. Haz clic en Continuar.
  5. Haz clic en Confirmar plan para confirmar el plan de facturación de Firebase.
  6. Es opcional habilitar Google Analytics para este codelab.
  7. Haz clic en Agregar Firebase:
  8. Cuando se cree el proyecto, haz clic en Continuar.
  9. En el menú Compilar, haz clic en Base de datos de Firestore.
  10. Haz clic en Crear base de datos.
  11. Elige tu región en el menú desplegable Ubicación y, luego, haz clic en Siguiente.
  12. Usa el Iniciar en modo de producción predeterminado y, luego, haz clic en Crear.

6. Escribe la aplicación

Primero, crea un directorio para el código fuente y cd en ese directorio.

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>`;

Crea un archivo input.css para tailwindCSS.

@tailwind base;
@tailwind components;
@tailwind utilities;

Crea el archivo tailwind.config.js para tailwindCSS.

/** @type {import('tailwindcss').Config} */
module.exports = {
    content: ["./**/*.{html,js}"],
    theme: {
        extend: {}
    },
    plugins: []
};

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

Dentro de 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 forma local para confirmar que hay un error en la aplicación cuando el usuario intenta guardar datos.

Primero, 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 en Cloud Shell) o puedes suplantar la cuenta de usuario creada anteriormente.

Usa ADC cuando se ejecuta de forma local

Si ejecutas en Cloud Shell, ya lo haces en una máquina virtual de Google Compute Engine. Las credenciales asociadas a esta máquina virtual (como se muestra cuando se ejecuta gcloud auth list) se usarán automáticamente con las credenciales predeterminadas de la aplicación (ADC), por lo que no es necesario usar el comando gcloud auth application-default login. Sin embargo, tu identidad aún necesitará el rol de usuario de Datastore. Puedes omitir la sección Ejecuta la app de forma local.

Sin embargo, si ejecutas 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 acceder con tus credenciales (siempre que tengas el rol de usuario de Datastore) o puedes acceder suplantando la cuenta de servicio que se usa en este codelab.

Opción 1: Usa tus credenciales para ADC

Si deseas usar tus credenciales, primero puedes ejecutar gcloud auth list para verificar cómo te autenticas en gcloud. A continuación, es posible que debas otorgarle 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 otorgarle 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: Suplanta una cuenta de servicio para ADC

Si deseas usar la cuenta de servicio creada en este codelab, tu cuenta de usuario deberá tener el rol de creador de tokens de cuentas de servicio. Puedes obtener este rol ejecutando el siguiente comando:

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member user:$USER \
  --role=roles/iam.serviceAccountTokenCreator

A continuación, 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 sitio web abriendo el botón Vista previa en la Web y seleccionando Vista previa en el puerto 8080.

Vista previa en la Web: botón de vista previa en el puerto 8080

Ingresa texto para los campos de entrada de nombre y ciudad, y haz clic en Guardar. Luego, actualiza la página. Notarás que el campo de ciudad no se conservó. Corregirás este error en la siguiente sección.

Detén la ejecución de la app de Express de forma 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 de rama predeterminado.

git init
git branch -M main

Confirma la base de código actual que contiene el error. Corregirás el error después de configurar 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 para ti o público. En este codelab, se recomienda que le asignes el nombre cloud-run-auto-deploy-codelab a tu repositorio. Para crear un repositorio vacío, dejarás sin marcar todos los parámetros de configuración predeterminados o los establecerás en ninguno de modo que no haya contenido en el repositorio de forma predeterminada cuando se cree, p. ej.,

Configuración predeterminada de GitHub

Si completaste este paso correctamente, verás las siguientes instrucciones en la página del repositorio vacío:

Instrucciones para un repo de GitHub vacío

Para seguir las instrucciones de 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>

luego, envía la rama principal al repositorio superior.

git push -u origin main

9. Configura la implementación continua

Ahora que tienes código en 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 Implementar continuamente a partir de un repositorio.
  • Haz clic en CONFIGURAR CLOUD BUILD.
  • En Repositorio de código fuente, haz lo siguiente:
    • Selecciona GitHub como proveedor de 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, haz lo siguiente:
    • Deja 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 /.
  • Haz clic en Guardar.
  • En Autenticación, haz lo siguiente:
    • Haz clic en Permitir invocaciones sin autenticar.
  • En Contenedores, volúmenes, herramientas de redes y seguridad, haz lo siguiente:
    • En la pestaña Seguridad, selecciona la cuenta de servicio que creaste en un paso anterior, p. ej., Cloud Run access to Firestore.
  • Haz clic en CREAR.

Esto implementará el servicio de Cloud Run que contiene el error que corregirás en la siguiente sección.

10. Corrige 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
    });

a

//fixed town bug
    await doc.set({
        name: name,
        town: town
    });

Ejecuta el siguiente comando para verificar la corrección:

npm run start

y abre tu navegador web. Vuelve a guardar los datos de la ciudad y actualiza. Verás que los datos de la ciudad ingresados recientemente se conservaron correctamente en la actualización.

Ahora que verificaste la corrección, puedes implementarla. Primero, confirma la corrección.

git add .
git commit -m "fixed town bug"

y, luego, envíala al repositorio superior en GitHub.

git push origin main

Cloud Build implementará automáticamente tus cambios. Puedes ir a la consola de Cloud para tu servicio de Cloud Run y supervisar los cambios de 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 ahora entrega 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 ingresados recientemente se conserven 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 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 de Cloud Run en https://console.cloud.google.com/run y borra el servicio de Cloud Run que creaste en este codelab, p.ej., borra el servicio cloud-run-auto-deploy-codelab.

Si eliges borrar todo el proyecto, 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. Puedes ver la lista de todos los proyectos disponibles ejecutando gcloud projects list.