Déployer automatiquement vos modifications de GitHub vers Cloud Run à l'aide de Cloud Build

1. Introduction

Présentation

Dans cet atelier de programmation, vous allez configurer Cloud Run pour créer et déployer automatiquement de nouvelles versions de votre application chaque fois que vous transférez les modifications de votre code source vers un dépôt GitHub.

Cette application de démonstration enregistre les données utilisateur dans Firestore, mais seule une partie des données est correctement enregistrée. Vous allez configurer des déploiements continus de sorte que, lorsque vous enverrez un correctif de bug à votre dépôt GitHub, il sera automatiquement disponible dans une nouvelle révision.

Points abordés

  • Écrire une application Web Express avec l'éditeur Cloud Shell
  • Associer votre compte GitHub à Google Cloud pour les déploiements continus
  • Déployer automatiquement votre application sur Cloud Run
  • Découvrez comment utiliser HTMX et TailwindCSS

2. Préparation

Prérequis

Activer Cloud Shell

  1. Dans Cloud Console, cliquez sur Activer Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Si vous démarrez Cloud Shell pour la première fois, un écran intermédiaire s'affiche pour vous expliquer de quoi il s'agit. Si cet écran s'est affiché, cliquez sur Continuer.

d95252b003979716.png

Le provisionnement et la connexion à Cloud Shell ne devraient pas prendre plus de quelques minutes.

7833d5e1c5d18f54.png

Cette machine virtuelle contient tous les outils de développement nécessaires. Elle comprend un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances du réseau et l'authentification. Vous pouvez réaliser une grande partie, voire la totalité, des activités de cet atelier de programmation dans un navigateur.

Une fois connecté à Cloud Shell, vous êtes en principe authentifié, et le projet est défini sur votre ID de projet.

  1. Exécutez la commande suivante dans Cloud Shell pour vérifier que vous êtes authentifié :
gcloud auth list

Résultat de la commande

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Exécutez la commande suivante dans Cloud Shell pour vérifier que la commande gcloud connaît votre projet :
gcloud config list project

Résultat de la commande

[core]
project = <PROJECT_ID>

Si vous obtenez un résultat différent, exécutez cette commande :

gcloud config set project <PROJECT_ID>

Résultat de la commande

Updated property [core/project].

3. Activer les API et définir les variables d'environnement

Activer les API

Cet atelier de programmation nécessite l'utilisation des API suivantes. Vous pouvez activer ces API en exécutant la commande suivante :

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

Configurer des variables d'environnement

Vous pouvez définir des variables d'environnement qui seront utilisées tout au long de cet atelier de programmation.

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. Créer un compte de service

Ce compte de service sera utilisé par Cloud Run pour appeler l'API Gemini Vertex AI. Ce compte de service sera également autorisé à lire et à écrire dans Firestore, et à lire les secrets de Secret Manager.

Commencez par créer le compte de service en exécutant cette commande :

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

Accordez maintenant au compte de service un accès en lecture et en écriture à Firestore.

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

5. Créer et configurer un projet Firebase

  1. Dans la console Firebase, cliquez sur Ajouter un projet.
  2. Saisissez <YOUR_PROJECT_ID> pour ajouter Firebase à l'un de vos projets Google Cloud existants.
  3. Si vous y êtes invité, lisez et acceptez les Conditions d'utilisation de Firebase.
  4. Cliquez sur Continuer.
  5. Cliquez sur Confirmer le mode de facturation pour confirmer le mode de facturation Firebase.
  6. L'activation de Google Analytics pour cet atelier de programmation est facultative.
  7. Cliquez sur Ajouter Firebase :
  8. Une fois le projet créé, cliquez sur Continuer.
  9. Dans le menu Build (Créer), cliquez sur Firestore database (Base de données Firestore).
  10. Cliquez sur Créer une base de données.
  11. Sélectionnez votre région dans le menu déroulant Emplacement, puis cliquez sur Suivant.
  12. Utilisez l'option par défaut Démarrer en mode de production, puis cliquez sur Créer.

6. Écrire l'application

Commencez par créer un répertoire pour le code source et accédez-y.

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

Créez ensuite un fichier package.json avec le contenu suivant :

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

Commencez par créer un fichier source app.js avec le contenu ci-dessous. Ce fichier contient le point d'entrée du service et la logique principale de l'application.

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);
});

