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

1. Wprowadzenie

Przegląd

W tym ćwiczeniu dowiesz się, jak skonfigurować Cloud Run, aby automatycznie kompilować i wdrażać nowe wersje aplikacji za każdym razem, gdy przenosisz zmiany w kodzie źródłowym do repozytorium GitHub.

Ta aplikacja demonstracyjna zapisuje dane użytkownika w usłudze Firestore, ale tylko część danych jest zapisywana prawidłowo. Skonfigurujesz ciągłe wdrażanie w taki sposób, aby po przekazaniu poprawki błędu do repozytorium GitHub była ona automatycznie dostępna w nowej wersji.

Czego się nauczysz

  • Pisanie aplikacji internetowej Express za pomocą edytora Cloud Shell
  • Łączenie konta GitHub z Google Cloud w celu ciągłego wdrażania
  • Automatyczne wdrażanie aplikacji w Cloud Run
  • Dowiedz się, jak korzystać z 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 tego środowiska. Jeśli pojawił się ekran pośredni, kliknij Dalej.

d95252b003979716.png

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

7833d5e1c5d18f54.png

Ta maszyna wirtualna zawiera wszystkie potrzebne narzędzia dla programistów. Zawiera również stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i usprawnia proces uwierzytelniania. Większość zadań w tym ćwiczeniu, a być może wszystkie, możesz wykonać w przeglądarce.

Po połączeniu z Cloud Shell zobaczysz, że uwierzytelnianie zostało już przeprowadzone, a projekt jest już ustawiony na Twój identyfikator projektu.

  1. Aby potwierdzić, że uwierzytelnianie zostało przeprowadzone, uruchom w Cloud Shell to polecenie:
gcloud auth list

Wynik polecenia

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Aby potwierdzić, że polecenie gcloud zna Twój projekt, uruchom w Cloud Shell to polecenie:
gcloud config list project

Wynik polecenia

[core]
project = <PROJECT_ID>

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

gcloud config set project <PROJECT_ID>

Wynik polecenia

Updated property [core/project].

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

Włącz interfejsy API

W tym ćwiczeniu musisz użyć tych 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

Konfigurowanie zmiennych środowiskowych

Możesz ustawić zmienne środowiskowe, których będziesz używać podczas naszych ćwiczeń 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ż mieć uprawnienia do odczytu i zapisu w Firestore oraz do odczytywania 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 dostęp do Firestore z możliwością zapisu i odczytu.

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

5. Tworzenie i konfigurowanie projektu w Firebase

  1. W konsoli Firebase kliknij Dodaj projekt.
  2. Wpisz <YOUR_PROJECT_ID>, aby dodać Firebase do jednego z istniejących projektów Google Cloud.
  3. Jeśli pojawi się taka prośba, przeczytaj i zaakceptuj warunki usługi Firebase.
  4. Kliknij Dalej.
  5. Aby potwierdzić plan płatności Firebase, kliknij Confirm Plan (Potwierdź plan).
  6. Włączenie Google Analytics w tym samouczku jest opcjonalne.
  7. Kliknij Dodaj Firebase.
  8. Gdy projekt zostanie utworzony, kliknij Dalej.
  9. W menu Kompilacja kliknij Baza danych Firestore.
  10. Kliknij Utwórz bazę danych.
  11. Wybierz region z menu Lokalizacja, a potem kliknij Dalej.
  12. Użyj domyślnego ustawienia Uruchom w trybie produkcyjnym, a następnie kliknij Utwórz.

6. Tworzenie aplikacji

Najpierw utwórz katalog kodu źródłowego i przejdź do niego.

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

Następnie utwórz plik package.json o tej treś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 treścią podaną poniżej. 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, który będzie korzystać z 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. Lokalne uruchamianie aplikacji

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

Najpierw musisz mieć rolę użytkownika Datastore, aby uzyskać dostęp do Firestore (jeśli używasz tożsamości do uwierzytelniania, np. działasz w Cloud Shell), lub możesz podszyć się pod wcześniej utworzone konto użytkownika.

Używanie ADC podczas uruchamiania lokalnego

Jeśli korzystasz z Cloud Shell, używasz już maszyny wirtualnej Google Compute Engine. Dane uwierzytelniające powiązane z tą maszyną wirtualną (widoczne po uruchomieniu gcloud auth list) będą automatycznie używane przez domyślne uwierzytelnianie aplikacji (ADC), więc nie musisz używać polecenia gcloud auth application-default login. Jednak tożsamość nadal będzie wymagać roli Użytkownik Datastore. Możesz przejść do sekcji Lokalne uruchamianie aplikacji.

Jeśli jednak korzystasz z lokalnego terminala (czyli nie z Cloud Shell), do uwierzytelniania w interfejsach API Google musisz używać domyślnego uwierzytelniania aplikacji. Możesz 1) zalogować się przy użyciu swoich danych logowania (jeśli masz rolę Użytkownik Datastore) lub 2) zalogować się, przyjmując tożsamość konta usługi użytego w tym ćwiczeniu.

Opcja 1. Używanie danych logowania do domyślnego uwierzytelniania aplikacji

Jeśli chcesz użyć swoich danych logowania, możesz najpierw uruchomić polecenie gcloud auth list, aby sprawdzić, jak uwierzytelniasz się w gcloud. Następnie może być konieczne przyznanie tożsamości roli użytkownika Vertex AI. Jeśli Twoja tożsamość ma rolę Właściciel, masz już rolę użytkownika Datastore. Jeśli nie, możesz uruchomić to polecenie, aby przyznać swojej tożsamości rolę użytkownika Vertex AI 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. Przyjmowanie tożsamości konta usługi na potrzeby ADC

