Cara men-deploy perubahan secara otomatis dari GitHub ke Cloud Run menggunakan Cloud Build

1. Pengantar

Ringkasan

Dalam codelab ini, Anda akan mengonfigurasi Cloud Run untuk mem-build dan men-deploy secara otomatis versi baru aplikasi Anda setiap kali Anda mengirim perubahan kode sumber ke repositori GitHub.

Aplikasi demo ini menyimpan data pengguna ke Firestore, tetapi hanya sebagian data yang disimpan dengan benar. Anda akan mengonfigurasi deployment berkelanjutan sehingga saat Anda mengirim perbaikan bug ke repositori GitHub, Anda akan otomatis melihat perbaikan tersebut tersedia dalam revisi baru.

Yang akan Anda pelajari

  • Menulis aplikasi web Express dengan Cloud Shell Editor
  • Menghubungkan akun GitHub Anda ke Google Cloud untuk deployment berkelanjutan
  • Men-deploy aplikasi Anda secara otomatis ke Cloud Run
  • Pelajari cara menggunakan HTMX dan TailwindCSS

2. Penyiapan dan Persyaratan

Prasyarat

Mengaktifkan Cloud Shell

  1. Dari Cloud Console, klik Aktifkan Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Jika ini adalah pertama kalinya Anda memulai Cloud Shell, Anda akan melihat layar perantara yang menjelaskan apa itu Cloud Shell. Jika Anda melihat layar perantara, klik Continue.

d95252b003979716.png

Perlu waktu beberapa saat untuk menyediakan dan terhubung ke Cloud Shell.

7833d5e1c5d18f54.png

Virtual machine ini dilengkapi dengan semua alat pengembangan yang diperlukan. VM ini menawarkan direktori beranda tetap sebesar 5 GB dan beroperasi di Google Cloud, sehingga sangat meningkatkan performa dan autentikasi jaringan. Sebagian besar pekerjaan Anda dalam codelab ini dapat dilakukan dengan browser.

Setelah terhubung ke Cloud Shell, Anda akan melihat bahwa Anda telah diautentikasi dan project telah ditetapkan ke project ID Anda.

  1. Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa Anda telah diautentikasi:
gcloud auth list

Output perintah

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa perintah gcloud mengetahui project Anda:
gcloud config list project

Output perintah

[core]
project = <PROJECT_ID>

Jika tidak, Anda dapat menyetelnya dengan perintah ini:

gcloud config set project <PROJECT_ID>

Output perintah

Updated property [core/project].

3. Mengaktifkan API dan Menetapkan Variabel Lingkungan

Mengaktifkan API

Codelab ini memerlukan penggunaan API berikut. Anda dapat mengaktifkan API tersebut dengan menjalankan perintah berikut:

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

Menyiapkan variabel lingkungan

Anda dapat menetapkan variabel lingkungan yang akan digunakan di seluruh codelab ini.

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. Membuat akun layanan

Akun layanan ini akan digunakan oleh Cloud Run untuk memanggil Vertex AI Gemini API. Akun layanan ini juga akan memiliki izin untuk membaca dan menulis ke Firestore serta membaca secret dari Secret Manager.

Pertama, buat akun layanan dengan menjalankan perintah ini:

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

Sekarang, beri akun layanan akses baca dan tulis ke Firestore.

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

5. Membuat dan mengonfigurasi project Firebase

  1. Di Firebase console, klik Tambahkan project.
  2. Masukkan <YOUR_PROJECT_ID> untuk menambahkan Firebase ke salah satu project Google Cloud yang ada
  3. Jika diminta, tinjau dan setujui persyaratan Firebase.
  4. Klik Lanjutkan.
  5. Klik Confirm Plan untuk mengonfirmasi paket penagihan Firebase.
  6. Anda dapat memilih untuk Mengaktifkan Google Analytics untuk codelab ini.
  7. Klik Add Firebase.
  8. Setelah project dibuat, klik Lanjutkan.
  9. Dari menu Build, klik Firestore database.
  10. Klik Create database.
  11. Pilih wilayah Anda dari drop-down Lokasi, lalu klik Berikutnya.
  12. Gunakan Mulai dalam mode produksi default, lalu klik Buat.

6. Menulis aplikasi

Pertama, buat direktori untuk kode sumber dan cd ke direktori tersebut.

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

Kemudian, buat file package.json dengan konten berikut:

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

Pertama, buat file sumber app.js dengan konten di bawah. File ini berisi titik entri untuk layanan dan berisi logika utama untuk aplikasi.

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

