Eseguire automaticamente il deployment delle modifiche da GitHub a Cloud Run utilizzando Cloud Build

1. Introduzione

Panoramica

In questo codelab, configurerai Cloud Run per creare ed eseguire il deployment automaticamente di nuove versioni della tua applicazione ogni volta che esegui il push delle modifiche al codice sorgente in un repository GitHub.

Questa applicazione demo salva i dati utente in Firestore, ma solo una parte dei dati viene salvata correttamente. Configurerai i deployment continui in modo che, quando esegui il push di una correzione di bug nel repository GitHub, la correzione diventi automaticamente disponibile in una nuova revisione.

Cosa imparerai a fare

  • Scrivere un'applicazione web Express con l'editor di Cloud Shell
  • Collega il tuo account GitHub a Google Cloud per i deployment continui
  • Esegui automaticamente il deployment dell'applicazione in Cloud Run
  • Scopri come utilizzare HTMX e TailwindCSS

2. Configurazione e requisiti

Prerequisiti

Attiva Cloud Shell

  1. Nella console Cloud, fai clic su Attiva Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Se è la prima volta che avvii Cloud Shell, viene visualizzata una schermata intermedia che ne descrive le funzionalità. Se è stata visualizzata una schermata intermedia, fai clic su Continua.

d95252b003979716.png

Bastano pochi istanti per eseguire il provisioning e connettersi a Cloud Shell.

7833d5e1c5d18f54.png

Questa macchina virtuale è caricata con tutti gli strumenti di sviluppo necessari. Offre una home directory permanente da 5 GB e viene eseguita in Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. Gran parte del lavoro per questo codelab, se non tutto, può essere svolto con un browser.

Una volta eseguita la connessione a Cloud Shell, dovresti vedere che il tuo account è autenticato e il progetto è impostato sul tuo ID progetto.

  1. Esegui questo comando in Cloud Shell per verificare che l'account sia autenticato:
gcloud auth list

Output comando

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Esegui questo comando in Cloud Shell per verificare che il comando gcloud conosca il tuo progetto:
gcloud config list project

Output comando

[core]
project = <PROJECT_ID>

In caso contrario, puoi impostarlo con questo comando:

gcloud config set project <PROJECT_ID>

Output comando

Updated property [core/project].

3. Abilita le API e imposta le variabili di ambiente

Abilita API

Questo codelab richiede l'utilizzo delle seguenti API. Puoi abilitare queste API eseguendo il seguente comando:

gcloud services enable run.googleapis.com \
    cloudbuild.googleapis.com \
    firestore.googleapis.com \
    iamcredentials.googleapis.com

Configura le variabili di ambiente

Puoi impostare le variabili di ambiente che verranno utilizzate in questo 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 un account di servizio

Questo service account verrà utilizzato da Cloud Run per chiamare l'API Gemini di Vertex AI. Questo service account avrà anche le autorizzazioni per leggere e scrivere in Firestore e leggere i secret da Secret Manager.

Innanzitutto, crea il service account eseguendo questo comando:

gcloud iam service-accounts create $SERVICE_ACCOUNT \
  --display-name="Cloud Run access to Firestore"

Ora concedi al service account l'accesso in lettura e scrittura a Firestore.

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
  --role=roles/datastore.user

5. Crea e configura un progetto Firebase

  1. Nella console Firebase, fai clic su Aggiungi progetto.
  2. Inserisci <YOUR_PROJECT_ID> per aggiungere Firebase a uno dei tuoi progetti Google Cloud esistenti
  3. Se richiesto, leggi e accetta i Termini di Firebase.
  4. Fai clic su Continua.
  5. Fai clic su Conferma piano per confermare il piano di fatturazione Firebase.
  6. L'abilitazione di Google Analytics per questo codelab è facoltativa.
  7. Fai clic su Aggiungi Firebase.
  8. Una volta creato il progetto, fai clic su Continua.
  9. Nel menu Build, fai clic su Database Firestore.
  10. Fai clic su Crea database.
  11. Scegli la tua regione dal menu a discesa Località, poi fai clic su Avanti.
  12. Utilizza l'opzione predefinita Avvia in modalità di produzione, quindi fai clic su Crea.

6. Scrivi l'applicazione

Innanzitutto, crea una directory per il codice sorgente e accedi tramite cd.

mkdir cloud-run-github-cd-demo && cd $_

Quindi, crea un file package.json con il seguente contenuto:

{
  "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"
  }
}

