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
來查看可用專案的清單。