Overview
在本篇文章中,將會設定 Cloud Run,以便每當將程式修改並推送到 GitHub 時,它都會自動構建和部署應用程序的最新版本。
在本文中會將用戶數據保存到 Firestore,但是只有部分數據被正確保存。我們會修正錯誤的程式並推送到 GitHub ,Cloud Build 會自動地部屬更新過的新版本應用程式。
前置作業
- 啟用 Cloud Shell
- PROJECT 的設定和身分驗證
gcloud auth list
# 驗證身分
gcloud config list project
# 確認當下要使用的 PROJECT
gcloud config set project <PROJECT_ID>
# 更改要使用的 PROJECT
- 啟用所需的 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
- 建立 Cloud Run 要使用的 service account
gcloud iam service-accounts create $SERVICE_ACCOUNT \\
--display-name="Cloud Run access to Firestore"
gcloud projects add-iam-policy-binding $PROJECT_ID \\
--member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \\
--role=roles/datastore.user
# grant the service account read and write access to Firestore
創建和設定一個 Firebase project
- 在 Firebase console, 點擊 Add project.
- 輸入 <YOUR_PROJECT_ID> ,接受 Firebase terms 規範
- 點擊 Continue.
- 點擊 Confirm Plan 來確定 Firebase 的 billing plan.
- Enable Google Analytics for this codelab (optional)
- 點擊 Add Firebase.
- project 建立完後, 點擊 Continue.
- 從左側 Build 清單, 點擊 Firestore database.
- 點擊 Create database.
- 選擇 region 後, 點擊 Next.
- 使用預設的 Start in production mode, 點擊 Create.
創建 application
- 建立並進入工作資料夾
mkdir cloud-run-github-cd-demo && cd $_
- 新增檔案 package.json
cat <<EOF >> 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"
}
}
EOF
- 新增檔案 app.js
cat <<EOF >> 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);
});
EOF
- 新增檔案 spinnerSvg.js
cat << EOF > 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>`;
EOF
- 新增檔案 input.css
cat << EOF > input.css
@tailwind base;
@tailwind components;
@tailwind utilities;
EOF
- 新增檔案 tailwind.config.js
cat << EOF > tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.{html,js}"],
theme: {
extend: {}
},
plugins: []
};
EOF
- 新增檔案 .gitignore
cat << EOF > .gitignore
node_modules/
npm-debug.log
coverage/
package-lock.json
.DS_Store
EOF
- 建立子資料夾public並進入資料夾
mkdir public && cd public
- 新增檔案 index.html
cat << EOF > index.html
<!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>
EOF
在地端測試應用程式
- 給予 datastore.user 權限
USER=<YOUR_PRINCIPAL_EMAIL>
gcloud projects add-iam-policy-binding $PROJECT_ID \\
--member user:$USER \\
--role=roles/datastore.user
- 驗證身分,會進入到一個登入畫面,登入後複製驗證碼貼到終端機上
gcloud auth application-default login
- 執行應用程式
cd ~/cloud-run-github-cd-demo
npm install
npm run dev

- 新增另外一個終端機使用指令確認應用程式成功運行
curl http://localhost:8080
新增 GitHub Repository
- 在 Cloud Shell 設定 Git (optional,第一次設定)
git config --global user.name <yujie70338>
git config --global user.email yujie70338@gmail.com
- 進行 Git 初始化,並下第一次的 commit
git init
git branch -M main
git add .
git commit -m "first commit for express application"
- 在 GitHub 建立一個 repo

- 設定地端 git 的 origin 為剛剛建立的 repo,並推送程式碼
# git remote add origin <https://github.com/yujie70338/cloud-run-auto-deploy-codelab.git> #使用https推送,輸入github帳密
git remote add origin git@github.com:yujie70338/cloud-run-auto-deploy-codelab.git #使用SSH推送,設定 SSH key
git push -u origin main
設定持續部屬 Continuous Deployment
- 前往 Cloud Console for Cloud Run 頁面
- 點擊 Create a Service
- 點擊 Continuously deploy from a repository
- 點擊 SET UP CLOUD BUILD.
- 在 Source repository 底下
-選擇 GitHub as the Repository Provider
-點擊 Manage connected repositories 以設定 Cloud Build 存取到 Github repo
-選擇剛建立的 repo 點擊 Next
- 在 Build Configuration 底下
-設定 Branch 為 ^main$
-設定 Build Type 為 Go, Node.js, Python, Java, .NET Core, Ruby or PHP via Google Cloud's buildpacks
- 設定 Build context directory 為
/
- 點擊 Save
- 在 Authentication 底下
-點擊 Allow unauthenticated invocations
- 在 Container(s), Volumes, Networking, Security 底下
點擊 Security tab, 選擇剛剛建立的 service account (Cloud Run access to Firestore)
- 點擊 CREATE
修正應用程式的 bug
- 修改
app.js
(第29行)
//TODO: fix this bug
await doc.set({
name: name
});
- 修改後
app.js
//fixed town bug
await doc.set({
name: name,
town: town
});
- 在地端進行測試
npm run start
- 測試成功後推送到 GitHub repo 的 main 分支
git add .
git commit -m "fixed town bug"
git push origin main
驗證自動部屬
- Cloud Run
- Cloud Build

- 網頁檢查

本文介紹了使用 Cloud Build 在 Cloud Run 進行持續部屬 (整合github),如果你喜歡這篇文章歡迎幫我按愛心鼓勵一下喔!~閱讀愉快!~