Créez un fichier appelé 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>`;

Créer un fichier input.css pour tailwindCSS

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

Créez le fichier tailwind.config.js pour tailwindCSS.

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

Créez un fichier .gitignore.

node_modules/

npm-debug.log
coverage/

package-lock.json

.DS_Store

Créez ensuite un répertoire public.

mkdir public
cd public

Dans ce répertoire public, créez le fichier index.html pour le frontend, qui utilisera 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. Exécuter l'application en local

Dans cette section, vous allez exécuter l'application en local pour confirmer qu'un bug se produit lorsque l'utilisateur tente d'enregistrer des données.

Tout d'abord, vous devez disposer du rôle Utilisateur Datastore pour accéder à Firestore (si vous utilisez votre identité pour l'authentification, par exemple si vous exécutez Cloud Shell) ou vous pouvez emprunter l'identité du compte utilisateur créé précédemment.

Utiliser les identifiants par défaut de l'application lors de l'exécution en local

Si vous exécutez Cloud Shell, vous êtes déjà sur une machine virtuelle Google Compute Engine. Vos identifiants associés à cette machine virtuelle (comme indiqué par l'exécution de gcloud auth list) seront automatiquement utilisés par les identifiants par défaut de l'application (ADC, Application Default Credentials). Il n'est donc pas nécessaire d'utiliser la commande gcloud auth application-default login. Toutefois, votre identité devra toujours disposer du rôle "Utilisateur Datastore". Vous pouvez passer directement à la section Exécuter l'application en local.

Toutefois, si vous exécutez l'exemple sur votre terminal local (c'est-à-dire pas dans Cloud Shell), vous devrez utiliser les identifiants par défaut de l'application pour vous authentifier auprès des API Google. Vous pouvez soit 1) vous connecter à l'aide de vos identifiants (à condition que vous disposiez du rôle "Utilisateur Datastore"), soit 2) vous connecter en empruntant l'identité du compte de service utilisé dans cet atelier de programmation.

Option 1 : Utiliser vos identifiants pour ADC

Si vous souhaitez utiliser vos identifiants, vous pouvez d'abord exécuter gcloud auth list pour vérifier votre méthode d'authentification dans gcloud. Ensuite, vous devrez peut-être attribuer le rôle Utilisateur Vertex AI à votre identité. Si votre identité dispose du rôle Propriétaire, vous disposez déjà du rôle utilisateur Utilisateur Datastore. Sinon, vous pouvez exécuter cette commande pour attribuer à votre identité le rôle d'utilisateur Vertex AI et le rôle d'utilisateur Datastore.

USER=<YOUR_PRINCIPAL_EMAIL>

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

Exécutez ensuite la commande suivante :

gcloud auth application-default login

Option 2 : Emprunter l'identité d'un compte de service pour ADC

Si vous souhaitez utiliser le compte de service créé dans cet atelier de programmation, votre compte utilisateur doit disposer du rôle Créateur de jetons de compte de service. Vous pouvez obtenir ce rôle en exécutant la commande suivante :

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

Ensuite, exécutez la commande suivante pour utiliser ADC avec le compte de service.

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

Exécuter l'application en local

Ensuite, assurez-vous de vous trouver dans le répertoire racine cloud-run-github-cd-demo de votre atelier de programmation.

cd .. && pwd

Vous allez maintenant installer les dépendances.

npm install

Enfin, vous pouvez démarrer l'application en exécutant le script suivant. Ce script générera également le fichier output.css à partir de tailwindCSS.

npm run dev

Ouvrez maintenant votre navigateur Web et accédez à http://localhost:8080. Si vous êtes dans Cloud Shell, vous pouvez ouvrir le site Web en cliquant sur le bouton "Aperçu sur le Web" et en sélectionnant "Prévisualiser le port 8080".

Bouton &quot;Aperçu sur le Web&quot; > &quot;Prévisualiser sur le port 8080&quot;

Saisissez du texte dans les champs de saisie du nom et de la ville, puis appuyez sur "Enregistrer". Actualisez ensuite la page. Vous remarquerez que le champ "Ville" n'a pas été conservé. Vous corrigerez ce bug dans la section suivante.

Arrêtez l'exécution de l'application Express en local (par exemple, Ctrl^c sur macOS).

8. Créer un dépôt GitHub

Dans votre répertoire local, créez un dépôt avec "main" comme nom de branche par défaut.

git init
git branch -M main

Validez le codebase actuel qui contient le bug. Vous corrigerez le bug une fois le déploiement continu configuré.

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

Accédez à GitHub et créez un dépôt vide, privé ou public. Dans cet atelier de programmation, nous vous recommandons de nommer votre dépôt cloud-run-auto-deploy-codelab. Pour créer un dépôt vide, laissez tous les paramètres par défaut décochés ou définis sur "Aucun" afin qu'aucun contenu ne soit dans le dépôt par défaut lors de sa création, par exemple :

Paramètres GitHub par défaut

Si vous avez effectué cette étape correctement, les instructions suivantes s'affichent sur la page du dépôt vide :

Instructions pour un dépôt GitHub vide

Vous allez suivre les instructions Envoyer un dépôt existant depuis la ligne de commande en exécutant les commandes suivantes :

Commencez par ajouter le dépôt distant en exécutant

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

Transférez ensuite la branche principale vers le dépôt en amont.

git push -u origin main

9. Configurer le déploiement continu

Maintenant que vous avez du code dans un dépôt GitHub, vous pouvez configurer le déploiement continu. Accédez à la console Cloud pour Cloud Run.

  • Cliquez sur "Créer un service".
  • Cliquez sur Déployer en continu depuis un dépôt.
  • Cliquez sur CONFIGURER CLOUD BUILD.
  • Sous "Dépôt source"
    • Sélectionner GitHub comme fournisseur de dépôt
    • Cliquez sur Gérer les dépôts connectés pour configurer l'accès de Cloud Build au dépôt.
    • Sélectionnez votre dépôt, puis cliquez sur Suivant.
  • Sous "Configuration de la compilation"
    • Laissez la branche sur ^main$.
    • Pour "Type de compilation", sélectionnez Go, Node.js, Python, Java, .NET Core, Ruby ou PHP via les buildpacks Google Cloud.
  • Laissez le répertoire de contexte pour le build sur /.
  • Cliquez sur Enregistrer.
  • Sous Authentification
    • Cliquez sur Autoriser les appels non authentifiés.
  • Sous Conteneur(s), volumes, mise en réseau, sécurité
    • Dans l'onglet "Sécurité", sélectionnez le compte de service que vous avez créé lors d'une étape précédente (par exemple, Cloud Run access to Firestore).
  • Cliquez sur CRÉER.

Cela déploiera le service Cloud Run contenant le bug que vous corrigerez dans la section suivante.

10. Corriger le bug

Corrigez le bug dans le code.

Dans l'éditeur Cloud Shell, ouvrez le fichier app.js et accédez au commentaire //TODO: fix this bug.

Remplacez la ligne suivante :

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

pour

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

Vérifiez la correction en exécutant

npm run start

et ouvrez votre navigateur Web. Enregistrez à nouveau les données pour la ville, puis actualisez. Vous verrez que les données de ville que vous venez de saisir sont toujours là après l'actualisation.

Maintenant que vous avez vérifié votre correctif, vous êtes prêt à le déployer. Commencez par valider le correctif.

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

et de le transférer vers le dépôt en amont sur GitHub.

git push origin main

Cloud Build déploiera automatiquement vos modifications. Vous pouvez accéder à la console Cloud pour votre service Cloud Run afin de surveiller les modifications apportées au déploiement.

Vérifier la correction en production

Une fois que la console Cloud de votre service Cloud Run indique qu'une deuxième révision diffuse désormais 100 % du trafic (par exemple, https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions), vous pouvez ouvrir l'URL du service Cloud Run dans votre navigateur et vérifier que les données de ville nouvellement saisies sont conservées après l'actualisation de la page.

11. Félicitations !

Bravo ! Vous avez terminé cet atelier de programmation.

Nous vous recommandons de consulter la documentation Cloud Run et Déploiement continu depuis Git.

Points abordés

  • Écrire une application Web Express avec l'éditeur Cloud Shell
  • Associer votre compte GitHub à Google Cloud pour les déploiements continus
  • Déployer automatiquement votre application sur Cloud Run
  • Découvrez comment utiliser HTMX et TailwindCSS

12. Effectuer un nettoyage

Pour éviter des frais involontaires (par exemple, si les services Cloud Run sont invoqués par inadvertance plus de fois que votre quota mensuel d'invocations Cloud Run dans le niveau sans frais), vous pouvez supprimer Cloud Run ou le projet que vous avez créé à l'étape 2.

Pour supprimer le service Cloud Run, accédez à la console Cloud Run sur https://console.cloud.google.com/run, puis supprimez le service Cloud Run que vous avez créé dans cet atelier de programmation (par exemple, le service cloud-run-auto-deploy-codelab).

Si vous choisissez de supprimer l'intégralité du projet, vous pouvez accéder à https://console.cloud.google.com/cloud-resource-manager, sélectionner le projet que vous avez créé à l'étape 2, puis choisir "Supprimer". Si vous supprimez le projet, vous devrez changer de projet dans votre SDK Cloud. Vous pouvez afficher la liste de tous les projets disponibles en exécutant gcloud projects list.