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 du 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 configurerez des déploiements continus de sorte que lorsque vous transférerez un correctif de bug dans votre dépôt GitHub, le correctif sera automatiquement disponible dans une nouvelle révision.

Points abordés

  • Écrire une application Web Express avec l'éditeur Cloud Shell
  • Connectez votre compte GitHub à Google Cloud pour les déploiements continus
  • Déployer automatiquement votre application sur Cloud Run
  • En savoir plus sur l'utilisation de HTMX et de 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 vous explique de quoi il s'agit. Si un écran intermédiaire 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 dans Google Cloud, ce qui améliore considérablement les performances du réseau et l'authentification. Une grande partie, voire la totalité, de votre travail dans cet atelier de programmation peut être effectué dans un navigateur.

Une fois connecté à Cloud Shell, vous êtes 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 des 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 les 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 permettra à Cloud Run d'appeler l'API Gemini Vertex AI. Ce compte de service sera également autorisé à lire et écrire dans Firestore, et à lire des secrets depuis Secret Manager.

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

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 (Compiler), cliquez sur Firestore Database (Base de données Firestore).
  10. Cliquez sur Créer une base de données.
  11. Choisissez votre région dans le menu déroulant Emplacement, puis cliquez sur Suivant.
  12. Utilisez l'option par défaut Start in production mode (Démarrer en mode production), puis cliquez sur Create (Créer).

6. Écrire l'application

Tout d'abord, créez un répertoire pour le code source et utilisez la commande cd pour y accéder.

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

Ensuite, créez 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éer un fichier intitulé 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;

Et créer le fichier tailwind.config.js pour tailwindCSS.

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

Créez ensuite un fichier .gitignore.

node_modules/

npm-debug.log
coverage/

package-lock.json

.DS_Store

Maintenant, créez un répertoire public.

mkdir public
cd public

Dans ce répertoire public, créez le fichier index.html du frontal, 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 vérifier qu'elle présente un bug lorsque l'utilisateur tente d'enregistrer des données.

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

Utiliser ADC en cas d'exécution locale

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

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

Option 1 : Utiliser vos identifiants pour l'ADC

Si vous souhaitez utiliser vos identifiants, vous pouvez d'abord exécuter gcloud auth list pour vérifier votre authentification dans gcloud. Ensuite, vous devrez peut-être attribuer à votre identité le rôle "Utilisateur Vertex AI". Si votre identité dispose du rôle Propriétaire, vous disposez déjà de ce rôle utilisateur d'utilisateur Datastore. Si ce n'est pas le cas, vous pouvez exécuter cette commande pour attribuer à votre identité les rôles d'utilisateur Vertex AI et 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 du 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, vous exécuterez 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

Vérifiez ensuite que vous êtes bien dans le répertoire racine cloud-run-github-cd-demo de votre atelier de programmation.

cd .. && pwd

Maintenant, vous allez installer des 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

À présent, accédez à http://localhost:8080 dans votre navigateur Web. Si vous utilisez Cloud Shell, vous pouvez ouvrir le site Web en ouvrant le bouton "Aperçu sur le Web", puis en sélectionnant "Prévisualiser le port 8080".

Aperçu sur le Web : Prévisualiser sur le bouton du port 8080

Saisissez le texte dans les champs de saisie du nom et de la ville, puis cliquez sur "Enregistrer". Actualisez ensuite la page. Vous remarquerez que le champ "city" n'est pas persistant. Vous corrigerez ce bug dans la section suivante.

Arrêtez l'exécution locale de l'application Express (par exemple, Ctrl^c sous 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 contenant 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. Cet atelier de programmation recommande de nommer votre dépôt cloud-run-auto-deploy-codelab.Pour créer un dépôt vide, vous devez laisser tous les paramètres par défaut décochés ou les définir sur "aucun" afin qu'aucun contenu ne soit dans le dépôt par défaut lors de sa création.

Paramètres GitHub par défaut

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

Instructions pour vider le dépôt GitHub

Suivez les instructions pour envoyer un dépôt existant à partir de la ligne de commande en exécutant les commandes suivantes:

Tout d'abord, ajoutez le dépôt distant en exécutant

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

puis transférez 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 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 "Source Repository" (Dépôt source) :
    • Sélectionner GitHub comme fournisseur de dépôts
    • 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"
    • Laisser la branche en tant que ^main$
    • Dans "Type de compilation", sélectionnez Go, Node.js, Python, Java, .NET Core, Ruby ou PHP via les buildpacks de Google Cloud.
  • Laissez le répertoire de contexte de compilation sur /
  • Cliquez sur Enregistrer.
  • Sous "Authentification"
    • Cliquez sur Allow unauthenticated invocations (Autoriser les appels non authentifiés).
  • Sous Conteneur(s), Volumes, Mise en réseau et Sécurité
    • Dans l'onglet "Sécurité", sélectionnez le compte de service que vous avez créé précédemment, par exemple Cloud Run access to Firestore
  • Cliquez sur CRÉER.

Le service Cloud Run contenant le bug que vous allez corriger dans la section suivante sera déployé.

10. Corriger le bug

Corriger 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 le correctif en exécutant

npm run start

et ouvrez votre navigateur Web. Enregistrez à nouveau les données pour la ville, puis actualisez la page. Lorsque vous actualisez la page, les nouvelles données sur les villes que vous venez de saisir ont été conservées correctement.

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

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

puis les 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 de votre service Cloud Run pour surveiller les modifications apportées au déploiement.

Vérifier le correctif 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/<VOTRE_RÉGION>/<NOM_DE_VOTRE_SERVICE>/revisions, vous pouvez ouvrir l'URL du service Cloud Run dans votre navigateur et vérifier que les nouvelles données de ville saisies sont conservées après avoir actualisé la page.

11. Félicitations !

Félicitations ! Vous avez terminé cet atelier de programmation.

Nous vous recommandons de consulter la documentation concernant Cloud Run et le déploiement continu à partir de git.

Points abordés

  • Écrire une application Web Express avec l'éditeur Cloud Shell
  • Connectez votre compte GitHub à Google Cloud pour les déploiements continus
  • Déployer automatiquement votre application sur Cloud Run
  • En savoir plus sur l'utilisation de HTMX et de TailwindCSS

12. Effectuer un nettoyage

Pour éviter des frais accidentels (par exemple, si les services Cloud Run sont invoqués plus de fois que l'allocation mensuelle des appels Cloud Run dans le niveau sans frais), vous pouvez supprimer Cloud Run ou le projet créé à l'étape 2.

Pour supprimer le service Cloud Run, accédez à la console Cloud Run à l'adresse https://console.cloud.google.com/run et supprimez le service Cloud Run que vous avez créé dans cet atelier de programmation, par exemple supprimez 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 cliquer sur "Supprimer". Si vous supprimez le projet, vous devrez le modifier dans Cloud SDK. Vous pouvez afficher la liste de tous les projets disponibles en exécutant gcloud projects list.