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 日
版权所有 2024 Google LLC 保留所有权利。Google 和 Google 徽标是 Google LLC 的商标。其他所有公司名和产品名可能是其各自相关公司的商标。