1. 简介
概览
在此 Codelab 中,您将配置 Cloud Run,以便在您每次将源代码更改推送到 GitHub 代码库时,自动构建和部署应用的新版本。
此演示应用会将用户数据保存到 Firestore,但只有部分数据能正确保存。您将配置持续部署,以便在将 bug 修复推送到 GitHub 代码库时,自动看到该修复在新的修订版本中生效。
学习内容
- 使用 Cloud Shell Editor 编写 Express Web 应用
- 将您的 GitHub 账号与 Google Cloud 相关联,以实现持续部署
- 自动将应用部署到 Cloud Run
- 了解如何使用 HTMX 和 TailwindCSS
2. 设置和要求
前提条件
- 您拥有 GitHub 账号,并且熟悉如何创建代码并将其推送到代码库。
- 您已登录 Cloud 控制台。
- 您之前已部署过 Cloud Run 服务。例如,您可以按照从源代码部署 Web 服务快速入门中的说明开始操作。
激活 Cloud Shell
- 在 Cloud Console 中,点击激活 Cloud Shell
。

如果您是第一次启动 Cloud Shell,系统会显示一个介绍其功能的过渡页面。如果您看到了过渡页面,请点击继续。

预配和连接到 Cloud Shell 只需花几分钟时间。

此虚拟机已加载所需的所有开发工具。它提供了一个持久的 5 GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。只需使用一个浏览器即可完成本 Codelab 中的大部分工作。
连接到 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
此 Codelab 需要使用以下 API。您可以运行以下命令来启用这些 API:
gcloud services enable run.googleapis.com \
cloudbuild.googleapis.com \
firestore.googleapis.com \
iamcredentials.googleapis.com
设置环境变量
您可以设置将在本 Codelab 中全程使用的环境变量。
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 结算方案。
- 您可以选择是否为此 Codelab 启用 Google Analytics。
- 点击添加 Firebase。
- 项目创建完毕后,点击继续。
- 在构建菜单中,点击 Firestore 数据库。
- 点击创建数据库。
- 从位置下拉菜单中选择您所在的区域,然后点击下一步。
- 使用默认的以生产模式开始,然后点击创建。
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
在该 public 目录中,为将使用 htmx 的前端创建 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>
7. 在本地运行应用
在本部分中,您将在本地运行应用,以确认当用户尝试保存数据时,应用中存在 bug。
首先,您需要拥有 Datastore User 角色才能访问 Firestore(如果您使用自己的身份进行身份验证,例如在 Cloud Shell 中运行),或者您可以模拟之前创建的用户账号。
在本地运行时使用 ADC
如果您在 Cloud Shell 中运行,则表示您已在 Google Compute Engine 虚拟机上运行。与此虚拟机关联的凭据(如运行 gcloud auth list 所示)将由应用默认凭证 (ADC) 自动使用,因此无需使用 gcloud auth application-default login 命令。不过,您的身份仍需要具有 Datastore 用户角色。您可以跳至在本地运行应用部分。
不过,如果您在本地终端(即不在 Cloud Shell 中)上运行,则需要使用应用默认凭据向 Google API 进行身份验证。您可以选择以下任一方式登录:1) 使用您的凭据登录(前提是您具有 Datastore User 角色);2) 通过模拟此 Codelab 中使用的服务账号登录。
方法 1)使用您的凭据进行 ADC
如果您想使用自己的凭据,可以先运行 gcloud auth list 来验证您在 gcloud 中的身份验证方式。接下来,您可能需要向您的身份授予 Vertex AI User 角色。如果您的身份具有 Owner 角色,则您已拥有此 Datastore User 用户角色。如果不是,您可以运行以下命令,为您的身份授予 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
如果您想使用在此 Codelab 中创建的服务账号,您的用户账号需要具有 Service Account Token Creator 角色。您可以通过运行以下命令来获取此角色:
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
在本地运行应用
接下来,请确保您位于 Codelab 的根目录 cloud-run-github-cd-demo 中。
cd .. && pwd
现在,您将安装依赖项。
npm install
最后,您可以运行以下脚本来启动应用。此脚本还会从 tailwindCSS 生成 output.css 文件。
npm run dev
现在,打开网络浏览器并前往 http://localhost:8080。如果您在 Cloud Shell 中,可以打开“网页预览”按钮,然后选择“预览端口 8080”来打开该网站。
在“姓名”和“城镇”输入字段中输入文本,然后点击“保存”。然后刷新页面。您会发现,城镇字段未保留。您将在后续部分中修复此 bug。
停止在本地运行的 Express 应用(例如,在 macOS 上按 Ctrl^c)。
8. 创建 GitHub 代码库
在本地目录中,创建一个以 main 作为默认分支名称的新代码库。
git init git branch -M main
提交包含 bug 的当前代码库。您将在配置持续部署后修复该 bug。
git add . git commit -m "first commit for express application"
前往 GitHub,然后创建一个仅供您自己访问或公开的空代码库。此 Codelab 建议将代码库命名为 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 控制台。
- 点击“创建服务”
- 点击从代码库持续部署
- 点击设置 Cloud Build。
- 在“源代码库”下
- 选择 GitHub 作为代码库提供方
- 点击管理关联的代码库,以配置 Cloud Build 对代码库的访问权限
- 选择您的代码库,然后点击下一步
- 在“Build Configuration”(构建配置)下
- 将“分支”保留为 ^main$
- 对于“构建类型”,请选择 Go、Node.js、Python、Java、.NET Core、Ruby 或 PHP(通过 Google Cloud 的 Buildpack)
- 将“构建上下文目录”保留为
/ - 点击保存
- 在“身份验证”下
- 点击允许未通过身份验证的调用
- 在“容器、卷、网络、安全性”下
- 在“安全性”标签页下,选择您在之前的步骤中创建的服务账号,例如
Cloud Run access to Firestore
- 在“安全性”标签页下,选择您在之前的步骤中创建的服务账号,例如
- 点击创建 (CREATE)
这会部署包含 bug 的 Cloud Run 服务,您将在下一部分中修复该 bug。
10. 修复 bug
修复了代码中的 bug
在 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 控制台,监控部署变更。
在生产环境中验证修复
当 Cloud Run 服务的 Cloud 控制台显示第 2 个修订版本现在正在处理 100% 的流量时(例如 https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions),您可以在浏览器中打开 Cloud Run 服务网址,并验证新输入的城镇数据在刷新页面后是否仍然存在。
11. 恭喜!
恭喜您完成此 Codelab!
建议您查看 Cloud Run 和通过 Git 进行的持续部署的相关文档。
所学内容
- 使用 Cloud Shell Editor 编写 Express Web 应用
- 将您的 GitHub 账号与 Google Cloud 相关联,以实现持续部署
- 自动将应用部署到 Cloud Run
- 了解如何使用 HTMX 和 TailwindCSS
12. 清理
为避免产生意外费用(例如,如果 Cloud Run 服务的调用次数意外超过免费层级中每月 Cloud Run 调用次数的分配额),您可以删除 Cloud Run 或删除您在第 2 步中创建的项目。
如需删除 Cloud Run 服务,请前往 Cloud Run Cloud 控制台 (https://console.cloud.google.com/run),然后删除您在本 Codelab 中创建的 Cloud Run 服务,例如删除 cloud-run-auto-deploy-codelab 服务。
如果您选择删除整个项目,可以前往 https://console.cloud.google.com/cloud-resource-manager,选择您在第 2 步中创建的项目,然后选择“删除”。如果您删除项目,则需要在 Cloud SDK 中更改项目。您可以运行 gcloud projects list 查看所有可用项目的列表。