Innanzitutto, crea un file di origine app.js con il seguente contenuto. Questo file contiene il punto di ingresso del servizio e la logica principale dell'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 file denominato 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>`;

Creare un file input.css per tailwindCSS

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

e crea il file tailwind.config.js per tailwindCSS

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

e crea un file .gitignore.

node_modules/

npm-debug.log
coverage/

package-lock.json

.DS_Store

Ora crea una nuova directory public.

mkdir public
cd public

All'interno di questa directory pubblica, crea il file index.html per il front-end, che utilizzerà 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. Esegui l'applicazione localmente

In questa sezione, eseguirai l'applicazione localmente per verificare che ci sia un bug nell'applicazione quando l'utente tenta di salvare i dati.

Innanzitutto, devi disporre del ruolo Utente Datastore per accedere a Firestore (se utilizzi la tua identità per l'autenticazione, ad esempio se esegui l'operazione in Cloud Shell) oppure puoi simulare l'identità dell'account utente creato in precedenza.

Utilizzo di ADC durante l'esecuzione locale

Se esegui Cloud Shell, la macchina virtuale Google Compute Engine è già in esecuzione. Le credenziali associate a questa macchina virtuale (come mostrato dall'esecuzione di gcloud auth list) verranno utilizzate automaticamente dalle credenziali predefinite dell'applicazione (ADC), pertanto non è necessario utilizzare il comando gcloud auth application-default login. Tuttavia, la tua identità dovrà comunque avere il ruolo Utente Datastore. Puoi passare direttamente alla sezione Esegui l'app localmente.

Tuttavia, se esegui l'operazione sul terminale locale (ovvero non in Cloud Shell), devi utilizzare le Credenziali predefinite dell'applicazione per l'autenticazione alle API di Google. Puoi 1) accedere utilizzando le tue credenziali (a condizione che tu disponga del ruolo Utente Datastore) oppure 2) accedere simulando l'identità del service account utilizzato in questo codelab.

Opzione 1: utilizzo delle credenziali per le Credenziali predefinite dell'applicazione

Se vuoi utilizzare le tue credenziali, puoi prima eseguire gcloud auth list per verificare la tua autenticazione in gcloud. Successivamente, potrebbe essere necessario concedere all'identità il ruolo Utente Vertex AI. Se la tua identità ha il ruolo Proprietario, disponi già di questo ruolo utente Datastore User. In caso contrario, puoi eseguire questo comando per concedere alla tua identità il ruolo Utente Vertex AI e il ruolo Utente Datastore.

USER=<YOUR_PRINCIPAL_EMAIL>

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

Quindi esegui questo comando

gcloud auth application-default login

Opzione 2: simulare l'identità di un service account per ADC

Se vuoi utilizzare il service account creato in questo codelab, il tuo account utente deve disporre del ruolo Creatore token service account. Puoi ottenere questo ruolo eseguendo il seguente comando:

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

Successivamente, esegui questo comando per utilizzare ADC con l'account di servizio

gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS

Esegui l'app localmente

Successivamente, assicurati di trovarti nella directory principale cloud-run-github-cd-demo del codelab.

cd .. && pwd

Ora installerai le dipendenze.

npm install

Infine, puoi avviare l'app eseguendo il seguente script. Questo script genererà anche il file output.css da tailwindCSS.

npm run dev

Ora apri il browser web all'indirizzo http://localhost:8080. Se ti trovi in Cloud Shell, puoi aprire il sito web aprendo il pulsante Anteprima web e selezionando Anteprima porta 8080.

Anteprima web - pulsante Anteprima sulla porta 8080

Inserisci il testo per i campi di input nome e città e fai clic su Salva. Poi aggiorna la pagina. Noterai che il campo della città non è stato mantenuto. Correggerai questo bug nella sezione successiva.

Interrompi l'esecuzione locale dell'app express (ad es. Ctrl^c su macOS).

8. Creare un repository GitHub

Nella directory locale, crea un nuovo repository con main come nome del branch predefinito.

git init
git branch -M main

Esegui il commit della codebase attuale che contiene il bug. Correggerai il bug dopo aver configurato il deployment continuo.

git add .
git commit -m "first commit for express application"

Vai su GitHub e crea un repository vuoto privato o pubblico. Questo codelab consiglia di denominare il repository cloud-run-auto-deploy-codelab Per creare un repository vuoto, lascia tutte le impostazioni predefinite deselezionate o impostate su Nessuno in modo che non ci siano contenuti nel repository per impostazione predefinita al momento della creazione, ad esempio:

Impostazioni predefinite di GitHub

Se hai completato correttamente questo passaggio, nella pagina del repository vuoto vedrai le seguenti istruzioni:

Istruzioni per il repository GitHub vuoto

Segui le istruzioni per eseguire il push di un repository esistente dalla riga di comando eseguendo i seguenti comandi:

Per prima cosa, aggiungi il repository remoto eseguendo

git remote add origin <YOUR-REPO-URL-PER-GITHUB-INSTRUCTIONS>

quindi esegui il push del ramo principale nel repository upstream.

git push -u origin main

9. Configura il deployment continuo

Ora che hai il codice in GitHub, puoi configurare il deployment continuo. Vai alla console Cloud per Cloud Run.

  • Fai clic su Crea un servizio.
  • Fai clic su Esegui il deployment continuo da un repository.
  • Fai clic su CONFIGURA CLOUD BUILD.
  • In Repository di origine
    • Seleziona GitHub come provider di repository
    • Fai clic su Gestisci repository connessi per configurare l'accesso di Cloud Build al repository.
    • Seleziona il repository e fai clic su Avanti.
  • In Configurazione build
    • Lascia Branch come ^main$
    • Per Tipo di build, seleziona Go, Node.js, Python, Java, .NET Core, Ruby o PHP tramite i buildpack Google Cloud
  • Lascia la directory contesto della build come /
  • Fai clic su Salva.
  • Nella sezione Autenticazione
    • Fai clic su Consenti chiamate non autenticate.
  • In Container, volumi, networking, sicurezza
    • Nella scheda Sicurezza, seleziona il service account che hai creato in un passaggio precedente, ad esempio Cloud Run access to Firestore.
  • Fai clic su CREA.

Verrà eseguito il deployment del servizio Cloud Run contenente il bug che correggerai nella sezione successiva.

10. Correggi il bug

Correggi il bug nel codice

Nell'editor di Cloud Shell, apri il file app.js e vai al commento che dice //TODO: fix this bug

modifica la seguente riga da

 //TODO: fix this bug
    await doc.set({
        name: name
    });

a

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

Verifica la correzione eseguendo

npm run start

e apri il browser web. Salva di nuovo i dati per la città e aggiorna. Vedrai che i dati della città appena inseriti sono stati mantenuti correttamente dopo l'aggiornamento.

Ora che hai verificato la correzione, puoi eseguirne il deployment. Innanzitutto, esegui il commit della correzione.

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

e poi eseguine il push nel repository upstream su GitHub.

git push origin main

Cloud Build eseguirà automaticamente il deployment delle modifiche. Puoi andare alla console Cloud per il tuo servizio Cloud Run per monitorare le modifiche al deployment.

Verifica la correzione in produzione

Quando Cloud Console per il servizio Cloud Run mostra che una seconda revisione ora gestisce il 100% del traffico, ad esempio https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions, puoi aprire l'URL del servizio Cloud Run nel browser e verificare che i dati della città appena inseriti vengano mantenuti dopo l'aggiornamento della pagina.

11. Complimenti!

Congratulazioni per aver completato il codelab.

Ti consigliamo di consultare la documentazione relativa a Cloud Run e al deployment continuo da git.

Argomenti trattati

  • Scrivere un'applicazione web Express con l'editor di Cloud Shell
  • Collega il tuo account GitHub a Google Cloud per i deployment continui
  • Esegui automaticamente il deployment dell'applicazione in Cloud Run
  • Scopri come utilizzare HTMX e TailwindCSS

12. Esegui la pulizia

Per evitare addebiti involontari (ad esempio, se i servizi Cloud Run vengono richiamati inavvertitamente più volte rispetto all'allocazione mensile di chiamate di Cloud Run nel livello senza costi), puoi eliminare Cloud Run o il progetto che hai creato nel passaggio 2.

Per eliminare il servizio Cloud Run, vai alla console Cloud Run all'indirizzo https://console.cloud.google.com/run ed elimina il servizio Cloud Run che hai creato in questo codelab, ad esempio elimina il servizio cloud-run-auto-deploy-codelab.

Se scegli di eliminare l'intero progetto, puoi andare alla pagina https://console.cloud.google.com/cloud-resource-manager, selezionare il progetto che hai creato nel passaggio 2 e scegliere Elimina. Se elimini il progetto, dovrai cambiare progetto in Cloud SDK. Puoi visualizzare l'elenco di tutti i progetti disponibili eseguendo gcloud projects list.