使用 Cloud Build 在 Cloud Run 進行持續部屬 (整合github)

閱讀時間約 29 分鐘

Overview

在本篇文章中,將會設定 Cloud Run,以便每當將程式修改並推送到 GitHub 時,它都會自動構建和部署應用程序的最新版本。

在本文中會將用戶數據保存到 Firestore,但是只有部分數據被正確保存。我們會修正錯誤的程式並推送到 GitHub ,Cloud Build 會自動地部屬更新過的新版本應用程式。

前置作業

  1. 啟用 Cloud Shell

有兩種方法。可以從 console 的右上角打開終端機或是到 https://shell.cloud.google.com/ 會打開純文字的 shell 介面(我比較喜歡這種)



  1. PROJECT 的設定和身分驗證
gcloud auth list
# 驗證身分
gcloud config list project
# 確認當下要使用的 PROJECT
gcloud config set project <PROJECT_ID>
# 更改要使用的 PROJECT

  1. 啟用所需的 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
  1. 建立 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

  1. 在 Firebase console, 點擊  Add project.
  2. 輸入 <YOUR_PROJECT_ID> ,接受 Firebase terms 規範
  3. 點擊 Continue.
  4. 點擊 Confirm Plan 來確定 Firebase 的 billing plan.
  5. Enable Google Analytics for this codelab (optional)
  6. 點擊 Add Firebase.
  7. project 建立完後, 點擊 Continue.
  8. 從左側 Build 清單, 點擊 Firestore database.
  9. 點擊 Create database.
  10. 選擇 region 後, 點擊 Next.
  11. 使用預設的 Start in production mode, 點擊 Create.


創建 application

  1. 建立並進入工作資料夾
mkdir cloud-run-github-cd-demo && cd $_
  1. 新增檔案 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

  1. 新增檔案 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

  1. 新增檔案 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
  1. 新增檔案 input.css
cat << EOF > input.css
@tailwind base;
@tailwind components;
@tailwind utilities;
EOF
  1. 新增檔案 tailwind.config.js
cat << EOF > tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.{html,js}"],
theme: {
extend: {}
},
plugins: []
};
EOF
  1. 新增檔案 .gitignore
cat << EOF > .gitignore
node_modules/

npm-debug.log
coverage/

package-lock.json

.DS_Store
EOF
  1. 建立子資料夾public並進入資料夾
mkdir public && cd public
  1. 新增檔案 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

在地端測試應用程式

  1. 給予 datastore.user 權限
USER=<YOUR_PRINCIPAL_EMAIL>

gcloud projects add-iam-policy-binding $PROJECT_ID \\
--member user:$USER \\
--role=roles/datastore.user
  1. 驗證身分,會進入到一個登入畫面,登入後複製驗證碼貼到終端機上
gcloud auth application-default login
  1. 執行應用程式
cd ~/cloud-run-github-cd-demo

npm install

npm run dev
raw-image
  1. 新增另外一個終端機使用指令確認應用程式成功運行
curl http://localhost:8080

新增 GitHub Repository

  1. 在 Cloud Shell 設定 Git (optional,第一次設定)
git config --global user.name <yujie70338>
git config --global user.email yujie70338@gmail.com
  1. 進行 Git 初始化,並下第一次的 commit
git init
git branch -M main
git add .
git commit -m "first commit for express application"

  1. GitHub 建立一個 repo
raw-image


  1. 設定地端 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
  1. 推送後的 repo

設定持續部屬 Continuous Deployment

  1. 前往  Cloud Console for Cloud Run 頁面
  2. 點擊 Create a Service
  3. 點擊 Continuously deploy from a repository
  4. 點擊 SET UP CLOUD BUILD.
  5. 在 Source repository 底下

-選擇 GitHub as the Repository Provider

-點擊 Manage connected repositories 以設定 Cloud Build 存取到 Github repo

-選擇剛建立的 repo 點擊 Next


  1. 在 Build Configuration 底下

-設定 Branch 為 ^main$

-設定 Build Type 為 Go, Node.js, Python, Java, .NET Core, Ruby or PHP via Google Cloud's buildpacks

  1. 設定 Build context directory 為 /
  2. 點擊 Save
  1. 在 Authentication 底下

-點擊 Allow unauthenticated invocations

  1. 在 Container(s), Volumes, Networking, Security 底下

點擊 Security tab, 選擇剛剛建立的 service account (Cloud Run access to Firestore)

  1. 點擊 CREATE



