关于此 Codelab
1. 概览
在之前的实验中,您构建了 Pic-a-daily 应用的事件驱动型版本,该版本使用 Google Cloud Storage 触发的 Cloud Functions 函数以用于图片分析服务,一个 GCS 通过 Pub/Sub 触发 Cloud Run 容器的缩略图服务触发 Cloud Run 容器,并使用 Eventarc 触发 Cloud Run 上的图片垃圾回收器服务。还有一项 Cloud Scheduler 触发的拼贴服务:

在本实验中,您将创建一个应用的编排版本。您无需使用不同类型的事件流经系统,而是使用 Workflows 来编排和调用服务,如下所示:

学习内容
- App Engine
- Cloud Firestore
- Cloud Functions
- Cloud Run
- Workflows
2. 设置和要求
自定进度的环境设置



请记住项目 ID,它在所有 Google Cloud 项目中都是唯一的名称(上述名称已被占用,您无法使用,抱歉!)。它稍后将在此 Codelab 中被称为 PROJECT_ID。
- 接下来,您需要在 Cloud 控制台中启用结算功能,才能使用 Google Cloud 资源。
运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。请务必按照“清理”部分部分,其中会指导您如何关停资源,以免产生超出本教程范围的结算费用。Google Cloud 的新用户符合参与 300 美元的免费试用计划的条件。
启动 Cloud Shell
虽然可以通过笔记本电脑对 Google Cloud 进行远程操作,但在此 Codelab 中,您将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。
在 GCP 控制台中,点击右上角工具栏上的 Cloud Shell 图标:

预配和连接到环境应该只需要片刻时间。完成后,您应该会看到如下内容:

这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证功能。只需一个浏览器,即可完成本实验中的所有工作。
3. Workflows 简介

您可以使用 Workflows 创建无服务器工作流,以便按照您定义的顺序将一系列无服务器任务关联在一起。您可以结合使用 Google Cloud 的 API、Cloud Functions 和 Cloud Run 等无服务器产品以及对外部 API 的调用,以创建灵活的无服务器应用。
正如您对编排器所期望的那样,Workflows 允许您使用基于 YAML/JSON 的工作流定义语言来定义业务逻辑的流,并提供 Workflows Execution API 和 Workflows 界面来触发这些流。
它不仅仅是一个具有下列可配置内置功能的编排系统:
- 在步骤之间灵活地进行重试和错误处理,确保步骤的执行可靠。
- 在步骤之间进行 JSON 解析和变量传递,以避免粘合代码。
- 用于决策的表达式公式支持按条件执行步骤。
- 适用于模块化且可重复使用的工作流的子工作流。
- 通过支持外部服务,您可以编排 Google Cloud 以外的服务。
- 为 Google Cloud 和外部服务提供身份验证支持,以确保安全执行步骤。
- 连接到 Google Cloud 服务(例如 Pub/Sub、Firestore、Tasks、Secret Manager),可简化集成。
更不用说,Workflows 是一款全代管式无服务器产品。您无需配置或扩缩服务器,并且只需为实际用量付费。
4. 启用 API
在本实验中,您将使用 Workflows 连接 Cloud Functions 和 Cloud Run 服务。您还将使用 App Engine、Cloud Build、Vision API 和其他服务。
在 Cloud Shell 中,确保启用了所有必要的服务:
gcloud services enable \ appengine.googleapis.com \ cloudbuild.googleapis.com \ cloudfunctions.googleapis.com \ compute.googleapis.com \ firestore.googleapis.com \ run.googleapis.com \ vision.googleapis.com \ workflows.googleapis.com \
一段时间后,您应该会看到操作成功完成:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
5. 获取代码
如果之前的 Codelab 中没有获得过该代码,请执行以下操作:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
您将获得以下与此实验相关的文件夹结构:
frontend | workflows | ├── functions ├── |── trigger-workflow ├── |── vision-data-transform ├── services ├── |── collage ├── |── thumbnails ├── workflows.yaml
以下是相关文件夹:
frontend包含我们将在实验 4 中重复使用的 App Engine 前端。functions包含为工作流创建的 Cloud Functions 函数。services包含为工作流修改的 Cloud Run 服务。workflows.yaml是工作流定义文件。
6. 探索 Workflows YAML
workflows.yaml 通过一系列步骤定义了工作流。我们来仔细了解一下它。
在工作流开始时,系统会传入一些参数。它们将由两个触发 Workflows 的 Cloud Functions 函数传入。我们稍后会介绍这些函数,但 Workflows 的启动方式如下:

在 YAML 中,您可以看到这些参数在 init 步骤中分配给了变量,例如触发事件的文件和存储分区名称,以及 Workflows 将调用的某些 Cloud Functions 和 Cloud Run 服务的网址:
main:
params: [args]
steps:
- init:
assign:
- file: ${args.file}
- bucket: ${args.bucket}
- gsUri: ${"gs://" + bucket + "/" + file}
- projectId: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
- urls: ${args.urls}
接下来,Workflows 检查事件类型。支持两种事件类型:object.finalize(文件保存在 Cloud Storage 存储分区中时发出)和 object.delete(文件被删除时发出)。执行任何其他操作都会引发不受支持的事件异常。

以下是 YAML 工作流定义中的步骤,我们将检查文件存储事件类型:
- eventTypeSwitch:
switch:
- condition: ${args.eventType == "google.storage.object.finalize"}
next: imageAnalysisCall
- condition: ${args.eventType == "google.storage.object.delete"}
next: pictureGarbageCollectionGCS
- eventTypeNotSupported:
raise: ${"eventType " + args.eventType + " is not supported"}
next: end
请注意 Workflows 如何通过 switch 指令及其各种条件支持 switch 语句和异常处理,以及如何在无法识别事件时引发错误。
接下来,我们来看看 imageAnalysisCall。这是来自 Workflows 的一系列调用,用于调用 Vision API 来分析图片,转换 Vision API 响应数据以对图片中识别出的内容的标签进行排序,选择主色,检查图片是否可以安全显示,然后将元数据保存到 Cloud Firestore。
请注意,除 Vision Transform Cloud Functions 函数(稍后将部署)之外的所有其他操作都在 Workflows 中完成:

YAML 格式的步骤如下:
- imageAnalysisCall:
call: http.post
args:
url: https://vision.googleapis.com/v1/images:annotate
headers:
Content-Type: application/json
auth:
type: OAuth2
body:
requests:
- image:
source:
gcsImageUri: ${gsUri}
features:
- type: LABEL_DETECTION
- type: SAFE_SEARCH_DETECTION
- type: IMAGE_PROPERTIES
result: imageAnalysisResponse
- transformImageAnalysisData:
call: http.post
args:
url: ${urls.VISION_DATA_TRANSFORM_URL}
auth:
type: OIDC
body: ${imageAnalysisResponse.body}
result: imageMetadata
- checkSafety:
switch:
- condition: ${imageMetadata.body.safe == true}
next: storeMetadata
next: end
- storeMetadata:
call: http.request
args:
url: ${"https://firestore.googleapis.com/v1/projects/" + projectId + "/databases/(default)/documents/pictures/" + file + "?updateMask.fieldPaths=color&updateMask.fieldPaths=labels&updateMask.fieldPaths=created"}
auth:
type: OAuth2
method: PATCH
body:
name: ${"projects/" + projectId + "/databases/(default)/documents/pictures/" + file}
fields:
color:
stringValue: ${imageMetadata.body.color}
created:
timestampValue: ${imageMetadata.body.created}
labels:
arrayValue:
values: ${imageMetadata.body.labels}
result: storeMetadataResponse
图片分析完毕后,接下来的两个步骤是创建图片的缩略图和最新图片的拼贴图。具体方法是部署 2 项 Cloud Run 服务,并通过 thumbnailCall 和 collageCall 步骤调用这些服务:

YAML 中的步骤:
- thumbnailCall:
call: http.post
args:
url: ${urls.THUMBNAILS_URL}
auth:
type: OIDC
body:
gcsImageUri: ${gsUri}
result: thumbnailResponse
- collageCall:
call: http.get
args:
url: ${urls.COLLAGE_URL}
auth:
type: OIDC
result: collageResponse
此执行分支以 finalizeCompleted 步骤中每项服务返回状态代码结束:
- finalizeCompleted:
return:
imageAnalysis: ${imageAnalysisResponse.code}
storeMetadata: ${storeMetadataResponse.code}
thumbnail: ${thumbnailResponse.code}
collage: ${collageResponse.code}
执行的另一个分支是从主存储分区中删除某个文件,该文件包含图片的高分辨率版本。在该分支中,我们需要从包含缩略图的存储分区中删除图片缩略图,并从 Firestore 中删除该图片的元数据。这两个操作都通过 Workflows 中的 HTTP 调用完成:

YAML 中的步骤:
- pictureGarbageCollectionGCS:
try:
call: http.request
args:
url: ${"https://storage.googleapis.com/storage/v1/b/thumbnails-" + projectId + "/o/" + file}
auth:
type: OAuth2
method: DELETE
result: gcsDeletionResult
except:
as: e
steps:
- dummyResultInOutVar:
assign:
- gcsDeletionResult:
code: 200
body: "Workaround for empty body response"
- pictureGarbageCollectionFirestore:
call: http.request
args:
url: ${"https://firestore.googleapis.com/v1/projects/" + projectId + "/databases/(default)/documents/pictures/" + file}
auth:
type: OAuth2
method: DELETE
result: firestoreDeletionResult
delete 分支最后会返回每个步骤的结果 / 代码:
- deleteCompleted:
return:
gcsDeletion: ${gcsDeletionResult}
firestoreDeletion: ${firestoreDeletionResult.code}
在以下步骤中,我们将创建 Workflows 的所有外部依赖项:存储分区、Cloud Functions、Cloud Run 服务和 Firestore 数据库。
7. 创建存储分区
您需要为图片设置 2 个存储分区:一个用于保存原始高分辨率图片,另一个用于保存图片的缩略图。
使用 gsutil 工具创建一个具有统一访问权限的公共区域级(本例中为欧洲)存储分区,以便用户上传照片:
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
gsutil mb -l EU gs://${BUCKET_PICTURES}
gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}
gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}
为缩略图再创建一个公开区域存储分区:
export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
gsutil mb -l EU gs://${BUCKET_THUMBNAILS}
gsutil uniformbucketlevelaccess set on gs://${BUCKET_THUMBNAILS}
gsutil iam ch allUsers:objectViewer gs://${BUCKET_THUMBNAILS}
您可以访问 Cloud 控制台的 Cloud Storage 部分,仔细检查存储分区是否已创建并公开:

8. Vision 数据转换 (Cloud Functions)
Workflows.yaml 以 init、eventTypeSwitch、eventTypeNotSupported 步开头。这可确保将来自存储分区的事件路由到正确的步骤。
对于 object.finalize 事件,imageAnalysisCall 步骤会调用 Vision API 以提取所创建图片的元数据。所有这些步骤都在 Workflows 中完成:

接下来,我们需要转换从 Vision API 返回的数据,然后才能将其保存到 Firestore。更具体地说,我们需要:
- 列出为图片返回的标签。
- 检索图片的主色。
- 确定照片是否安全。
这是在 Cloud Functions 函数的代码中完成的,Workflows 会直接调用此函数:

