每日图片:实验 2 - 创建图片缩略图

每日图片:实验 2 - 创建图片缩略图

关于此 Codelab

subject上次更新时间:11月 14, 2021
account_circleGuillaume Laforge, Mete Atamel 编写

1. 概览

在此 Codelab 中,您将在上一个实验的基础上进行构建,并添加缩略图服务。缩略图服务是一个网络容器,用于拍摄大照片并根据照片创建缩略图。

当照片上传到 Cloud Storage 时,Cloud Pub/Sub 会向 Cloud Run 网络容器发送一条通知,该容器随后会调整图片的大小并将其保存回 Cloud Storage 的另一个存储分区中。

31fa4f8a294d90df.png

学习内容

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. 设置和要求

自定进度的环境设置

  1. 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

96a9c957bc475304

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

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

启动 Cloud Shell

虽然可以通过笔记本电脑对 Google Cloud 进行远程操作,但在此 Codelab 中,您将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

在 GCP 控制台中,点击右上角工具栏上的 Cloud Shell 图标:

bce75f34b2c53987.png

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

f6ef2b5f13479f3a.png

这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 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

最后,您应该会得到一个新的公开存储分区:

“8e75c8099938e972”

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 按钮,使用内置文本编辑器:

3d145fe299dd8b3e

您也可以在专门的浏览器窗口中打开编辑器,从而获得更大的屏幕空间。

依赖项

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 / 节点 Web 框架。正文解析器模块用于轻松解析传入请求。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 网页控制台的 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 属性包含另一个表示 Cloud Storage 事件详细信息的 JSON 文档,该文档会以及其他元数据一起指示文件名和存储分区名称。

{
 
"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

一两分钟后,构建应该会成功:

b354b3a9a3631097.png

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

df00f198dd2bf6bf.png

在“build 工件”中点击 build ID 以获取详情视图您应该会看到容器映像已上传到 Cloud Registry (GCR):

a4577ce0744f73e2.png

如果需要,您可以仔细检查容器映像是否在 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 服务成为只能由特定服务账号触发的内部服务。

如果部署成功,您应该会看到以下输出:

c0f28e7d6de0024.png

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

9bfe48e3c8b597e5

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 主题,您应该会在底部看到订阅:

e8ab86dccb8d890.png

11. 测试服务

如需测试设置是否有效,请向 uploaded-pictures 存储分区上传一张新图片,并在 thumbnails 存储分区中检查调整后的新图片是否按预期显示。

在完成 Cloud Run 服务的各个步骤时,您还可以仔细检查日志以查看是否显示日志记录消息:

42c025e2d7d6ca3a

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

后续步骤