Jak automatycznie wdrażać zmiany z GitHuba w Cloud Run za pomocą Cloud Build

1. Wprowadzenie

Omówienie

W ramach tego ćwiczenia w Codelabs skonfigurujesz Cloud Run tak, aby automatycznie kompilował i wdrażał nowe wersje aplikacji za każdym razem, gdy przekażesz zmiany kodu źródłowego do repozytorium GitHub.

Ta aplikacja demonstracyjna zapisuje dane użytkownika w firestore, ale tylko część danych jest zapisywana prawidłowo. Ciągłe wdrożenia skonfigurujesz w taki sposób, że po przekazaniu poprawki błędu do repozytorium GitHub automatycznie wyświetli się poprawka w nowej wersji.

Czego się nauczysz

  • Pisanie aplikacji internetowej Express w edytorze Cloud Shell
  • Połącz konto GitHub z Google Cloud na potrzeby ciągłego wdrażania
  • Automatyczne wdrażanie aplikacji w Cloud Run
  • Dowiedz się, jak używać HTMX i TailwindCSS

2. Konfiguracja i wymagania

Wymagania wstępne

Aktywowanie Cloud Shell

  1. W konsoli Cloud kliknij Aktywuj Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Jeśli uruchamiasz Cloud Shell po raz pierwszy, zobaczysz ekran pośredni z opisem tej usługi. Jeśli wyświetlił się ekran pośredni, kliknij Dalej.

d95252b003979716.png

Uzyskanie dostępu do Cloud Shell i połączenie się z nim powinno zająć tylko kilka chwil.

7833d5e1c5d18f54.png

Ta maszyna wirtualna ma wszystkie potrzebne narzędzia dla programistów. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i uwierzytelnianie. Większość zadań w ramach tego ćwiczenia z programowania można wykonać w przeglądarce.

Po nawiązaniu połączenia z Cloud Shell powinno pojawić się potwierdzenie, że użytkownik jest uwierzytelniony, a projekt jest ustawiony na identyfikator Twojego projektu.

  1. Uruchom to polecenie w Cloud Shell, aby potwierdzić, że jesteś uwierzytelniony:
gcloud auth list

Dane wyjściowe polecenia

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Uruchom to polecenie w Cloud Shell, aby sprawdzić, czy polecenie gcloud zna Twój projekt:
gcloud config list project

Dane wyjściowe polecenia

[core]
project = <PROJECT_ID>

Jeśli tak nie jest, możesz go ustawić za pomocą tego polecenia:

gcloud config set project <PROJECT_ID>

Dane wyjściowe polecenia

Updated property [core/project].

3. Włączanie interfejsów API i ustawianie zmiennych środowiskowych

Włącz interfejsy API

To ćwiczenie w Codelabs wymaga używania poniższych interfejsów API. Możesz włączyć te interfejsy API, uruchamiając to polecenie:

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

Skonfiguruj zmienne środowiskowe

Możesz ustawić zmienne środowiskowe, które będą używane podczas tego ćwiczenia z programowania.

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. Tworzenie konta usługi

To konto usługi będzie używane przez Cloud Run do wywoływania interfejsu Vertex AI Gemini API. To konto usługi będzie też miało uprawnienia do odczytu i zapisu w Firestore oraz do odczytu obiektów tajnych z usługi Secret Manager.

Najpierw utwórz konto usługi, uruchamiając to polecenie:

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

Teraz przyznaj kontu usługi uprawnienia do odczytu i zapisu w Firestore.

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

5. Tworzenie i konfigurowanie projektu Firebase

  1. W konsoli Firebase kliknij Dodaj projekt.
  2. Wpisz <IDENTYFIKATOR_TWOJEGO_PROJEKTU> aby dodać Firebase do jednego z istniejących projektów Google Cloud
  3. W razie potrzeby przeczytaj i zaakceptuj warunki korzystania z Firebase.
  4. Kliknij Dalej.
  5. Kliknij Potwierdź abonament, aby potwierdzić abonament Firebase.
  6. W tym ćwiczeniu z programowania można opcjonalnie włączyć Google Analytics.
  7. Kliknij Dodaj Firebase.
  8. Po utworzeniu projektu kliknij Dalej.
  9. W menu Build (Tworzenie) kliknij Firestore database (Baza danych Firestore).
  10. Kliknij Utwórz bazę danych.
  11. Wybierz region z menu Lokalizacja i kliknij Dalej.
  12. Użyj domyślnej opcji Rozpocznij w trybie produkcyjnym, a następnie kliknij Utwórz.

6. Tworzenie aplikacji

Najpierw utwórz katalog na kod źródłowy i cd w tym katalogu.

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

Następnie utwórz plik package.json z tą zawartością:

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

Najpierw utwórz plik źródłowy app.js z poniższą zawartością. Ten plik zawiera punkt wejścia usługi i główną logikę aplikacji.

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

Utwórz plik o nazwie 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>`;

Utwórz plik input.css dla tailwindCSS

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

I utwórz plik tailwind.config.js dla tailwindCSS.

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

I utwórz plik .gitignore.

node_modules/

npm-debug.log
coverage/

package-lock.json

.DS_Store

Teraz utwórz nowy katalog public.

mkdir public
cd public

W tym katalogu publicznym utwórz plik index.html dla interfejsu o nazwie 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. Uruchamianie aplikacji lokalnie

W tej sekcji uruchomisz aplikację lokalnie, aby potwierdzić, że w niej wystąpił błąd, gdy użytkownik próbuje zapisać dane.

Najpierw musisz mieć rolę użytkownika Datastore, aby uzyskać dostęp do Firestore (jeśli do uwierzytelniania używasz swojej tożsamości, np. pracujesz w Cloud Shell), lub możesz przyjąć wcześniej utworzone konto użytkownika.

Korzystanie z ADC w przypadku uruchomienia lokalnego

Jeżeli korzystasz z Cloud Shell, oznacza to, że korzystasz już z maszyny wirtualnej Google Compute Engine. Twoje dane logowania powiązane z tą maszyną wirtualną (wyświetlane przez uruchomienie gcloud auth list) będą automatycznie używane przez domyślne dane logowania aplikacji (ADC), więc nie musisz używać polecenia gcloud auth application-default login. Twoja tożsamość nadal będzie jednak wymagała roli użytkownika Datastore. Możesz przejść do sekcji Uruchamianie aplikacji lokalnie.

Jeśli jednak korzystasz z terminala lokalnego (tzn. nie w Cloud Shell), do uwierzytelniania w interfejsach API Google musisz używać domyślnych danych logowania aplikacji. Możesz 1) zalogować się za pomocą swoich danych logowania (jeśli masz rolę użytkownika Datastore) lub 2) zalogować się, podszywając się pod konto usługi użyte w tym ćwiczeniu z programowania.

Opcja 1. Używanie Twoich danych logowania do ADC

Jeśli chcesz użyć danych logowania, najpierw uruchom polecenie gcloud auth list, aby sprawdzić sposób uwierzytelniania w gcloud. Następnie może być konieczne przypisanie do tożsamości roli użytkownika Vertex AI. Jeśli Twoja tożsamość ma rolę właściciela, masz już tę rolę użytkownika Datastore. Jeśli nie, możesz uruchomić to polecenie, aby przypisać rolę użytkownika Vertex AI tożsamości i rolę użytkownika Datastore.

USER=<YOUR_PRINCIPAL_EMAIL>

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

Następnie uruchom to polecenie

gcloud auth application-default login

Opcja 2. Odgrywanie roli konta usługi na potrzeby ADC

Jeśli chcesz używać konta usługi utworzonego w ramach tego ćwiczenia w Codelabs, Twoje konto użytkownika musi mieć przypisaną rolę twórcy tokenów konta usługi. Aby uzyskać tę rolę, uruchom następujące polecenie:

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

Następnie uruchom poniższe polecenie, aby użyć ADC z kontem usługi

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

Lokalne uruchamianie aplikacji

Upewnij się, że jesteś w katalogu głównym cloud-run-github-cd-demo, w którym wykonujesz ćwiczenia z programowania.

cd .. && pwd

Teraz zainstalujesz zależności.

npm install

Na koniec możesz uruchomić aplikację, uruchamiając poniższy skrypt. Ten skrypt wygeneruje też plikoutput.css z tailwindCSS.

npm run dev

Otwórz w przeglądarce stronę http://localhost:8080. Jeśli korzystasz z Cloud Shell, możesz otworzyć witrynę, klikając przycisk Podgląd w przeglądarce i wybierając Port podglądu 8080.

podgląd w przeglądarce – przycisk podglądu na porcie 8080

Wpisz tekst w polach nazwy i miejscowości, a następnie kliknij Zapisz. Następnie odśwież stronę. Jak widać, pole miejscowości nie zostało zachowane. Naprawimy ten błąd w kolejnej sekcji.

Wyłącz lokalne uruchamianie aplikacji Express (np. naciskając Ctrl^c w systemie macOS).

8. Tworzenie repozytorium GitHub

W katalogu lokalnym utwórz nowe repozytorium z nazwą gałęzi „main” (główna).

git init
git branch -M main

Zatwierdź bieżącą bazę kodu, która zawiera błąd. Błąd zostanie naprawiony po skonfigurowaniu ciągłego wdrażania.

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

Otwórz GitHuba i utwórz puste repozytorium, które będzie prywatne lub publiczne. W ramach tego ćwiczenia w Codelabs zalecamy nazwanie repozytorium cloud-run-auto-deploy-codelab Aby utworzyć puste repozytorium, pozostaw wszystkie ustawienia domyślne odznaczone lub ustaw wartość „brak”, dzięki czemu po utworzeniu domyślnie żadne treści nie będą się w nim znajdować, np.

Domyślne ustawienia GitHuba

Jeśli wykonasz ten krok prawidłowo, na stronie pustego repozytorium zobaczysz te instrukcje:

Instrukcje dotyczące pustego repozytorium GitHub

Postępuj zgodnie z instrukcjami przekazywania istniejącego repozytorium z wiersza poleceń, uruchamiając te polecenia:

Najpierw dodaj repozytorium zdalne, uruchamiając polecenie

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

a następnie wypchnij gałąź główną do repozytorium nadrzędnego.

git push -u origin main

9. Skonfiguruj ciągłe wdrażanie

Po utworzeniu kodu w GitHubie możesz skonfigurować ciągłe wdrażanie. Otwórz konsolę Cloud dla Cloud Run.

  • Kliknij Utwórz usługę
  • Kliknij Wdrażaj w sposób ciągły z repozytorium.
  • Kliknij SKONFIGURUJ CLOUD BUILD.
  • W repozytorium źródłowym
    • Wybierz GitHub jako dostawcę repozytorium
    • Kliknij Zarządzaj połączonymi repozytoriami, aby skonfigurować dostęp Cloud Build do repozytorium.
    • Wybierz repozytorium i kliknij Dalej
  • W sekcji Konfiguracja kompilacji
    • Pozostaw gałąź jako ^main$
    • Jako typ kompilacji wybierz Go, Node.js, Python, Java, .NET Core, Ruby lub PHP za pomocą pakietów kompilacji Google Cloud.
  • Pozostaw katalog kontekstu kompilacji jako /
  • Kliknij Zapisz.
  • W sekcji Uwierzytelnianie
    • Kliknij Zezwalaj na nieuwierzytelnione wywołania.
  • W sekcji Kontenery, Woluminy, Sieć, Zabezpieczenia
    • Na karcie Zabezpieczenia wybierz konto usługi utworzone w poprzednim kroku, na przykład Cloud Run access to Firestore
  • Kliknij UTWÓRZ.

Spowoduje to wdrożenie usługi Cloud Run zawierającej błąd, który naprawisz w następnej sekcji.

10. Napraw błąd

Napraw błąd w kodzie

W edytorze Cloud Shell otwórz plik app.js i przejdź do komentarza o treści //TODO: fix this bug

zmień następujący wiersz z

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

 

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

Sprawdź poprawkę, uruchamiając polecenie

npm run start

i otwórz przeglądarkę. Znowu zapisz dane o mieście i odśwież stronę. Zobaczysz, że po odświeżeniu dane o mieście zostały poprawnie zapisane.

Po zweryfikowaniu poprawki możesz ją wdrożyć. Najpierw zatwierdź poprawkę.

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

a potem wypchnij go do repozytorium nadrzędnego w GitHubie.

git push origin main

Cloud Build automatycznie wdroży zmiany. Aby monitorować zmiany wdrożenia, możesz otworzyć konsolę Cloud dla usługi Cloud Run.

Sprawdzanie poprawki w wersji produkcyjnej

Gdy w konsoli Cloud dla usługi Cloud Run widoczna jest druga wersja usługi, która obsługuje 100% ruchu, np. https://console.cloud.google.com/run/detail/<TWÓJ_REGION>/<NAZWA_USŁUGI>/revisions, możesz otworzyć adres URL usługi Cloud Run w przeglądarce i sprawdzić, czy nowo wprowadzone dane miasta są zachowywane po odświeżeniu strony.

11. Gratulacje!

Gratulujemy ukończenia ćwiczeń z programowania.

Zalecamy zapoznanie się z dokumentacją na temat Cloud Run i informacjami o ciągłym wdrażaniu z git.

Omówione zagadnienia

  • Pisanie aplikacji internetowej Express w edytorze Cloud Shell
  • Połącz konto GitHub z Google Cloud na potrzeby ciągłego wdrażania
  • Automatyczne wdrażanie aplikacji w Cloud Run
  • Dowiedz się, jak używać HTMX i TailwindCSS

12. Czyszczenie danych

Aby uniknąć niezamierzonych opłat (na przykład jeśli usługi Cloud Run były wywoływane więcej razy niż przez miesięczny przydział wywołań Cloud Run na poziomie bezpłatnym), możesz usunąć Cloud Run albo projekt utworzony w kroku 2.

Aby usunąć usługę Cloud Run, otwórz konsolę Cloud Run na stronie https://console.cloud.google.com/run i usuń usługę Cloud Run utworzoną w ramach tego ćwiczenia w Codelabs, np. usuń usługę cloud-run-auto-deploy-codelab.

Jeśli zdecydujesz się usunąć cały projekt, możesz otworzyć stronę https://console.cloud.google.com/cloud-resource-manager, wybrać projekt utworzony w kroku 2 i kliknąć Usuń. Jeśli usuniesz projekt, musisz zmienić projekty w pakiecie SDK Cloud. Aby wyświetlić listę wszystkich dostępnych projektów, uruchom polecenie gcloud projects list.