GSP761
概览
对于无服务器 Cloud Run 开发 课程中的实验,您需要通读一个虚构的业务场景,并协助其中的人物完成无服务器迁移计划。
12 年前,Lily 创办了 Pet Theory 连锁宠物医院。随着连锁医院的发展,Lily 花在与保险公司电话沟通的时间上变多了,而用在治疗宠物上的时间变少了。如果保险公司能够在线查看治疗费用总额,该有多好!
在本系列之前的一些实验中,计算机顾问 Ruby 和 DevOps 工程师 Patrick 将 Pet Theory 的客户数据库迁移到了云端无服务器 Firestore 数据库中,然后为客户开放了在线预约的访问权限。由于 Pet Theory 的运维团队只有一个人,他们需要一个无需大量持续维护的无服务器解决方案。
在本实验中,您将帮助 Ruby 和 Patrick 向保险公司授予对客户数据的访问权限,同时确保不暴露客户的个人身份信息 (PII)。您将使用 Cloud Run(属于无服务器产品)构建一个安全的表现层状态转换 (REST) API 网关。这将允许保险公司查看治疗总费用,而看不到客户的个人身份信息。
目标
在本实验中,您将执行以下操作:
使用 Go 开发一个 REST API
将测试客户数据导入到 Firestore 中
将此 REST API 连接至 Firestore 数据库
将此 REST API 部署至 Cloud Run
前提条件
这是一个中级 实验,假设您熟悉 Cloud 控制台和 Cloud Shell 环境。本实验是一系列实验中的一个。完成之前的实验会有所帮助,但这不是硬性要求。
将数据导入到无服务器数据库
使用 Firebase 和 Firestore 构建无服务器 Web 应用
构建可创建 PDF 文件的无服务器应用
设置和要求
点击“开始实验”按钮前的注意事项
请阅读以下说明。实验是计时的,并且您无法暂停实验。计时器在您点击开始实验 后即开始计时,显示 Google Cloud 资源可供您使用多长时间。
此实操实验可让您在真实的云环境中开展实验活动,免受模拟或演示环境的局限。为此,我们会向您提供新的临时凭据,您可以在该实验的规定时间内通过此凭据登录和访问 Google Cloud。
为完成此实验,您需要:
能够使用标准的互联网浏览器(建议使用 Chrome 浏览器)。
注意 :请使用无痕模式(推荐)或无痕浏览器窗口运行此实验。这可以避免您的个人账号与学生账号之间发生冲突,这种冲突可能导致您的个人账号产生额外费用。
注意 :请仅使用学生账号完成本实验。如果您使用其他 Google Cloud 账号,则可能会向该账号收取费用。
如何开始实验并登录 Google Cloud 控制台
点击开始实验 按钮。如果该实验需要付费,系统会打开一个对话框供您选择支付方式。左侧是“实验详细信息”窗格,其中包含以下各项:
“打开 Google Cloud 控制台”按钮
剩余时间
进行该实验时必须使用的临时凭据
帮助您逐步完成本实验所需的其他信息(如果需要)
点击打开 Google Cloud 控制台 (如果您使用的是 Chrome 浏览器,请右键点击并选择在无痕式窗口中打开链接 )。
该实验会启动资源并打开另一个标签页,显示“登录”页面。
提示 :将这些标签页安排在不同的窗口中,并排显示。
注意 :如果您看见选择账号 对话框,请点击使用其他账号 。
如有必要,请复制下方的用户名 ,然后将其粘贴到登录 对话框中。
{{{user_0.username | "<用户名>"}}}
您也可以在“实验详细信息”窗格中找到“用户名”。
点击下一步 。
复制下面的密码 ,然后将其粘贴到欢迎 对话框中。
{{{user_0.password | "<密码>"}}}
您也可以在“实验详细信息”窗格中找到“密码”。
点击下一步 。
重要提示: 您必须使用实验提供的凭据。请勿使用您的 Google Cloud 账号凭据。
注意 :在本实验中使用您自己的 Google Cloud 账号可能会产生额外费用。
继续在后续页面中点击以完成相应操作:
接受条款及条件。
由于这是临时账号,请勿添加账号恢复选项或双重验证。
请勿注册免费试用。
片刻之后,系统会在此标签页中打开 Google Cloud 控制台。
注意: 如需访问 Google Cloud 产品和服务,请点击导航菜单 ,或在搜索 字段中输入服务或产品的名称。
激活 Cloud Shell
Cloud Shell 是一种装有开发者工具的虚拟机。它提供了一个永久性的 5GB 主目录,并且在 Google Cloud 上运行。Cloud Shell 提供可用于访问您的 Google Cloud 资源的命令行工具。
点击 Google Cloud 控制台顶部的激活 Cloud Shell 。
如果您连接成功,即表示您已通过身份验证,且当前项目会被设为您的 PROJECT_ID 环境变量所指的项目。输出内容中有一行说明了此会话的 PROJECT_ID :
Your Cloud Platform project in this session is set to YOUR_PROJECT_ID
gcloud
是 Google Cloud 的命令行工具。它已预先安装在 Cloud Shell 上,且支持 Tab 自动补全功能。
(可选)您可以通过此命令列出活跃账号名称:
gcloud auth list
点击授权 。
现在,输出的内容应如下所示:
输出:
ACTIVE: *
ACCOUNT: student-01-xxxxxxxxxxxx@qwiklabs.net
To set the active account, run:
$ gcloud config set account `ACCOUNT`
(可选)您可以通过此命令列出项目 ID:
gcloud config list project
输出 :
[core]
project = <project_ID>
输出示例 :
[core]
project = qwiklabs-gcp-44776a13dea667a6
Note: For full documentation of gcloud
, in Google Cloud, refer to the gcloud CLI overview guide .
Lily,Pet Theory 创始人
Ruby,您好!
还记得我们上周聊过吗?保险公司的文书工作和电话快压得我喘不过气来了。如果有一种方法能让保险代理高效安全地访问客户记录就好了。
目前的工作量让人难以为继。您能提供帮助吗?
Lily
Ruby,软件顾问
Lily,您好!
我昨天跟 Patrick 一起吃午饭时,想到了一个计划来让授权第三方更方便安全地访问 Pet Theory 的数字记录。
我们将按四个步骤实施此计划:
构建一个简单的 REST API。
导入客户测试数据。
将此 REST API 连接至客户数据库。
向此 REST API 添加身份验证机制。
Patrick 和我具备完成前两个步骤的技能,所以很容易开始执行这个计划。我们打算在本周末之前做出一个可行的原型设计。
Ruby
帮助 Ruby 管理为 Pet Theory 构建 REST API 所需的活动。
任务 1. 启用 Google API
对于此实验,系统已为您启用 2 个 API:
名称
API
Cloud Build
cloudbuild.googleapis.com
Cloud Run
run.googleapis.com
任务 2. 开发 REST API
激活您的项目:
gcloud config set project $(gcloud projects list --format='value(PROJECT_ID)' --filter='qwiklabs-gcp')
克隆 pet-theory 代码库并访问其源代码:
git clone https://github.com/rosera/pet-theory.git && cd pet-theory/lab08
使用您常用的文本编辑器,或点击 Cloud Shell 边栏中的代码编辑器按钮,查看 go.mod
和 go.sum
文件。
创建 main.go
文件并将以下内容添加到此文件中:
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "{status: 'running'}")
})
log.Println("Pets REST API listening on port", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("Error launching Pets REST API server: %v", err)
}
}
注意:
在上面的代码中,您创建一个端点来测试该服务是否按预期上线并运行。通过在服务网址后附加“/v1/”,您可以验证应用是否按预期运行。Cloud Run 会部署容器,因此您需要提供一个容器定义。名为 `Dockerfile` 的文件告知 Cloud Run 使用哪个 Go 版本、在应用中包含哪些文件以及如何启动代码。
现在,创建一个名为 Dockerfile
的文件并向其中添加以下内容:
FROM gcr.io/distroless/base-debian12
WORKDIR /usr/src/app
COPY server .
CMD [ "/usr/src/app/server" ]
文件 server
是从 main.go
构建的执行二进制文件。
运行以下命令以构建二进制文件:
go build -o server
运行构建命令之后,确保必需的 Dockerfile 和 sever 文件位于同一目录中:
ls -la
.
├── Dockerfile
├── go.mod
├── go.sum
├── main.go
└── server
对于 Cloud Run 中大多数基于 Go 的应用,类似于上面的模板 Dockerfile 文件通常无需修改即可使用。
运行以下命令部署您的简单 REST API:
gcloud builds submit \
--tag gcr.io/$GOOGLE_CLOUD_PROJECT/rest-api:0.1
此命令使用您的代码构建一个容器并将其放入项目的 Container Registry 中。依次点击导航菜单 > Container Registry 即可看到此容器。如果没有看到 rest-api
,请点击刷新 。
点击检查我的进度 ,验证您是否完成了上述任务。
使用 Cloud Build 创建映像
容器构建完成之后,运行以下代码进行部署:
gcloud run deploy rest-api \
--image gcr.io/$GOOGLE_CLOUD_PROJECT/rest-api:0.1 \
--platform managed \
--region {{{ project_0.default_region | "Filled in at lab startup." }}} \
--allow-unauthenticated \
--max-instances=2
部署完成后,您会看到类似如下的消息:
Service [rest-api] revision [rest-api-00001] has been deployed and is serving
traffic at https://rest-api-[hash].a.run.app
点击检查我的进度 ,验证您是否完成了上述任务。
已部署 REST API 服务
点击消息末尾处的服务网址,将其在新的浏览器标签页中打开。在该网址的结尾处附加 /v1/
,然后按 Enter 键。
您应会看到以下消息:
REST API 已上线且运行正常。原型服务可用之后,在下一部分中,您将使用该 API 从 Firestore 数据库中检索“客户”信息。
任务 3. 导入测试客户数据
Ruby,软件顾问
Patrick,您好!
您还保存着我们之前创建的虚构客户数据吗?我们需要利用这些数据来进行测试。
您还记得我们是如何设置 Firestore 数据库并导入数据的吗?
Ruby
Patrick,IT 管理员
Ruby,您好!
是的,我还留着那些测试数据。我今天会将它们迁移到 Firestore,您明天可以用来进行测试。
Patrick
Ruby 与 Patrick 之前创建了一个包含 10 个客户的测试数据库,其中包括针对每位客户的猫咪的建议治疗方案。
帮助 Patrick 配置此 Firestore 数据库并导入客户测试数据。首先,在项目中启用 Firestore。
返回 Cloud 控制台并依次点击导航菜单 > Firestore 。
点击创建数据库 按钮。
点击原生模式 按钮,然后点击继续 。
对于位置类型 ,选择区域 。
从可用列表中选择区域 ,然后点击创建数据库 。
等待数据库创建完毕,然后再继续操作。
点击检查我的进度 ,验证您是否完成了上述任务。
已创建 Firestore 数据库
将导入的文件迁移到为您创建的 Cloud Storage 存储桶中:
gsutil mb -c standard -l {{{ project_0.default_region | Region }}} gs://$GOOGLE_CLOUD_PROJECT-customer
gsutil cp -r gs://spls/gsp645/2019-10-06T20:10:37_43617 gs://$GOOGLE_CLOUD_PROJECT-customer
现在,将这些数据导入到 Firebase:
gcloud beta firestore import gs://$GOOGLE_CLOUD_PROJECT-customer/2019-10-06T20:10:37_43617/
重新加载 Cloud 控制台浏览器以查看 Firestore 结果。
在 Firestore 下,点击“默认”下的 customers 。您应该会看到已导入的宠物数据,请简单浏览一下。如果未看到任何数据,请试试刷新页面。
很好,现在已成功创建了 Firestore 数据库并填充了测试数据!
任务 4. 将此 REST API 连接至 Firestore 数据库
Ruby,软件顾问
Lily,您好!
跟您讲一下进度:Patrick 和我完成了清单上的前两项任务。
现在我要开始设计 REST API 的结构,使它可以访问 Firestore 中的客户数据。
Ruby
Lily,Pet Theory 创始人
Ruby,您好!
太好了,Ruby!希望看到下一阶段正式启动。
Lily
在此部分中,您将帮助 Ruby 在 REST API 中创建另一个端点,如下所示:
https://rest-api-[hash].a.run.app/v1/customer/22530
例如,该网址应该返回 ID 为 22530 的客户(如果此客户存在于 Firestore 数据库中)所有提议的、接受的和拒绝的治疗方案的总费用:
{
"status": "success",
"data": {
"proposed": 1602,
"approved": 585,
"rejected": 489
}
}
注意: 如果数据库中不存在此客户,系统应会返回状态代码 404(未找到)和一条错误消息。
这一新功能需要通过一个软件包来访问 Firestore 数据库,并用另一个软件包处理跨域资源共享 (CORS)。
获取 $GOOGLE_CLOUD_PROJECT 环境变量的值
echo $GOOGLE_CLOUD_PROJECT
打开 pet-theory/lab08 目录下现有的 main.go
文件。
注意: 使用为 $GOOGLE_CLOUD_PROJECT 显示的值更新 main.go 的内容。
将该文件的内容替换为以下代码,确保 PROJECT_ID
设置为 :
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"cloud.google.com/go/firestore"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"google.golang.org/api/iterator"
)
var client *firestore.Client
func main() {
var err error
ctx := context.Background()
client, err = firestore.NewClient(ctx, "{{{ project_0.project_id | \"Filled in at lab startup\"}}}")
if err != nil {
log.Fatalf("Error initializing Cloud Firestore client: %v", err)
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
r := mux.NewRouter()
r.HandleFunc("/v1/", rootHandler)
r.HandleFunc("/v1/customer/{id}", customerHandler)
log.Println("Pets REST API listening on port", port)
cors := handlers.CORS(
handlers.AllowedHeaders([]string{"X-Requested-With", "Authorization", "Origin"}),
handlers.AllowedOrigins([]string{"https://storage.googleapis.com"}),
handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "OPTIONS", "PATCH", "CONNECT"}),
)
if err := http.ListenAndServe(":"+port, cors(r)); err != nil {
log.Fatalf("Error launching Pets REST API server: %v", err)
}
}
在该文件的末尾处添加处理程序支持:
func rootHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "{status: 'running'}")
}
func customerHandler(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
ctx := context.Background()
customer, err := getCustomer(ctx, id)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"status": "fail", "data": '%s'}`, err)
return
}
if customer == nil {
w.WriteHeader(http.StatusNotFound)
msg := fmt.Sprintf("`Customer \"%s\" not found`", id)
fmt.Fprintf(w, fmt.Sprintf(`{"status": "fail", "data": {"title": %s}}`, msg))
return
}
amount, err := getAmounts(ctx, customer)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"status": "fail", "data": "Unable to fetch amounts: %s"}`, err)
return
}
data, err := json.Marshal(amount)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"status": "fail", "data": "Unable to fetch amounts: %s"}`, err)
return
}
fmt.Fprintf(w, fmt.Sprintf(`{"status": "success", "data": %s}`, data))
}
在该文件的末尾处添加客户支持:
type Customer struct {
Email string `firestore:"email"`
ID string `firestore:"id"`
Name string `firestore:"name"`
Phone string `firestore:"phone"`
}
func getCustomer(ctx context.Context, id string) (*Customer, error) {
query := client.Collection("customers").Where("id", "==", id)
iter := query.Documents(ctx)
var c Customer
for {
doc, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
err = doc.DataTo(&c)
if err != nil {
return nil, err
}
}
return &c, nil
}
func getAmounts(ctx context.Context, c *Customer) (map[string]int64, error) {
if c == nil {
return map[string]int64{}, fmt.Errorf("Customer should be non-nil: %v", c)
}
result := map[string]int64{
"proposed": 0,
"approved": 0,
"rejected": 0,
}
query := client.Collection(fmt.Sprintf("customers/%s/treatments", c.Email))
if query == nil {
return map[string]int64{}, fmt.Errorf("Query is nil: %v", c)
}
iter := query.Documents(ctx)
for {
doc, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
treatment := doc.Data()
result[treatment["status"].(string)] += treatment["cost"].(int64)
}
return result, nil
}
保存 该文件。
任务 6. 随堂测验
响应 `/v1/customer/` 模式网址的函数是哪一个
getAmounts
customerHandler
向客户端返回“success”的语句是哪一条
fmt.Fprintf(w, `{"status": "fail", "data": "Unable to fetch amounts: %s"}
fmt.Fprintf(w, fmt.Sprintf(`{"status": "success", "data": %s}
从 Firestore 数据库中读取数据的函数是哪两个
customerHandler 和 getCustomer
getCustomer 和 getAmounts
任务 7. 部署新的修订版
对源代码进行重新构建:
go build -o server
为 REST API 构建一个新映像:
gcloud builds submit \
--tag gcr.io/$GOOGLE_CLOUD_PROJECT/rest-api:0.2
点击检查我的进度 ,验证已完成以下目标:
构建映像修订版 0.2
部署更新后的映像:
gcloud run deploy rest-api \
--image gcr.io/$GOOGLE_CLOUD_PROJECT/rest-api:0.2 \
--platform managed \
--region {{{ project_0.default_region | "Filled in at lab startup." }}} \
--allow-unauthenticated \
--max-instances=2
4. 部署完成后,您将看到与之前类似的消息。部署新版本后,您的 REST API 的网址没有发生变化:
Service [rest-api] revision [rest-api-00002] has been deployed and is serving
traffic at https://rest-api-[hash].a.run.app
返回到指向该网址(末尾处有 /v1/
)的浏览器标签页。刷新并确保可以收到与以前同样的消息,这表示该 API 仍在运行。
在浏览器地址栏中,将 /customer/22530
附加到应用网址。您应该会收到此 JSON 响应,里面列出了客户提议、批准和拒绝的治疗方案的总金额:
除了 22530,您还可以在此网址中输入其他几个客户 ID:
34216
70156(所有金额数应为 0)
12345(客户/宠物不存在,应返回错误,例如 Query is nil )
您构建了一个可从数据库中读取数据的可扩缩、低维护量的无服务器 REST API。
恭喜!
恭喜!在此实验中,您帮助 Ruby 和 Patrick 成功为 Pet Theory 构建了一个原型 REST API。您创建了连接至 Firestore 数据库的 REST API,并将其部署到了 Cloud Run。您还测试了此 API 以确保其按预期运行。
Google Cloud 培训和认证
…可帮助您充分利用 Google Cloud 技术。我们的课程 会讲解各项技能与最佳实践,可帮助您迅速上手使用并继续学习更深入的知识。我们提供从基础到高级的全方位培训,并有点播、直播和虚拟三种方式选择,让您可以按照自己的日程安排学习时间。各项认证 可以帮助您核实并证明您在 Google Cloud 技术方面的技能与专业知识。
上次更新手册的时间:2024 年 5 月 6 日
上次测试实验的时间:2024 年 5 月 6 日
版权所有 2025 Google LLC 保留所有权利。Google 和 Google 徽标是 Google LLC 的商标。其他所有公司名和产品名可能是其各自相关公司的商标。