1. 概览
在此 Codelab 中,您将在上一个实验的基础上添加缩略图服务。缩略图服务是一个网络容器,可接收大图片并从中创建缩略图。
当图片上传到 Cloud Storage 时,系统会通过 Cloud Pub/Sub 将通知发送到 Cloud Run 网络容器,然后该容器会调整图片大小并将其保存回 Cloud Storage 中的另一个存储分区。

学习内容
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub
2. 设置和要求
自定进度的环境设置
- 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个。



- 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串,您可以随时对其进行更新。
- 项目 ID 在所有 Google Cloud 项目中必须是唯一的,并且不可变(一经设置便无法更改)。Cloud Console 会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(它通常标识为
PROJECT_ID),因此如果您不喜欢某个 ID,请再生成一个随机 ID,还可以尝试自己创建一个,并确认是否可用。然后,项目创建后,ID 会处于“冻结”状态。 - 第三个值是一些 API 使用的项目编号。如需详细了解所有这三个值,请参阅文档。
- 接下来,您需要在 Cloud Console 中启用结算功能,才能使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。要关闭资源以避免产生超出本教程范围的费用,请按照此 Codelab 末尾提供的任何“清理”说明操作。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。
启动 Cloud Shell
虽然可以通过笔记本电脑对 Google Cloud 进行远程操作,但在此 Codelab 中,您将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。
在 GCP 控制台中,点击右上角工具栏上的 Cloud Shell 图标:

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

这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证功能。只需一个浏览器,即可完成本实验中的所有工作。
3. 启用 API
在本实验中,您需要使用 Cloud Build 构建容器映像,并使用 Cloud Run 部署容器。
在 Cloud Shell 中启用这两个 API:
gcloud services enable cloudbuild.googleapis.com \ run.googleapis.com
您应该会看到操作已成功完成:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. 创建另一个分桶
您将上传图片的缩略图存储在另一个存储分区中。我们来使用 gsutil 创建第二个存储分区。
在 Cloud Shell 中,为唯一的存储分区名称设置变量。Cloud Shell 已将 GOOGLE_CLOUD_PROJECT 设置为您的唯一项目 ID。您可以将其附加到存储分区名称中。然后,在欧洲创建一个具有统一级访问权限的公共多区域存储分区:
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
最后,您应该会获得一个新的公开存储分区:

5. 克隆代码
克隆代码并前往包含服务的目录:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop cd serverless-photosharing-workshop/services/thumbnails/nodejs
该服务的布局文件如下:
services
|
├── thumbnails
|
├── nodejs
|
├── Dockerfile
├── index.js
├── package.json
在 thumbnails/nodejs 文件夹中,您有 3 个文件:
index.js包含 Node.js 代码package.json定义库依赖项Dockerfile定义容器映像
6. 探索代码
如需浏览代码,您可以点击 Cloud Shell 窗口顶部的 Open Editor 按钮,以使用内置文本编辑器:

您还可以在专用浏览器窗口中打开编辑器,以获得更多屏幕空间。
依赖项
package.json 文件定义了所需的库依赖项:
{
"name": "thumbnail_service",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"bluebird": "^3.7.2",
"express": "^4.17.1",
"imagemagick": "^0.1.3",
"@google-cloud/firestore": "^4.9.9",
"@google-cloud/storage": "^5.8.3"
}
}
Cloud Storage 库用于读取和保存 Cloud Storage 中的图片文件。用于更新图片元数据的 Firestore。Express 是一个 JavaScript / Node Web 框架。body-parser 模块用于轻松解析传入的请求。Bluebird 用于处理 promise,而 Imagemagick 是一个用于处理图片的库。
Dockerfile
Dockerfile 定义应用的容器映像:
FROM node:14-slim
# installing Imagemagick
RUN set -ex; \
apt-get -y update; \
apt-get -y install imagemagick; \
rm -rf /var/lib/apt/lists/*; \
mkdir /tmp/original; \
mkdir /tmp/thumbnail;
WORKDIR /picadaily/services/thumbnails
COPY package*.json ./
RUN npm install --production
COPY . .
CMD [ "npm", "start" ]
基础映像为 Node 14,imagemagick 库用于图像处理。创建了一些临时目录,用于保存原始图片文件和缩略图文件。然后,在通过 npm start 启动代码之前,安装代码所需的 NPM 模块。
index.js
让我们分段探索代码,以便更好地了解此程序的作用。
const express = require('express');
const imageMagick = require('imagemagick');
const Promise = require("bluebird");
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const Firestore = require('@google-cloud/firestore');
const app = express();
app.use(express.json());
我们首先需要所需的依赖项,然后创建 Express Web 应用,并指明我们想要使用 JSON 正文解析器,因为传入的请求实际上只是通过 POST 请求发送到我们应用的 JSON 载荷。
app.post('/', async (req, res) => {
try {
// ...
} catch (err) {
console.log(`Error: creating the thumbnail: ${err}`);
console.error(err);
res.status(500).send(err);
}
});
我们正在接收 / 根网址上的这些传入载荷,并使用一些错误逻辑处理来封装我们的代码,以便通过查看 Google Cloud Web 控制台中 Stackdriver Logging 界面上显示的日志,更好地了解代码中可能出现故障的原因。
const pubSubMessage = req.body;
console.log(`PubSub message: ${JSON.stringify(pubSubMessage)}`);
const fileEvent = JSON.parse(Buffer.from(pubSubMessage.message.data, 'base64').toString().trim());
console.log(`Received thumbnail request for file ${fileEvent.name} from bucket ${fileEvent.bucket}`);
在 Cloud Run 平台上,Pub/Sub 消息通过 HTTP POST 请求发送,采用 JSON 载荷的形式,如下所示:
{
"message": {
"attributes": {
"bucketId": "uploaded-pictures",
"eventTime": "2020-02-27T09:22:43.255225Z",
"eventType": "OBJECT_FINALIZE",
"notificationConfig": "projects/_/buckets/uploaded-pictures/notificationConfigs/28",
"objectGeneration": "1582795363255481",
"objectId": "IMG_20200213_181159.jpg",
"payloadFormat": "JSON_API_V1"
},
"data": "ewogICJraW5kIjogInN0b3JhZ2Ujb2JqZWN...FQUU9Igp9Cg==",
"messageId": "1014308302773399",
"message_id": "1014308302773399",
"publishTime": "2020-02-27T09:22:43.973Z",
"publish_time": "2020-02-27T09:22:43.973Z"
},
"subscription": "projects/serverless-picadaily/subscriptions/gcs-events-subscription"
}
不过,此 JSON 文档中真正有趣的是 message.data 属性中包含的内容,它只是一个字符串,但会将实际载荷编码为 Base 64。这就是为什么上面的代码要对相应属性的 Base 64 内容进行解码。该 data 属性解码后会包含另一个 JSON 文档,其中包含 Cloud Storage 事件详细信息,包括文件名和存储分区名称等元数据。
{
"kind": "storage#object",
"id": "uploaded-pictures/IMG_20200213_181159.jpg/1582795363255481",
"selfLink": "https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg",
"name": "IMG_20200213_181159.jpg",
"bucket": "uploaded-pictures",
"generation": "1582795363255481",
"metageneration": "1",
"contentType": "image/jpeg",
"timeCreated": "2020-02-27T09:22:43.255Z",
"updated": "2020-02-27T09:22:43.255Z",
"storageClass": "STANDARD",
"timeStorageClassUpdated": "2020-02-27T09:22:43.255Z",
"size": "4944335",
"md5Hash": "QzBIoPJBV2EvqB1EVk1riw==",
"mediaLink": "https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg?generation=1582795363255481&alt=media",
"crc32c": "hQ3uHg==",
"etag": "CLmJhJu08ecCEAE="
}
我们感兴趣的是图片和存储分区名称,因为我们的代码将从存储分区中提取该图片以进行缩略图处理:
const bucket = storage.bucket(fileEvent.bucket);
const thumbBucket = storage.bucket(process.env.BUCKET_THUMBNAILS);
const originalFile = path.resolve('/tmp/original', fileEvent.name);
const thumbFile = path.resolve('/tmp/thumbnail', fileEvent.name);
await bucket.file(fileEvent.name).download({
destination: originalFile
});
console.log(`Downloaded picture into ${originalFile}`);
我们正在从环境变量中检索输出存储分区的名称。
我们有源存储分区(其文件创建触发了 Cloud Run 服务)和目标存储分区(用于存储生成的图片)。我们使用 path 内置 API 来处理本地文件,因为 imagemagick 库会在 /tmp 临时目录中本地创建缩略图。我们await了一个异步调用来下载已上传的图片文件。
const resizeCrop = Promise.promisify(im.crop);
await resizeCrop({
srcPath: originalFile,
dstPath: thumbFile,
width: 400,
height: 400
});
console.log(`Created local thumbnail in ${thumbFile}`);
imagemagick 模块对 async / await 的支持不太好,因此我们将其封装在 JavaScript Promise(由 Bluebird 模块提供)中。然后,我们使用源文件和目标文件的参数以及要创建的缩略图的尺寸,调用我们创建的异步调整大小 / 裁剪函数。
await thumbBucket.upload(thumbFile);
console.log(`Uploaded thumbnail to Cloud Storage bucket ${process.env.BUCKET_THUMBNAILS}`);
将缩略图文件上传到 Cloud Storage 后,我们还会更新 Cloud Firestore 中的元数据,以添加一个布尔值标志,表明相应图片的缩略图确实已生成:
const pictureStore = new Firestore().collection('pictures');
const doc = pictureStore.doc(fileEvent.name);
await doc.set({
thumbnail: true
}, {merge: true});
console.log(`Updated Firestore about thumbnail creation for ${fileEvent.name}`);
res.status(204).send(`${fileEvent.name} processed`);
请求结束后,我们会回复 HTTP POST 请求,告知文件已正确处理。
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Started thumbnail generator on port ${PORT}`);
});
在源文件的末尾,我们有让 Express 实际在 8080 默认端口上启动 Web 应用的指令。
7. 在本地测试
在部署到云端之前,请先在本地测试代码,确保其正常运行。
在 thumbnails/nodejs 文件夹中,安装 npm 依赖项并启动服务器:
npm install; npm start
如果一切顺利,它应该会在端口 8080 上启动服务器:
Started thumbnail generator on port 8080
使用 CTRL-C 退出。
8. 构建和发布容器映像
Cloud Run 会运行容器,但您首先需要构建容器映像(在 Dockerfile 中定义)。Google Cloud Build 可用于构建容器映像,然后将其托管到 Google Container Registry。
在 Dockerfile 所在的 thumbnails/nodejs 文件夹中,运行以下命令来构建容器映像:
gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service
一两分钟后,构建应该会成功:

Cloud Build 的“历史记录”部分也应显示成功构建:

点击 build ID 以获取详情视图,在“build artifacts”(构建制品)标签页中,您应该会看到容器映像已上传到 Cloud Registry (GCR):

如果您愿意,可以仔细检查容器映像是否在 Cloud Shell 中本地运行:
docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service
它应该会在容器中启动端口 8080 上的服务器:
Started thumbnail generator on port 8080
使用 CTRL-C 退出。
9. 部署到 Cloud Run
在部署到 Cloud Run 之前,请将 Cloud Run 区域设置为某个受支持的区域,并将平台设置为 managed:
gcloud config set run/region europe-west1 gcloud config set run/platform managed
您可以检查配置是否已设置:
gcloud config list ... [run] platform = managed region = europe-west1
运行以下命令,在 Cloud Run 上部署容器映像:
SERVICE_NAME=thumbnail-service
gcloud run deploy $SERVICE_NAME \
--image gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service \
--no-allow-unauthenticated \
--update-env-vars BUCKET_THUMBNAILS=$BUCKET_THUMBNAILS
请注意 --no-allow-unauthenticated 标志。这会使 Cloud Run 服务成为仅由特定服务账号触发的内部服务。
如果部署成功,您应该会看到以下输出:

如果您前往 Cloud 控制台界面,还应该会看到服务已成功部署:

10. 通过 Pub/Sub 将 Cloud Storage 事件发送到 Cloud Run
服务已准备就绪,但您仍需将 Cloud Storage 事件发送到新创建的 Cloud Run 服务。Cloud Storage 可以通过 Cloud Pub/Sub 发送文件创建事件,但需要执行几个步骤才能使其正常运行。
创建 Pub/Sub 主题作为通信管道:
TOPIC_NAME=cloudstorage-cloudrun-topic gcloud pubsub topics create $TOPIC_NAME
在存储分区中存储文件时创建 Pub/Sub 通知:
BUCKET_PICTURES=uploaded-pictures-$GOOGLE_CLOUD_PROJECT gsutil notification create -t $TOPIC_NAME -f json gs://$BUCKET_PICTURES
为稍后将创建的 Pub/Sub 订阅创建服务账号:
SERVICE_ACCOUNT=$TOPIC_NAME-sa
gcloud iam service-accounts create $SERVICE_ACCOUNT \
--display-name "Cloud Run Pub/Sub Invoker"
向服务账号授予调用 Cloud Run 服务的权限:
SERVICE_NAME=thumbnail-service gcloud run services add-iam-policy-binding $SERVICE_NAME \ --member=serviceAccount:$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com \ --role=roles/run.invoker
如果您在 2021 年 4 月 8 日或之前启用了 Pub/Sub 服务账号,请将 iam.serviceAccountTokenCreator 角色授予 Pub/Sub 服务账号:
PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
--member=serviceAccount:service-$PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
--role=roles/iam.serviceAccountTokenCreator
IAM 更改可能需要几分钟时间才能传播。
最后,使用该服务账号创建 Pub/Sub 订阅:
SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --format 'value(status.url)') gcloud pubsub subscriptions create $TOPIC_NAME-subscription --topic $TOPIC_NAME \ --push-endpoint=$SERVICE_URL \ --push-auth-service-account=$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com
您可以检查订阅是否已创建。前往控制台中的 Pub/Sub,选择 gcs-events 主题,然后在底部您应该会看到订阅:

11. 测试服务
如需测试设置是否正常运行,请将新图片上传到 uploaded-pictures 存储分区,然后在 thumbnails 存储分区中检查新调整大小的图片是否按预期显示。
您还可以仔细检查日志,查看在 Cloud Run 服务经历各个步骤时是否显示了日志记录消息:

12. 清理(可选)
如果您不打算继续学习本系列中的其他实验,可以清理资源,以节省费用,并践行良好的云资源管理实践。您可以按如下方式逐个清理资源。
删除存储分区:
gsutil rb gs://$BUCKET_THUMBNAILS
删除服务:
gcloud run services delete $SERVICE_NAME -q
删除 Pub/Sub 主题:
gcloud pubsub topics delete $TOPIC_NAME
或者,您也可以删除整个项目:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
13. 恭喜!
现在一切都已就绪:
- 在 Cloud Storage 中创建了一个通知,用于在新图片上传时在主题上发送 Pub/Sub 消息。
- 定义了所需的 IAM 绑定和账号(与 Cloud Functions 不同,后者是全自动的,而这里是手动配置的)。
- 创建了订阅,以便 Cloud Run 服务接收 Pub/Sub 消息。
- 每当有新图片上传到存储分区时,新 Cloud Run 服务都会调整图片大小。
所学内容
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub