1. 總覽
在本程式碼研究室中,您將以上一個實驗室為基礎,新增縮圖服務。縮圖服務是網頁容器,可接收大圖片並建立縮圖。
圖片上傳至 Cloud Storage 時,系統會透過 Cloud Pub/Sub 將通知傳送至 Cloud Run 網頁容器,然後容器會調整圖片大小,並將圖片儲存回 Cloud Storage 的另一個 bucket。

課程內容
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub
2. 設定和需求
自修實驗室環境設定
- 登入 Google Cloud 控制台,然後建立新專案或重複使用現有專案。如果沒有 Gmail 或 Google Workspace 帳戶,請先建立帳戶。



- 專案名稱是這個專案參與者的顯示名稱。這是 Google API 未使用的字元字串,您隨時可以更新。
- 專案 ID 在所有 Google Cloud 專案中不得重複,且設定後即無法變更。Cloud 控制台會自動產生專屬字串,通常您不需要在意該字串為何。在大多數程式碼研究室中,您需要參照專案 ID (通常會標示為
PROJECT_ID),因此如果您不喜歡該字串,可以產生另一個隨機字串,或是嘗試使用自己的字串,看看是否可用。專案建立後,系統就會「凍結」該值。 - 還有第三個值,也就是部分 API 使用的「專案編號」。如要進一步瞭解這三種值,請參閱說明文件。
- 接著,您需要在 Cloud 控制台中啟用帳單,才能使用 Cloud 資源/API。完成本程式碼研究室的費用應該不高,甚至完全免費。如要停用資源,避免在本教學課程結束後繼續產生帳單費用,請按照程式碼研究室結尾的「清除」操作說明操作。Google Cloud 新使用者可參加價值$300 美元的免費試用計畫。
啟動 Cloud Shell
雖然可以透過筆電遠端操作 Google Cloud,但在本程式碼研究室中,您將使用 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. 建立另一個 bucket
您會將上傳圖片的縮圖儲存在另一個 bucket。我們使用 gsutil 建立第二個 bucket。
在 Cloud Shell 中,為專屬 bucket 名稱設定變數。Cloud Shell 已將 GOOGLE_CLOUD_PROJECT 設為專屬專案 ID。你可以將該字串附加至 bucket 名稱。接著,在歐洲建立具有統一存取層級的公開多區域 bucket:
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 網頁架構,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 模組,然後以 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 訊息會透過 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。因此,上述程式碼會解碼這個屬性的 Base64 內容。解碼後的 data 屬性會包含另一個 JSON 文件,代表 Cloud Storage 事件詳細資料,其中包含檔案名稱和 bucket 名稱等中繼資料。
{
"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="
}
我們感興趣的是圖片和 bucket 名稱,因為程式碼會從 bucket 擷取該圖片,以進行縮圖處理:
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}`);
我們是從環境變數擷取輸出儲存空間 bucket 的名稱。
我們有來源 bucket (其檔案建立作業觸發了 Cloud Run 服務),以及儲存結果圖片的目的地 bucket。我們使用 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 預設通訊埠上實際啟動網頁應用程式。
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 的「記錄」部分也應會顯示建構作業成功:

按一下建構作業 ID 即可查看詳細資料,在「建構作業構件」分頁中,您應該會看到容器映像檔已上傳至 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 控制台 UI 時,您也應該會看到服務已成功部署:

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
在檔案儲存至 bucket 時建立 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 bucket,並在 thumbnails bucket 中檢查是否如預期顯示新的縮放圖片。
您也可以再次檢查記錄,查看 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 訊息。
- 每當有新圖片上傳至 bucket,新的 Cloud Run 服務就會調整圖片大小。
涵蓋內容
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub