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 automaticamente il deployment delle 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 degli utenti su Firestore. Tuttavia, viene salvata correttamente solo una parte parziale dei dati. Configurerai deployment continui in modo che, quando esegui il push di una correzione di bug nel tuo repository GitHub, la correzione diventi automaticamente disponibile in una nuova revisione.

Cosa imparerai a fare

  • Scrivi un'applicazione web Express con l'editor di Cloud Shell
  • Connetti il tuo account GitHub a Google Cloud per 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. Dalla console Cloud, fai clic su Attiva Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Se è la prima volta che avvii Cloud Shell, ti verrà mostrata una schermata intermedia che descrive di cosa si tratta. Se ti è stata presentata una schermata intermedia, fai clic su Continua.

d95252b003979716.png

Il provisioning e la connessione a Cloud Shell dovrebbero richiedere solo qualche istante.

7833d5e1c5d18f54.png

Questa macchina virtuale viene 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 di rete e l'autenticazione. Gran parte, se non tutto, del lavoro in questo codelab può essere svolto con un browser.

Una volta stabilita la connessione a Cloud Shell, dovresti vedere che hai eseguito l'autenticazione e che 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 confermare che il comando gcloud è a conoscenza del 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 API seguenti. Puoi abilitare queste API eseguendo questo 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 account di servizio verrà utilizzato da Cloud Run per chiamare l'API Gemini di Vertex AI. Questo account di servizio avrà anche le autorizzazioni per leggere e scrivere su Firestore e leggere i secret da Secret Manager.

Innanzitutto, crea l'account di servizio eseguendo questo comando:

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

Ora concedi all'account di servizio 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. Attivare Google Analytics per questo codelab è facoltativo.
  7. Fai clic su Aggiungi Firebase.
  8. Una volta creato il progetto, fai clic su Continua.
  9. Dal menu Crea, fai clic su Database Firestore.
  10. Fai clic su Crea database.
  11. Scegli la tua regione dall'elenco a discesa Località, poi fai clic su Avanti.
  12. Utilizza l'impostazione predefinita Avvia in modalità di produzione, quindi fai clic su Crea.

6. Scrivi l'applicazione

Per prima cosa, crea una directory per il codice sorgente e accedi a quella directory.

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

Quindi, crea un file package.json con i seguenti contenuti:

{
  "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 sorgente app.js con i contenuti seguenti. 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>`;

Crea un file input.css per tailwindCSS

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

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

E all'interno della directory pubblica, crea il file index.html per il front-end, che utilizzerà il formato 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 in locale

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

Innanzitutto, dovrai 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 impersonare l'account utente creato in precedenza.

Utilizzo di ADC durante l'esecuzione in locale

Se l'esecuzione è in Cloud Shell, significa che è già in esecuzione su una macchina virtuale Google Compute Engine. Le tue credenziali associate a questa macchina virtuale (come mostrato dall'esecuzione di gcloud auth list) verranno utilizzate automaticamente da Credenziali predefinite dell'applicazione (ADC), quindi non è necessario utilizzare il comando gcloud auth application-default login. Tuttavia, per la tua identità sarà comunque necessario il ruolo Utente Datastore. Puoi andare direttamente alla sezione Eseguire l'app in locale.

Se invece è in esecuzione sul terminale locale (ovvero non in Cloud Shell), dovrai utilizzare le Credenziali predefinite dell'applicazione per autenticarti alle API di Google. Puoi 1) accedere utilizzando le tue credenziali (a condizione che tu disponga del ruolo Utente Datastore) o 2) accedere impersonando l'account di servizio utilizzato in questo codelab.

Opzione 1) Utilizzare le tue credenziali per ADC

Se vuoi utilizzare le tue credenziali, puoi prima eseguire gcloud auth list per verificare il modo in cui hai eseguito l'autenticazione in gcloud. Successivamente, potresti dover concedere alla tua identità il ruolo Vertex AI User. Se la tua identità ha il ruolo Proprietario, hai già questo ruolo utente Utente Datastore. 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