Jeśli chcesz użyć konta usługi utworzonego w tym ćwiczeniu, Twoje konto użytkownika musi mieć rolę twórcy tokenów konta usługi. Aby uzyskać tę rolę, uruchom to polecenie:

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

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

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

Lokalne uruchamianie aplikacji

Następnie upewnij się, że znajdujesz się w katalogu głównym cloud-run-github-cd-demo dla tego ćwiczenia.

cd .. && pwd

Teraz zainstalujesz zależności.

npm install

Na koniec możesz uruchomić aplikację, wykonując ten skrypt. Ten skrypt wygeneruje też plik output.css z biblioteki tailwindCSS.

npm run dev

Teraz otwórz przeglądarkę i wpisz adres http://localhost:8080. Jeśli korzystasz z Cloud Shell, możesz otworzyć witrynę, klikając przycisk Podgląd w przeglądarce i wybierając opcję Podejrzyj port 8080.

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

Wpisz tekst w polach nazwy i miasta, a potem kliknij Zapisz. Następnie odśwież stronę. Zauważysz, że pole miasta nie zostało zachowane. W kolejnej sekcji naprawisz ten błąd.

Zatrzymaj aplikację Express działającą lokalnie (np. Ctrl^c na macOS).

8. Tworzenie repozytorium GitHub

W katalogu lokalnym utwórz nowe repozytorium z gałęzią domyślną o nazwie main.

git init
git branch -M main

Zatwierdź bieżącą bazę kodu zawierającą błąd. Błąd naprawisz po skonfigurowaniu ciągłego wdrażania.

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

Otwórz GitHub i utwórz puste repozytorium, które będzie prywatne lub publiczne. W tym ćwiczeniu w Codelabs zalecamy nadanie repozytorium nazwy cloud-run-auto-deploy-codelab Aby utworzyć puste repozytorium, pozostaw wszystkie ustawienia domyślne odznaczone lub ustawione na „brak”, tak aby po utworzeniu repozytorium nie zawierało ono domyślnie żadnych treści, np.

Ustawienia domyślne GitHub

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

Instrukcje dotyczące pustego repozytorium GitHub

Postępuj zgodnie z instrukcjami przesyłania istniejącego repozytorium z wiersza poleceń, wykonując te polecenia:

Najpierw dodaj repozytorium zdalne, wpisując

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

następnie wypchnij główną gałąź do repozytorium upstream.

git push -u origin main

9. Konfigurowanie ciągłego wdrażania

Teraz, gdy masz kod w GitHubie, możesz skonfigurować ciągłe wdrażanie. Otwórz Cloud Console dla Cloud Run.

  • Kliknij Utwórz usługę.
  • Kliknij Wdrażaj w sposób ciągły z repozytorium.
  • Kliknij SKONFIGURUJ CLOUD BUILD.
  • W sekcji Repozytorium źródłowe
    • Wybierz GitHub jako dostawcę repozytorium
    • Aby skonfigurować dostęp Cloud Build do repozytorium, kliknij Zarządzaj połączonymi repozytoriami.
    • Wybierz repozytorium i kliknij Dalej.
  • W sekcji Konfiguracja kompilacji
    • W polu Gałąź pozostaw wartość ^main$.
    • Jako typ kompilacji wybierz Go, Node.js, Python, Java, .NET Core, Ruby lub PHP za pomocą pakietów kompilacji Google Cloud.
  • Pozostaw pole Katalog kontekstu kompilacji z wartością /.
  • Kliknij Zapisz.
  • W sekcji Uwierzytelnianie
    • Kliknij Zezwalaj na nieuwierzytelnione wywołania.
  • W sekcji Kontenery, woluminy, sieć, zabezpieczenia
    • Na karcie Bezpieczeństwo wybierz konto usługi utworzone w poprzednim kroku, np.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. Naprawianie błędu

Napraw błąd w kodzie

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

zmień ten wiersz z

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

do

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

Sprawdź poprawkę, uruchamiając

npm run start

i otwórz przeglądarkę. Ponownie zapisz dane dla miasta i odśwież je. Po odświeżeniu zobaczysz, że nowo wprowadzone dane o mieście zostały prawidłowo zachowane.

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

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

a następnie wypchnij go do repozytorium nadrzędnego w GitHubie.

git push origin main

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

Sprawdź poprawkę w środowisku produkcyjnym

Gdy w Cloud Console dla usługi Cloud Run pojawi się informacja, że druga wersja obsługuje 100% ruchu (np. https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions), możesz otworzyć adres URL usługi Cloud Run w przeglądarce i sprawdzić, czy nowo wprowadzone dane o mieście są zachowywane po odświeżeniu strony.

11. Gratulacje!

Gratulujemy ukończenia ćwiczenia!

Zalecamy zapoznanie się z dokumentacją Cloud Run i ciągłego wdrażania z Git.

Omówione zagadnienia

  • Pisanie aplikacji internetowej Express za pomocą edytora Cloud Shell
  • Łączenie konta GitHub z Google Cloud w celu ciągłego wdrażania
  • Automatyczne wdrażanie aplikacji w Cloud Run
  • Dowiedz się, jak korzystać z HTMX i TailwindCSS

12. Czyszczenie danych

Aby uniknąć przypadkowych opłat (np. jeśli usługi Cloud Run zostaną przypadkowo wywołane więcej razy niż miesięczna liczba wywołań Cloud Run w bezpłatnej wersji), możesz usunąć Cloud Run lub projekt utworzony w kroku 2.

Aby usunąć usługę Cloud Run, otwórz konsolę Cloud Run w Google Cloud pod adresem https://console.cloud.google.com/run i usuń usługę Cloud Run utworzoną w tym ćwiczeniu, np. usuń usługę cloud-run-auto-deploy-codelab.

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