Buat file bernama 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>`;

Buat file input.css untuk tailwindCSS

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

Lalu, buat file tailwind.config.js untuk tailwindCSS

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

Buat file .gitignore.

node_modules/

npm-debug.log
coverage/

package-lock.json

.DS_Store

Sekarang, buat direktori public baru.

mkdir public
cd public

Di dalam direktori publik tersebut, buat file index.html untuk front-end, yang akan menggunakan 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. Menjalankan aplikasi secara lokal

Di bagian ini, Anda akan menjalankan aplikasi secara lokal untuk mengonfirmasi bahwa ada bug di aplikasi saat pengguna mencoba menyimpan data.

Pertama, Anda harus memiliki peran Pengguna Datastore untuk mengakses Firestore (jika menggunakan identitas Anda untuk autentikasi, misalnya Anda menjalankan di Cloud Shell) atau Anda dapat meniru akun pengguna yang dibuat sebelumnya.

Menggunakan ADC saat berjalan secara lokal

Jika Anda menjalankan di Cloud Shell, Anda sudah menjalankan di virtual machine Google Compute Engine. Kredensial Anda yang terkait dengan mesin virtual ini (seperti yang ditunjukkan dengan menjalankan gcloud auth list) akan otomatis digunakan oleh Kredensial Default Aplikasi (ADC), sehingga Anda tidak perlu menggunakan perintah gcloud auth application-default login. Namun, identitas Anda tetap memerlukan peran Pengguna Datastore. Anda dapat langsung membuka bagian Jalankan aplikasi secara lokal.

Namun, jika Anda menjalankan di terminal lokal (yaitu, bukan di Cloud Shell), Anda harus menggunakan Kredensial Default Aplikasi untuk melakukan autentikasi ke Google API. Anda dapat 1) login menggunakan kredensial Anda (asalkan Anda memiliki peran Pengguna Datastore) atau 2) Anda dapat login dengan meniru identitas akun layanan yang digunakan dalam codelab ini.

Opsi 1) Menggunakan kredensial Anda untuk ADC

Jika ingin menggunakan kredensial, Anda dapat menjalankan gcloud auth list terlebih dahulu untuk memverifikasi cara Anda diautentikasi di gcloud. Selanjutnya, Anda mungkin perlu memberikan peran Vertex AI User ke identitas Anda. Jika identitas Anda memiliki peran Pemilik, Anda sudah memiliki peran pengguna Datastore User ini. Jika tidak, Anda dapat menjalankan perintah ini untuk memberikan peran pengguna Vertex AI dan peran Pengguna Datastore ke identitas Anda.

USER=<YOUR_PRINCIPAL_EMAIL>

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

Kemudian, jalankan perintah berikut

gcloud auth application-default login

Opsi 2) Meniru Identitas Akun Layanan untuk ADC

Jika Anda ingin menggunakan akun layanan yang dibuat dalam codelab ini, akun pengguna Anda harus memiliki peran Service Account Token Creator. Anda dapat memperoleh peran ini dengan menjalankan perintah berikut:

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

Selanjutnya, Anda akan menjalankan perintah berikut untuk menggunakan ADC dengan akun layanan

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

Menjalankan aplikasi secara lokal

Selanjutnya, pastikan Anda berada di direktori root cloud-run-github-cd-demo untuk codelab Anda.

cd .. && pwd

Sekarang, Anda akan menginstal dependensi.

npm install

Terakhir, Anda dapat memulai aplikasi dengan menjalankan skrip berikut. Skrip ini juga akan membuat file output.css dari tailwindCSS.

npm run dev

Sekarang buka browser web Anda ke http://localhost:8080. Jika Anda berada di Cloud Shell, Anda dapat membuka situs dengan membuka tombol Web Preview dan memilih Preview Port 8080.

tombol pratinjau web - pratinjau di port 8080

Masukkan teks untuk kolom input nama dan kota, lalu klik simpan. Kemudian, muat ulang halaman. Anda akan melihat bahwa kolom kota tidak tetap ada. Anda akan memperbaiki bug ini di bagian berikutnya.

Hentikan aplikasi Express agar tidak berjalan secara lokal (misalnya, Ctrl^c di MacOS).

8. Membuat Repositori GitHub

Di direktori lokal, buat repo baru dengan main sebagai nama cabang default.

git init
git branch -M main

Lakukan commit pada codebase saat ini yang berisi bug. Anda akan memperbaiki bug setelah continuous deployment dikonfigurasi.

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

Buka GitHub dan buat repositori kosong yang bersifat pribadi atau publik. Codelab ini merekomendasikan penamaan repositori Anda dengan cloud-run-auto-deploy-codelab Untuk membuat repositori kosong, Anda harus membiarkan semua setelan default tidak dicentang atau ditetapkan ke tidak ada sehingga tidak ada konten dalam repositori secara default saat dibuat, misalnya

Setelan default GitHub

Jika Anda menyelesaikan langkah ini dengan benar, Anda akan melihat petunjuk berikut di halaman repositori kosong:

Petunjuk repo GitHub kosong

Anda akan mengikuti petunjuk mendorong repositori yang ada dari command line dengan menjalankan perintah berikut:

Pertama, tambahkan repositori jarak jauh dengan menjalankan

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

lalu kirim cabang utama ke repo upstream.

git push -u origin main

9. Menyiapkan Continuous Deployment

Setelah memiliki kode di GitHub, Anda dapat menyiapkan deployment berkelanjutan. Buka Cloud Console untuk Cloud Run.

  • Klik Buat Layanan
  • Klik Deploy terus-menerus dari repositori
  • Klik SIAPKAN CLOUD BUILD.
  • Di bagian Repositori sumber
    • Pilih GitHub sebagai Penyedia Repositori
    • Klik Manage connected repositories untuk mengonfigurasi akses Cloud Build ke repositori
    • Pilih repositori Anda, lalu klik Berikutnya
  • Di bagian Konfigurasi Build
    • Biarkan Cabang sebagai ^main$
    • Untuk Jenis Build, pilih Go, Node.js, Python, Java, .NET Core, Ruby, atau PHP melalui buildpack Google Cloud
  • Biarkan Build context directory sebagai /
  • Klik Simpan
  • Di bagian Authentication
    • Klik Izinkan pemanggilan yang tidak diautentikasi
  • Di bagian Container(s), Volumes, Networking, Security
    • Di tab Keamanan, pilih akun layanan yang Anda buat pada langkah sebelumnya, misalnya Cloud Run access to Firestore
  • Klik BUAT

Tindakan ini akan men-deploy layanan Cloud Run yang berisi bug yang akan Anda perbaiki di bagian berikutnya.

10. Memperbaiki bug

Perbaiki bug dalam kode

Di Cloud Shell Editor, buka file app.js dan buka komentar yang bertuliskan //TODO: fix this bug

ubah baris berikut dari

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

hingga

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

Verifikasi perbaikan dengan menjalankan

npm run start

dan buka browser web Anda. Simpan data lagi untuk kota, lalu muat ulang. Anda akan melihat data kota yang baru dimasukkan tetap ada dengan benar saat dimuat ulang.

Setelah memverifikasi perbaikan, Anda siap men-deploy-nya. Pertama, lakukan perbaikan.

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

lalu kirim ke repositori upstream di GitHub.

git push origin main

Cloud Build akan otomatis men-deploy perubahan Anda. Anda dapat membuka Konsol Cloud untuk layanan Cloud Run guna memantau perubahan deployment.

Memverifikasi perbaikan dalam produksi

Setelah Konsol Cloud untuk layanan Cloud Run Anda menunjukkan bahwa revisi ke-2 kini menyalurkan 100% traffic, misalnya https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions, Anda dapat membuka URL layanan Cloud Run di browser dan memverifikasi bahwa data kota yang baru dimasukkan tetap ada setelah halaman dimuat ulang.

11. Selamat!

Selamat, Anda telah menyelesaikan codelab.

Sebaiknya tinjau dokumentasi Cloud Run dan deployment berkelanjutan dari git.

Yang telah kita bahas

  • Menulis aplikasi web Express dengan Cloud Shell Editor
  • Menghubungkan akun GitHub Anda ke Google Cloud untuk deployment berkelanjutan
  • Men-deploy aplikasi Anda secara otomatis ke Cloud Run
  • Pelajari cara menggunakan HTMX dan TailwindCSS

12. Pembersihan

Untuk menghindari biaya yang tidak disengaja (misalnya, jika layanan Cloud Run tidak sengaja dipanggil lebih banyak daripada alokasi pemanggilan Cloud Run bulanan Anda di tingkat gratis), Anda dapat menghapus Cloud Run atau menghapus project yang Anda buat di Langkah 2.

Untuk menghapus layanan Cloud Run, buka Konsol Cloud Run di https://console.cloud.google.com/run dan hapus layanan Cloud Run yang Anda buat dalam codelab ini, misalnya, hapus layanan cloud-run-auto-deploy-codelab.

Jika Anda memilih untuk menghapus seluruh project, Anda dapat membuka https://console.cloud.google.com/cloud-resource-manager, memilih project yang Anda buat di Langkah 2, lalu memilih Hapus. Jika menghapus project, Anda harus mengubah project di Cloud SDK. Anda dapat melihat daftar semua project yang tersedia dengan menjalankan gcloud projects list.