1. 簡介
總覽
在本程式碼研究室中,您將設定 Cloud Run,在每次將原始碼變更推送至 GitHub 存放區時,自動建構及部署新版應用程式。
這個示範應用程式會將使用者資料儲存到 Firestore,但正確儲存僅有部分資料。您可以設定持續部署作業,這樣一來,當您將錯誤修正推送至 GitHub 存放區時,系統會自動在新修訂版本中看見修正內容。
課程內容
- 使用 Cloud Shell 編輯器編寫 Express 網頁應用程式
- 將 GitHub 帳戶連結至 Google Cloud,即可持續部署
- 將應用程式自動部署至 Cloud Run
- 瞭解如何使用 HTMX 和 TailwindCSS
2. 設定和需求
必要條件
- 您擁有 GitHub 帳戶,並熟悉建立程式碼並將程式碼推送至存放區。
- 您已登入 Cloud 控制台。
- 先前已部署 Cloud Run 服務。舉例來說,您可以按照從原始碼部署網路服務的快速入門導覽課程著手。
啟用 Cloud Shell
- 在 Cloud 控制台中,按一下「啟用 Cloud Shell」圖示
。

如果您是第一次啟動 Cloud Shell,系統會顯示中繼畫面,說明這項服務的內容。如果系統顯示中繼畫面,請按一下「繼續」。

佈建並連線至 Cloud Shell 只需幾分鐘的時間。