修正應用程式的 bug

  1. 修改 app.js (第29行)
 //TODO: fix this bug
await doc.set({
name: name
});


  1. 修改後 app.js
//fixed town bug
await doc.set({
name: name,
town: town
});

  1. 在地端進行測試
npm run start
  1. 測試成功後推送到 GitHub repo 的 main 分支
git add .
git commit -m "fixed town bug"

git push origin main

驗證自動部屬

  1. Cloud Run
  1. Cloud Build
raw-image
  1. 網頁檢查
raw-image


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

延伸閱讀

參考資料

avatar-img
18會員
41內容數
歡迎來到「Marcos的方格子」!目前在「Marcos談科技」撰寫在職涯上學習到的知識,在「Marcos談書」分享我在日常的閱讀和心得,歡迎您的到來!!
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Marcos的方格子 的其他內容
「Prompt hacking」與利用軟件漏洞的傳統駭客方法不同,Prompt hacking 是使用精心設計的提詞工程,並利用大型語言模型(Large Language Models, LLM)中的漏洞,使它們執行意外的操作或透露敏感信息。
人工智慧的出現協助企業創造下一波的成長紅利,但是也導致資安上的諸多挑戰,本篇整理 Best Practices for Securely Deploying AI on Google Cloud 和相關參考資料,希望藉由各種解決方案和最佳實踐,在使用人工智慧的同時也減少其帶來的安全性風險。
Google Cloud Professional Network Engineer 出題方向/學習資源/心得整理
本文介紹了 Google Cloud 的網絡服務層級之間的差異。讀者可以瞭解到 Google Cloud 的 VPC(Virtual Private Cloud)具有全球級的特點,以及 Premium Tier 和 Standard Tier 之間不同的差異。
在企業內部環境中,對服務和API的安全且高效率的存取至關重要。本文探討了GCP提供的 Private GoogleAccess、Private Service Connect、Serverless VPC Access、Private Services Access 的區別,以及它們如何使組織受益。
大型語言模型(Large Language Model,LLM)是一項人工智慧技術,其目的在於理解和生成人類語言,可將其想像成一種高階的「文字預測機器」,然而,它們並非真正理解語言。除了在上篇介紹的技巧可以協助我們在使用 LLM 時給予指示之外,今天我們會介紹使用 LLM 的框架。
「Prompt hacking」與利用軟件漏洞的傳統駭客方法不同,Prompt hacking 是使用精心設計的提詞工程,並利用大型語言模型(Large Language Models, LLM)中的漏洞,使它們執行意外的操作或透露敏感信息。
人工智慧的出現協助企業創造下一波的成長紅利,但是也導致資安上的諸多挑戰,本篇整理 Best Practices for Securely Deploying AI on Google Cloud 和相關參考資料,希望藉由各種解決方案和最佳實踐,在使用人工智慧的同時也減少其帶來的安全性風險。
Google Cloud Professional Network Engineer 出題方向/學習資源/心得整理
本文介紹了 Google Cloud 的網絡服務層級之間的差異。讀者可以瞭解到 Google Cloud 的 VPC(Virtual Private Cloud)具有全球級的特點,以及 Premium Tier 和 Standard Tier 之間不同的差異。
在企業內部環境中,對服務和API的安全且高效率的存取至關重要。本文探討了GCP提供的 Private GoogleAccess、Private Service Connect、Serverless VPC Access、Private Services Access 的區別,以及它們如何使組織受益。
大型語言模型(Large Language Model,LLM)是一項人工智慧技術,其目的在於理解和生成人類語言,可將其想像成一種高階的「文字預測機器」,然而,它們並非真正理解語言。除了在上篇介紹的技巧可以協助我們在使用 LLM 時給予指示之外,今天我們會介紹使用 LLM 的框架。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
PROCESS macro for SPSS 可以用非常簡單方式學會調節中介模式。本文將介紹四種類型的變項,並解釋調節式中介的公式,還有如何操作最4.0版本的PROCESS macro for SPSS。文末也會附上所有所有Process模型圖例,提供給讀者方便分析~
Thumbnail
PROCESS macro for SPSS 可以用非常簡單方式使用調節分析。本文將介紹三種類型的變項,還有如何操作最4.2版本的PROCESS macro for SPSS進行調節模式。文末也會附上所有所有Process模型圖例,提供給讀者方便分析~
Thumbnail
PROCESS macro for SPSS 可以用非常簡單方式進中介模式。本文將介紹三種類型的變項,還有如何操作最4.0版本的PROCESS macro for SPSS。文末也會附上所有所有Process模型圖例,提供給讀者方便分析~
Thumbnail
Potato Media雖然和方格子及Matters同樣歸類為寫作平台,同樣強調將內容變現,前者卻與後面兩者完全不同,當然,所獲得的收入報酬也不會一樣,更清楚一點來說,連獲得收益的方式也大不相同。
Thumbnail
我們將介紹各種類型的信度和統計方法,包含Cohen Kappa 係數、組內相關係數、α係數的SPSS教學。信度的可以使用不同的評估方法來評估。信度對於確定評分標準或量表的一致性和穩定度至關重要。
Thumbnail
如果依變項並非連續變項時,就可以改用羅吉斯迴歸。接下來本文將介紹勝算、勝算比、計算範例、二元/順序/多項式羅吉斯迴歸分析範例和SPSS操作方法。
Thumbnail
通常我們對於類別變項就直接看敘述統計大小,但如果我們想要用檢定確定兩者差距是達到統計顯著,就要用卡方檢定(Chi-square test)是一種統計學方法,獨立性考驗用於檢驗兩個類別變項各組別之間是否有顯著關聯。本文將介紹卡方檢定並介紹上機操作和事後比較方法。
Thumbnail
本篇介紹Mplus的「結構方程模型(Structural Equation Modelling, SEM)」之語法內容,並透過例題向大家示範如何分析撰寫SEM的語法。本文為新手教學,輸入方式可能不是最有效率,但是比較簡單且不太會犯錯
Thumbnail
當樣本有所關聯時,就不能使用獨立樣本t檢定,而是需要使用相依樣本t檢定,本文檢定介紹使用時機,並教導如何使用SPSS進行相依樣本t檢定
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
PROCESS macro for SPSS 可以用非常簡單方式學會調節中介模式。本文將介紹四種類型的變項,並解釋調節式中介的公式,還有如何操作最4.0版本的PROCESS macro for SPSS。文末也會附上所有所有Process模型圖例,提供給讀者方便分析~
Thumbnail
PROCESS macro for SPSS 可以用非常簡單方式使用調節分析。本文將介紹三種類型的變項,還有如何操作最4.2版本的PROCESS macro for SPSS進行調節模式。文末也會附上所有所有Process模型圖例,提供給讀者方便分析~
Thumbnail
PROCESS macro for SPSS 可以用非常簡單方式進中介模式。本文將介紹三種類型的變項,還有如何操作最4.0版本的PROCESS macro for SPSS。文末也會附上所有所有Process模型圖例,提供給讀者方便分析~
Thumbnail
Potato Media雖然和方格子及Matters同樣歸類為寫作平台,同樣強調將內容變現,前者卻與後面兩者完全不同,當然,所獲得的收入報酬也不會一樣,更清楚一點來說,連獲得收益的方式也大不相同。
Thumbnail
我們將介紹各種類型的信度和統計方法,包含Cohen Kappa 係數、組內相關係數、α係數的SPSS教學。信度的可以使用不同的評估方法來評估。信度對於確定評分標準或量表的一致性和穩定度至關重要。
Thumbnail
如果依變項並非連續變項時,就可以改用羅吉斯迴歸。接下來本文將介紹勝算、勝算比、計算範例、二元/順序/多項式羅吉斯迴歸分析範例和SPSS操作方法。
Thumbnail
通常我們對於類別變項就直接看敘述統計大小,但如果我們想要用檢定確定兩者差距是達到統計顯著,就要用卡方檢定(Chi-square test)是一種統計學方法,獨立性考驗用於檢驗兩個類別變項各組別之間是否有顯著關聯。本文將介紹卡方檢定並介紹上機操作和事後比較方法。
Thumbnail
本篇介紹Mplus的「結構方程模型(Structural Equation Modelling, SEM)」之語法內容,並透過例題向大家示範如何分析撰寫SEM的語法。本文為新手教學,輸入方式可能不是最有效率,但是比較簡單且不太會犯錯
Thumbnail
當樣本有所關聯時,就不能使用獨立樣本t檢定,而是需要使用相依樣本t檢定,本文檢定介紹使用時機,並教導如何使用SPSS進行相依樣本t檢定