多拍一天:研究室 2:製作圖片縮圖

1. 總覽

在本程式碼研究室中,您將在先前的研究室中建構內容,並新增縮圖服務。縮圖服務是擷取大型圖片的網路容器,會從這些圖片建立縮圖。

圖片上傳至 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.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • 「專案名稱」是這項專案參與者的顯示名稱。這是 Google API 不使用的字元字串,您可以隨時更新。
  • 所有 Google Cloud 專案的專案 ID 均不得重複,且設定後即無法變更。Cloud 控制台會自動產生一個不重複的字串。但通常是在乎它何在在大部分的程式碼研究室中,您必須參照專案 ID (通常稱為 PROJECT_ID),因此如果您不喜歡的話,可以再隨機產生一個,或者,您也可以自行嘗試看看是否可用。是「凍結」建立專案後
  • 還有第三個值,也就是部分 API 使用的專案編號。如要進一步瞭解這三個值,請參閱說明文件
  1. 接下來,您需要在 Cloud 控制台中啟用計費功能,才能使用 Cloud 資源/API。執行這個程式碼研究室並不會產生任何費用,如果有的話。如要關閉資源,以免產生本教學課程結束後產生的費用,請按照任「清除所用資源」操作請參閱本程式碼研究室結尾處的操作說明。Google Cloud 的新使用者符合 $300 美元免費試用計畫的資格。

啟動 Cloud Shell

雖然 Google Cloud 可以從筆記型電腦遠端操作,但在本程式碼研究室中,您將使用 Google Cloud Shell,這是一種在 Cloud 中執行的指令列環境。

在 GCP 控制台的右上方,按一下「Cloud Shell」圖示:

bce75f34b2c53987.png

佈建並連線至環境的作業只需幾分鐘的時間。完成後,您應該會看到類似下方的內容:

f6ef2b5f13479f3a.png

這部虛擬機器都裝載了您需要的所有開發工具。提供永久的 5 GB 主目錄,而且在 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.png

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.png

您也可以在專用的瀏覽器視窗中開啟編輯器,使用更多螢幕空間。

依附元件

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 網路架構。主體剖析器模組可用來輕鬆剖析傳入的要求。Bluebird 用於處理承諾,而 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 模組,再透過 npm start 啟動程式碼。

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 網頁應用程式,並且指出我們要使用 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 訊息和下列格式的 JSON 酬載是透過 HTTP POST 要求傳送:

{
  "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 服務的來源值區,以及用來儲存結果圖片的目的地值區。由於 Imagemagick 程式庫會在本機的 /tmp 暫存目錄中建立縮圖,因此我們使用 path 內建 API 處理本機檔案。我們 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 承諾內 (由 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 預設連接埠啟動網頁應用程式。

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。

在包含 Dockerfilethumbnails/nodejs 資料夾中發出以下指令,建構容器映像檔:

gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service

經過一到兩分鐘後,建構作業應會成功:

b354b3a9a3631097.png

Cloud Build 的「歷史」部分應該也會顯示成功的版本:

df00f198dd2bf6bf.png

按一下建構 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 控制台 UI,應該也會看到服務已成功部署:

9bfe48e3c8b597e5.png

10. 透過 Pub/Sub 將 Cloud Storage 事件傳送至 Cloud Run

服務已準備就緒,但您仍需為新建立的 Cloud Run 服務設定 Cloud Storage 事件。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.png

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

後續步驟