這個虛擬機器已載入所有必要的開發工具。提供永久的 5 GB 主目錄,而且在 Google Cloud 中運作,大幅提高網路效能和驗證能力。在本程式碼研究室中,您的大部分作業都可透過瀏覽器完成。
連線至 Cloud Shell 後,您應會發現自己通過驗證,且專案已設為您的專案 ID。
- 在 Cloud Shell 中執行下列指令,確認您已通過驗證:
gcloud auth list
指令輸出
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- 在 Cloud Shell 中執行下列指令,確認 gcloud 指令知道您的專案:
gcloud config list project
指令輸出
[core] project = <PROJECT_ID>
如果尚未設定,請使用下列指令進行設定:
gcloud config set project <PROJECT_ID>
指令輸出
Updated property [core/project].
3. 啟用 API 並設定環境變數
啟用 API
本程式碼研究室需要使用下列 API。您可以執行下列指令來啟用這些 API:
gcloud services enable run.googleapis.com \
cloudbuild.googleapis.com \
firestore.googleapis.com \
iamcredentials.googleapis.com
設定環境變數
您可以設定將在本程式碼研究室中使用的環境變數。
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. 建立服務帳戶
Cloud Run 會使用這個服務帳戶呼叫 Vertex AI Gemini API。這個服務帳戶也有權讀取、寫入 Firestore,以及讀取 Secret Manager 的密鑰。
首先,請執行下列指令來建立服務帳戶:
gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run access to Firestore"
接著,將 Firestore 的讀取及寫入權限授予服務帳戶。
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/datastore.user
5. 建立及設定 Firebase 專案
- 在 Firebase 控制台,按一下「新增專案」。
- 輸入 <YOUR_PROJECT_ID>,將 Firebase 新增至現有的其中一項 Google Cloud 專案
- 如果出現提示訊息,請詳閱並接受 Firebase 條款。
- 按一下「繼續」。
- 按一下「確認方案」即可確認訂閱 Firebase 計費方案。
- 您可以選擇在本程式碼研究室中啟用 Google Analytics。
- 按一下「新增 Firebase」。
- 專案建立完成後,按一下「Continue」。
- 在「建構」選單中,按一下「Firestore 資料庫」。
- 按一下 [Create database] (建立資料庫)。
- 從「位置」下拉式選單中選擇您的區域,然後點選「下一步」。
- 使用預設的「在正式環境中啟動」,然後點選「建立」。
6. 編寫應用程式
首先,建立原始碼的目錄,並以 cd 指向該目錄。
mkdir cloud-run-github-cd-demo && cd $_
接著,建立含有以下內容的 package.json 檔案:
{
"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"
}
}
首先,請建立含有以下內容的 app.js 來源檔案。此檔案包含服務的進入點,且包含應用程式的主要邏輯。
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);
});
建立名為 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>`;
為 tailwindCSS 建立 input.css 檔案
@tailwind base; @tailwind components; @tailwind utilities;
並建立 tailwindCSS 的 tailwind.config.js 檔案
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.{html,js}"],
theme: {
extend: {}
},
plugins: []
};
並建立 .gitignore 檔案。
node_modules/ npm-debug.log coverage/ package-lock.json .DS_Store
現在,建立新的 public 目錄。
mkdir public cd public
在該公開目錄中,為前端建立 index.html 檔案,用來使用 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. 在本機執行應用程式
在本節中,您將在本機執行應用程式,確認使用者嘗試儲存資料時,應用程式存在錯誤。
首先,您必須具備 Datastore 使用者角色,才能存取 Firestore (如果使用身分進行驗證,例如透過 Cloud Shell 執行),也可以模擬先前建立的使用者帳戶。
在本機執行時使用 ADC
如果您是在 Cloud Shell 中執行,代表已經在 Google Compute Engine 虛擬機器上執行。應用程式預設憑證 (ADC) 會自動使用與這個虛擬機器相關聯的憑證 (由執行 gcloud auth list 顯示的憑證),因此您無需使用 gcloud auth application-default login 指令。不過,您的身分仍需具備 Datastore User 角色。請直接跳到「在本機執行應用程式」一節。
不過,如果您是在本機終端機 (而非 Cloud Shell 中) 執行應用程式,則必須使用應用程式預設憑證向 Google API 進行驗證。您可以使用憑證 (前提是您已具備 Datastore 使用者角色) 登入;或者,您可以模擬在本程式碼研究室中使用的服務帳戶,藉此登入。
做法 1) 針對 ADC 使用憑證
如要使用憑證,可以先執行 gcloud auth list 以驗證您在 gcloud 中的驗證方式。接下來,您可能需要向身分授予「Vertex AI 使用者」角色。如果您的身分具備「擁有者」角色,則您已經擁有這個 Datastore 使用者角色。如果沒有,您可以執行以下指令,授予身分識別 Vertex AI 使用者角色和 Datastore 使用者角色。
USER=<YOUR_PRINCIPAL_EMAIL> gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/datastore.user
接著執行下列指令
gcloud auth application-default login
選項 2) 模擬 ADC 的服務帳戶
如要使用在本程式碼研究室中建立的服務帳戶,您的使用者帳戶必須具備服務帳戶權杖建立者角色。如要取得這個角色,請執行下列指令:
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/iam.serviceAccountTokenCreator
接著執行下列指令,以透過服務帳戶使用 ADC
gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS
在本機執行應用程式
接下來,請確認您位於程式碼研究室的根目錄 cloud-run-github-cd-demo。
cd .. && pwd
現在您將安裝依附元件。
npm install
最後,您可以執行以下指令來啟動應用程式。這個指令碼也會產生 tailwindCSS 的 output.css 檔案。
npm run dev
現在,開啟網路瀏覽器並前往 http://localhost:8080。如果您使用 Cloud Shell,可以開啟網站,方法是開啟「網頁預覽」按鈕,然後選取「預覽通訊埠 8080」。

輸入名稱和城鎮輸入欄位的文字,然後按一下「儲存」。然後重新整理頁面。您會發現鎮區欄位並未保留。您將在後續章節中修正此錯誤。
停止本機執行速成應用程式 (例如在 MacOS 上為 Ctrl^c 鍵)。
8. 建立 GitHub 存放區
在本機目錄中,建立以 main 為預設分支版本名稱的新存放區。
git init git branch -M main
提交包含錯誤的現有程式碼集。設定持續部署之後,您將修正錯誤。
git add . git commit -m "first commit for express application"
前往 GitHub 建立僅限您本人或公開的空白存放區。本程式碼研究室建議為存放區命名 cloud-run-auto-deploy-codelab 如要建立空白的存放區,您會不要勾選任何預設設定或設為無,以免根據預設,存放區中沒有任何內容會在建立時位於存放區。舉例來說,

如果您正確完成這個步驟,存放區頁面會顯示下列操作說明:

您將執行下列指令,依照透過指令列推送現有存放區的操作說明:
首先,執行下列指令來新增遠端存放區
git remote add origin <YOUR-REPO-URL-PER-GITHUB-INSTRUCTIONS>
然後將主要分支版本推送到上游存放區。
git push -u origin main
9. 設定持續部署
您在 GitHub 中已有程式碼,可以設定持續部署。前往 Cloud Run 適用的 Cloud 控制台。
- 按一下「建立服務」
- 按一下「持續從存放區部署」
- 按一下「設定雲端建構」。
- 位於原始碼存放區
- 選取 GitHub 做為存放區供應商
- 點選「管理已連結的存放區」,設定存放區的 Cloud Build 存取權
- 選取您的存放區,然後點選「Next」(下一步)。
- 在「Build Configuration」(建構設定)
- 下
- 將分支版本保留為 ^main$
- 在「建構類型」部分,請選取「Go、Node.js、Python、Java、.NET Core、Ruby 或 PHP (透過 Google Cloud 的 buildpacks」)
- 將 Build 結構定義目錄保留為
/ - 點選「儲存」。
- 在「驗證」下
- 按一下「允許未經驗證的叫用」。
- 在「容器」、「磁碟區」、「網路」、「安全性」下
- 在「安全性」分頁下方,選取先前步驟中建立的服務帳戶 (例如
Cloud Run access to Firestore
- 在「安全性」分頁下方,選取先前步驟中建立的服務帳戶 (例如
- 按一下 [Create]
這麼做會部署 Cloud Run 服務,當中含有您將在下一節中修正的錯誤。
10. 修正錯誤
修正程式碼中的錯誤
在 Cloud Shell 編輯器中,開啟 app.js 檔案,然後找到顯示 //TODO: fix this bug 的註解
將下列程式碼從
//TODO: fix this bug
await doc.set({
name: name
});
到
//fixed town bug
await doc.set({
name: name,
town: town
});
執行下列指令以驗證修正結果
npm run start
然後開啟網路瀏覽器。再次儲存城鎮資料,然後重新整理。重新整理後,新輸入的城鎮資料就會持續正確。
驗證修正結果後,就可以部署內容了。首先,提交修正
git add . git commit -m "fixed town bug"
然後推送至 GitHub 的上游存放區。
git push origin main
Cloud Build 會自動部署變更。您可以前往 Cloud Run 服務的 Cloud Run 服務,監控部署作業的變更。
在實際工作環境中驗證修正結果
Cloud Run 服務的 Cloud 控制台顯示第 2 個修訂版本後,現在就會處理 100% 的流量,例如:https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions,您可以在瀏覽器中開啟 Cloud Run 服務網址,並在重新整理頁面後,驗證新輸入的城鎮資料會保留下來。
11. 恭喜!
恭喜您完成本程式碼研究室!
建議您查看 Cloud Run 的說明文件和透過 Git 進行持續部署的說明文件。
涵蓋內容
- 使用 Cloud Shell 編輯器編寫 Express 網頁應用程式
- 將 GitHub 帳戶連結至 Google Cloud,即可持續部署
- 將應用程式自動部署至 Cloud Run
- 瞭解如何使用 HTMX 和 TailwindCSS
12. 清除所用資源
為避免產生意外費用 (舉例來說,如果 Cloud Run 服務意外叫用次數超過免費方案的每月 Cloud Run 叫用分配數量),您可以刪除 Cloud Run 或刪除步驟 2 中建立的專案。
如要刪除 Cloud Run 服務,請前往 https://console.cloud.google.com/run 的 Cloud Run Cloud 控制台,然後刪除您在本程式碼研究室中建立的 Cloud Run 服務,例如:刪除 cloud-run-auto-deploy-codelab 服務。
如果選擇刪除整個專案,您可以前往 https://console.cloud.google.com/cloud-resource-manager,選取您在步驟 2 建立的專案,然後選擇「刪除」。如果刪除專案,您必須變更 Cloud SDK 中的專案。您可以執行 gcloud projects list 來查看可用專案的清單。