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 secara otomatis membangun dan men-deploy versi baru aplikasi 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 sedemikian rupa sehingga saat Anda mengirim perbaikan bug ke repositori GitHub, Anda akan otomatis melihat perbaikan tersebut tersedia di revisi baru.

Yang akan Anda pelajari

  • Menulis aplikasi web Express dengan Cloud Shell Editor
  • Hubungkan 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 pertama kalinya Anda memulai Cloud Shell, Anda akan melihat layar perantara yang menjelaskan apa itu Cloud Shell. Jika Anda melihat layar perantara, klik Lanjutkan.

d95252b003979716.png

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

7833d5e1c5d18f54.pngS

Mesin virtual ini dimuat dengan semua alat pengembangan yang diperlukan. Layanan 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 sudah 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 Add project.
  2. Masukkan <YOUR_PROJECT_ID> untuk menambahkan Firebase ke salah satu project Google Cloud Anda yang sudah ada
  3. Jika diminta, tinjau dan setujui persyaratan Firebase.
  4. Klik Lanjutkan.
  5. Klik Confirm Plan untuk mengonfirmasi paket penagihan Firebase.
  6. Mengaktifkan Google Analytics di codelab ini bersifat opsional.
  7. Klik Add Firebase.
  8. Setelah project dibuat, klik Continue.
  9. Dari menu Build, klik Firestore database.
  10. Klik Buat database.
  11. Pilih wilayah 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 {i>cd<i} ke direktori tersebut.

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

Lalu, 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 ini. 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 dengan nama 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>`;

Membuat file input.css untuk tailwindCSS

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

Dan membuat file tailwind.config.js untuk tailwindCSS

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

Lalu, buat file .gitignore.

node_modules/

npm-debug.log
coverage/

package-lock.json

.DS_Store

Sekarang, buat direktori public baru.

mkdir public
cd public

Dan dalam direktori publik tersebut, buat file index.html untuk frontend, 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 dalam aplikasi saat pengguna mencoba menyimpan data.

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

Menggunakan ADC saat berjalan secara lokal

Jika menjalankan Cloud Shell, berarti Anda sudah menjalankannya di virtual machine Google Compute Engine. Kredensial Anda yang terkait dengan virtual machine ini (seperti yang ditunjukkan dengan menjalankan gcloud auth list) akan otomatis digunakan oleh Application Default Credentials (ADC), sehingga Anda tidak perlu menggunakan perintah gcloud auth application-default login. Namun, identitas Anda masih memerlukan peran Datastore User. Anda dapat langsung melanjutkan ke bagian Menjalankan aplikasi secara lokal.

Namun, jika menjalankan di terminal lokal (bukan di Cloud Shell), Anda harus menggunakan Kredensial Default Aplikasi untuk melakukan autentikasi ke Google API. Anda dapat 1) login menggunakan kredensial (asalkan Anda memiliki peran Pengguna Datastore), atau 2) Anda dapat login dengan meniru 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 autentikasi Anda di gcloud. Selanjutnya, Anda mungkin perlu memberikan peran Vertex AI User ke identitas Anda. Jika identitas Anda memiliki peran Pemilik, berarti Anda sudah memiliki peran pengguna Datastore User ini. Jika tidak, Anda dapat menjalankan perintah ini untuk memberikan peran pengguna Vertex AI dan peran Datastore User kepada identitas Anda.

USER=<YOUR_PRINCIPAL_EMAIL>

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

Lalu jalankan perintah berikut

gcloud auth application-default login

Opsi 2) Meniru Identitas Akun Layanan untuk ADC

Jika Anda ingin menggunakan akun layanan yang dibuat di 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 utama 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 menghasilkan file output.css dari tailwindCSS.

npm run dev

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

pratinjau web - pratinjau pada tombol port 8080

Masukkan teks untuk kolom input nama dan kota, lalu tekan simpan. Lalu muat ulang halaman. Anda akan melihat bahwa {i>field<i} kota tidak ada lagi. Anda akan memperbaiki bug ini di bagian berikutnya.

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

8. Membuat Repositori GitHub

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

git init
git branch -M main

Commit codebase saat ini yang berisi bug. Anda akan memperbaiki bug setelah deployment berkelanjutan dikonfigurasi.

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

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

Setelan default GitHub

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

Kosongkan petunjuk repo GitHub

Anda akan mengikuti petunjuk push 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 mengirim cabang utama ke repositori upstream.

git push -u origin main

9. Siapkan Deployment Berkelanjutan

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

  • Klik Create a Service
  • Klik Continuously deploy from a repository
  • Klik SIAPKAN CLOUD BUILD.
  • Di bagian Repositori sumber
    • Pilih GitHub sebagai Penyedia Repositori
    • Klik Manage Connected repository untuk mengonfigurasi akses Cloud Build ke repo
    • Pilih repositori Anda, lalu klik Next
  • Di bagian Konfigurasi Build
    • Keluar dari Cabang sebagai ^main$
    • Untuk Build Type, pilih Go, Node.js, Python, Java, .NET Core, Ruby, atau PHP melalui buildpack Google Cloud
  • Biarkan direktori konteks Build sebagai /
  • Klik Simpan
  • Pada Autentikasi
    • Klik Allow unauthenticated invocations
  • Di bagian Penampung, Volume, Jaringan, Keamanan
    • Pada tab Keamanan, pilih akun layanan yang Anda buat di langkah sebelumnya, misalnya Cloud Run access to Firestore
  • Klik BUAT

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

10. Memperbaiki bug

Memperbaiki 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 membuka {i>browser<i} web. Simpan data lagi untuk kota, dan muat ulang. Anda akan melihat data kota yang baru dimasukkan telah dipertahankan dengan benar saat dimuat ulang.

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

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

dan kemudian mengirimkannya ke repositori upstream di GitHub.

git push origin main

Cloud Build akan men-deploy perubahan Anda secara otomatis. 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 menampilkan revisi ke-2 kini melayani 100% traffic, misalnya Dengan https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions, Anda dapat membuka URL layanan Cloud Run di browser dan memastikan bahwa data kota yang baru dimasukkan tetap ada setelah memuat ulang halaman.

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
  • Hubungkan 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 tagihan yang tidak disengaja, (misalnya, jika layanan Cloud Run secara tidak sengaja dipanggil lebih sering daripada alokasi panggilan Cloud Run bulanan Anda di paket gratis), Anda dapat menghapus Cloud Run atau menghapus project yang Anda buat di Langkah 2.

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

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