Poi esegui questo comando

gcloud auth application-default login

Opzione 2) Furto d'identità di un account di servizio per ADC

Se vuoi utilizzare l'account di servizio creato in questo codelab, il tuo account utente dovrà avere il ruolo Creatore token account di servizio. Puoi ottenere questo ruolo eseguendo questo comando:

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

Successivamente, eseguirai 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

Dopodiché, assicurati di essere nella directory principale cloud-run-github-cd-demo del codelab.

cd .. && pwd

Ora installerai le dipendenze.

npm install

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

npm run dev

Ora apri il browser web su http://localhost:8080. Se sei in Cloud Shell, puoi aprire il sito web aprendo il pulsante Anteprima web e selezionando Porta di anteprima 8080.

anteprima web: pulsante Anteprima sulla porta 8080

Inserisci il testo per i campi di immissione del nome e della città e fai clic su Salva. Quindi aggiorna la pagina. Noterai che il campo della città non è rimasto. Lo correggerai nella sezione successiva.

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

8. Crea un repository GitHub

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

git init
git branch -M main

Esegui il commit del codebase corrente che contiene il bug. correggerai il bug dopo aver configurato il deployment continuo.

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

Vai a GitHub e crea un repository vuoto, privato o pubblico. Questo codelab consiglia di assegnare un nome al repository cloud-run-auto-deploy-codelab. Per creare un repository vuoto, lasci tutte le impostazioni predefinite deselezionate o non vuoi impostare nessuna, in modo che, per impostazione predefinita, nessun contenuto sia presente nel repository al momento della creazione, ad esempio

Impostazioni predefinite di GitHub

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

Istruzioni vuote per il repository GitHub

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

Innanzitutto, aggiungi il repository remoto eseguendo

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

quindi esegui il push del ramo principale al repository a monte.

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 codice sorgente
    • Seleziona GitHub come provider del repository
    • Fai clic su Gestisci repository connessi per configurare l'accesso di Cloud Build al repository
    • Seleziona il tuo repository e fai clic su Avanti
  • In Configurazione build
    • Lascia Ramo impostato su ^main$
    • Per Tipo di build, seleziona Go, Node.js, Python, Java, .NET Core, Ruby o PHP tramite i buildpack di Google Cloud
  • Lascia la directory di contesto della build su /
  • Fai clic su Salva.
  • In Autenticazione
    • Fai clic su Consenti chiamate non autenticate.
  • In Container, Volumi, Networking, Sicurezza
    • Nella scheda Sicurezza, seleziona l'account di servizio creato in un passaggio precedente, ad esempio Cloud Run access to Firestore
  • Fai clic su CREA.

In questo modo verrà eseguito il deployment del servizio Cloud Run contenente il bug che verrà corretto nella prossima sezione.

10. Correggere il bug

Correggere il bug nel codice

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

modifica la riga seguente 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. I dati relativi alla città appena inseriti sono 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"

ed eseguirne il push nel repository upstream su GitHub.

git push origin main

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

Verifica la correzione in produzione

Quando la console Cloud per il tuo servizio Cloud Run mostra che una seconda revisione sta gestendo 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 siano persistenti dopo l'aggiornamento della pagina.

11. Complimenti!

Complimenti per aver completato il codelab.

Ti consigliamo di consultare la documentazione Cloud Run e il deployment continuo da Git.

Argomenti trattati

  • Scrivi un'applicazione web Express con l'editor di Cloud Shell
  • Connetti il tuo account GitHub a Google Cloud per 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 inavvertitamente richiamati più volte rispetto all'allocazione mensile dei callout Cloud Run nel livello senza costi, puoi eliminare Cloud Run o eliminare 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 all'indirizzo 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 modificarli in Cloud SDK. Puoi visualizzare l'elenco di tutti i progetti disponibili eseguendo gcloud projects list.