探索代码
Cloud Functions 函数称为 vision-data-transform。您可以在 index.js 中查看其完整代码。如您所见,此函数的唯一目的是执行从 JSON 到 JSON 的转换,以便方便地在 Firestore 中存储图片元数据。
部署到 Cloud Functions
导航到该文件夹:
cd workflows/functions/vision-data-transform/nodejs
设置您选择的区域:
export REGION=europe-west1
gcloud config set functions/region ${REGION}
使用以下命令部署该函数:
export SERVICE_NAME=vision-data-transform
gcloud functions deploy ${SERVICE_NAME} \
--source=. \
--runtime nodejs10 \
--entry-point=vision_data_transform \
--trigger-http \
--allow-unauthenticated
部署函数后,Workflows transformImageAnalysisData 步骤将能够调用此函数以执行 Vision API 数据转换。
9. 准备数据库
工作流程中的下一步是通过图片数据检查图片的安全性,然后将 Vision API 返回的图片相关信息存储到 Cloud Firestore 数据库(一种快速、全代管式、无服务器、云原生的 NoSQL 文档数据库)中:

这两项操作都在 Workflows 中完成,但您需要创建 Firestore 数据库才能存储元数据。
首先,在您想要 Firestore 数据库的区域中创建 App Engine 应用(Firestore 的一项要求):
export REGION_FIRESTORE=europe-west2
gcloud app create --region=${REGION_FIRESTORE}
接下来,在同一区域中创建 Firestore 数据库:
gcloud firestore databases create --region=${REGION_FIRESTORE}
这些文档将在我们的集合中以编程方式创建,并包含 4 个字段:
- name(字符串):已上传图片的文件名,也是文档的键
- labels(字符串数组):Vision API 识别出的项目的标签
- color(字符串):主色的十六进制颜色代码(即#ab12ef)
- created(日期):存储此图片的元数据时的时间戳
- thumbnail(布尔值):选填字段,如果为此照片生成了缩略图,此字段将是 true
我们将在 Firestore 中搜索包含缩略图的图片,并按创建日期排序,因此需要创建一个搜索索引。您可以使用以下命令创建索引:
gcloud firestore indexes composite create --collection-group=pictures \ --field-config field-path=thumbnail,order=descending \ --field-config field-path=created,order=descending
请注意,索引创建过程最多可能需要 10 分钟左右。
创建索引后,您可以在 Cloud 控制台中查看该索引:

Workflows storeMetadata 步骤现在能够将图片元数据存储到 Firestore。
10. 缩略图服务 (Cloud Run)
链中的下一个步骤是创建图片的缩略图。这是在 Cloud Run 服务的代码中完成的,Workflows 会在 thumbnailCall 步骤中调用此服务:

探索代码
Cloud Run 服务称为 thumbnails。您可以在 index.js 中查看其完整代码。
构建和发布容器映像
Cloud Run 可以运行容器,但您首先需要构建容器映像(在 Dockerfile 中定义)。Google Cloud Build 可用于构建容器映像,然后托管到 Google Container Registry。
导航到该文件夹:
cd workflows/services/thumbnails/nodejs
构建:
export SERVICE_SRC=thumbnails
export SERVICE_NAME=${SERVICE_SRC}-service
gcloud builds submit \
. \
--tag gcr.io/${GOOGLE_CLOUD_PROJECT}/${SERVICE_NAME}
一两分钟后,构建应该会成功,容器将部署到 Google Container Registry。
部署到 Cloud Run
设置一些所需的变量和配置:
export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
export REGION=europe-west1
gcloud config set run/region ${REGION}
gcloud config set run/platform managed
使用以下命令进行部署:
gcloud run deploy ${SERVICE_NAME} \
--image gcr.io/${GOOGLE_CLOUD_PROJECT}/${SERVICE_NAME} \
--no-allow-unauthenticated \
--memory=1Gi \
--update-env-vars BUCKET_THUMBNAILS=${BUCKET_THUMBNAILS}
部署该服务后,Workflows thumbnailCall 步骤将能够调用此服务。
11. 拼图服务 (Cloud Run)
链中的下一个步骤是根据最新的图片创建拼贴。这是在 Cloud Run 服务的代码中完成的,Workflows 会在 collageCall 步骤中调用此服务:

探索代码
Cloud Run 服务称为 collage。您可以在 index.js 中查看其完整代码。
构建和发布容器映像
Cloud Run 可以运行容器,但您首先需要构建容器映像(在 Dockerfile 中定义)。Google Cloud Build 可用于构建容器映像,然后托管到 Google Container Registry。
导航到该文件夹:
cd services/collage/nodejs
构建:
export SERVICE_SRC=collage
export SERVICE_NAME=${SERVICE_SRC}-service
gcloud builds submit \
. \
--tag gcr.io/${GOOGLE_CLOUD_PROJECT}/${SERVICE_NAME}
一两分钟后,构建应该会成功,容器将部署到 Google Container Registry。
部署到 Cloud Run
设置一些所需的变量和配置:
export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
export REGION=europe-west1
gcloud config set run/region ${REGION}
gcloud config set run/platform managed
部署:
gcloud run deploy ${SERVICE_NAME} \
--image gcr.io/${GOOGLE_CLOUD_PROJECT}/${SERVICE_NAME} \
--no-allow-unauthenticated \
--memory=1Gi \
--update-env-vars BUCKET_THUMBNAILS=${BUCKET_THUMBNAILS}
部署服务后,您可以在 Cloud 控制台的“Cloud Run”部分下检查两项服务是否正在运行,并且工作流 collageCall 步骤将能够调用此服务:

12. Workflows 部署
我们部署了 Workflows 的所有外部依赖项。其余所有步骤(finalizeCompleted、pictureGarbageCollectionGCS、pictureGarbageCollectionFirestore、deleteCompleted)都可以由 Workflows 自行完成。
现在可以部署 Workflows 了!
前往包含 workflows.yaml 文件的文件夹,并使用以下命令部署该文件:
export WORKFLOW_REGION=europe-west4
export WORKFLOW_NAME=picadaily-workflows
gcloud workflows deploy ${WORKFLOW_NAME} \
--source=workflows.yaml \
--location=${WORKFLOW_REGION}
工作流应该会在几秒钟后部署完毕,然后您可以在 Cloud 控制台的“Workflows”部分看到它:

如果需要,您可以点击工作流并进行修改。在修改过程中,您会获得非常直观的工作流程:

您也可以在 Cloud 控制台中使用正确的参数手动执行工作流。相反,我们会在下一步中自动执行该操作,以响应 Cloud Storage 事件。
13. Workflows 触发器 (Cloud Functions)
工作流已部署并准备就绪。现在,当 Cloud Storage 存储分区中创建或删除文件时,我们需要触发 Workflows。分别为 storage.object.finalize 和 storage.object.delete 事件。
Workflows 提供了用于创建、管理和执行您可以使用的 Workflows 的 API 和客户端库。在这种情况下,您将使用 Workflows Execution API,具体来说就是使用其 Node.js 客户端库来触发工作流。
您将通过 Cloud Functions 函数触发 Workflows,以监听 Cloud Storage 事件。由于一个 Cloud Functions 函数只能监听一种事件类型,因此您需要部署两个 Cloud Functions 函数来同时监听 create 事件和 delete 事件:

探索代码
Cloud Functions 函数称为 trigger-workflow。您可以在 index.js 中查看其完整代码。
部署到 Cloud Functions
导航到该文件夹:
cd workflows/functions/trigger-workflow/nodejs
设置一些所需的变量和配置:
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
export REGION=europe-west1
export WORKFLOW_NAME=picadaily-workflows
export WORKFLOW_REGION=europe-west4
export COLLAGE_URL=$(gcloud run services describe collage-service --format 'value(status.url)')
export THUMBNAILS_URL=$(gcloud run services describe thumbnails-service --format 'value(status.url)')
export VISION_DATA_TRANSFORM_URL=$(gcloud functions describe vision-data-transform --format 'value(httpsTrigger.url)')
gcloud config set functions/region ${REGION}
部署用于响应 finalize 事件的函数:
export SERVICE_NAME=trigger-workflow-on-finalize
gcloud functions deploy ${SERVICE_NAME} \
--source=. \
--runtime nodejs10 \
--entry-point=trigger_workflow \
--trigger-resource=${BUCKET_PICTURES} \
--trigger-event=google.storage.object.finalize \
--allow-unauthenticated \
--set-env-vars GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT},WORKFLOW_REGION=${WORKFLOW_REGION},WORKFLOW_NAME=${WORKFLOW_NAME},THUMBNAILS_URL=${THUMBNAILS_URL},COLLAGE_URL=${COLLAGE_URL},VISION_DATA_TRANSFORM_URL=${VISION_DATA_TRANSFORM_URL}
部署用于响应删除事件的第二个函数:
export SERVICE_NAME=trigger-workflow-on-delete
gcloud functions deploy ${SERVICE_NAME} \
--source=. \
--runtime nodejs10 \
--entry-point=trigger_workflow \
--trigger-resource=${BUCKET_PICTURES} \
--trigger-event=google.storage.object.delete \
--allow-unauthenticated \
--set-env-vars GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT},WORKFLOW_REGION=${WORKFLOW_REGION},WORKFLOW_NAME=${WORKFLOW_NAME},THUMBNAILS_URL=${THUMBNAILS_URL},COLLAGE_URL=${COLLAGE_URL},VISION_DATA_TRANSFORM_URL=${VISION_DATA_TRANSFORM_URL}
部署完成后,您可以在 Cloud 控制台中看到这两项功能:

14. 前端 (App Engine)
在此步骤中,您将通过 Pic-a-daily: Lab 4 - 创建网络前端 (Pic-a-daily: Lab 4-Create a web frontend) 在 Google App Engine 上创建一个网络前端,以便用户从网络应用上传图片,以及浏览上传的图片及其缩略图。

如需详细了解 App Engine 并阅读代码说明,请参阅每日图片:实验 4 - 创建网络前端。
探索代码
App Engine 应用名为 frontend。您可以在 index.js 中查看其完整代码。
部署到 App Engine
导航到该文件夹:
cd frontend
设置您选择的区域,并将 app.yaml 中的 GOOGLE_CLOUD_PROJECT 替换为您的实际项目 ID:
export REGION=europe-west1
gcloud config set compute/region ${REGION}
sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml
部署:
gcloud app deploy app.yaml -q
一两分钟后,系统会告知您应用正在处理流量:
Beginning deployment of service [default]... ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 8 files to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [default]...done. Setting traffic split for service [default]...done. Deployed service [default] to [https://GOOGLE_CLOUD_PROJECT.appspot.com] You can stream logs from the command line by running: $ gcloud app logs tail -s default To view your application in the web browser run: $ gcloud app browse
您还可以访问 Cloud 控制台的 App Engine 部分,查看应用是否已部署,并探索 App Engine 的功能,例如版本控制和流量分配:

15. 测试工作流
如需进行测试,请转到应用的默认 App Engine 网址 (https://<YOUR_PROJECT_ID>.appspot.com/),您应该会看到前端界面已启动并运行!

上传图片。这应该会触发 Workflows,并且您可以在 Cloud 控制台中看到处于 Active 状态的工作流执行情况:

完成工作流后,您可以点击执行 ID 并查看不同服务的输出:

再上传 3 张照片。您还应该看到 Cloud Storage 存储分区和 App Engine 前端中图片的缩略图和拼贴已更新:

16. 清理(可选)
如果您不打算保留该应用,可以通过删除整个项目来清理资源,从而节省成本并成为一个整体优秀的云公民:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
17. 恭喜!
您使用 Workflows 创建了应用的编排版本,以编排和调用服务。
所学内容
- App Engine
- Cloud Firestore
- Cloud Functions
- Cloud Run
- Workflows