Pic-a-daily: Lab 2 — Tạo hình thu nhỏ của ảnh

1. Tổng quan

Trong lớp học lập trình này, bạn sẽ tiếp tục với lớp học lập trình trước và thêm một dịch vụ hình thu nhỏ. Dịch vụ hình thu nhỏ là một vùng chứa trên web lấy các hình ảnh lớn và tạo hình thu nhỏ từ các hình ảnh đó.

Khi hình ảnh được tải lên Cloud Storage, một thông báo sẽ được gửi qua Cloud Pub/Sub đến một vùng chứa web Cloud Run. Vùng chứa này sau đó sẽ đổi kích thước hình ảnh và lưu lại vào một bộ chứa khác trong Cloud Storage.

31fa4f8a294d90df.png

Kiến thức bạn sẽ học được

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. Thiết lập và yêu cầu

Thiết lập môi trường theo tốc độ của riêng bạn

  1. Đăng nhập vào Google Cloud Console rồi tạo một dự án mới hoặc sử dụng lại một dự án hiện có. Nếu chưa có tài khoản Gmail hoặc Google Workspace, bạn phải tạo một tài khoản.

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • Tên dự án là tên hiển thị của những người tham gia dự án này. Đây là một chuỗi ký tự mà các API của Google không dùng và bạn có thể cập nhật chuỗi này bất cứ lúc nào.
  • Mã dự án phải là duy nhất trên tất cả các dự án trên Google Cloud và không thể thay đổi (bạn không thể thay đổi sau khi đã đặt). Cloud Console sẽ tự động tạo một chuỗi duy nhất; thường thì bạn không cần quan tâm đến chuỗi này. Trong hầu hết các lớp học lập trình, bạn sẽ cần tham chiếu đến Mã dự án (thường được xác định là PROJECT_ID). Vì vậy, nếu không thích mã này, bạn có thể tạo một mã ngẫu nhiên khác hoặc thử mã của riêng mình để xem mã đó có dùng được hay không. Sau đó, mã này sẽ "đóng băng" sau khi dự án được tạo.
  • Có một giá trị thứ ba là Số dự án mà một số API sử dụng. Tìm hiểu thêm về cả 3 giá trị này trong tài liệu.
  1. Tiếp theo, bạn cần bật tính năng thanh toán trong Cloud Console để sử dụng các tài nguyên/API trên Cloud. Việc thực hiện lớp học lập trình này sẽ không tốn nhiều chi phí, nếu có. Để tắt các tài nguyên nhằm tránh bị tính phí ngoài phạm vi hướng dẫn này, hãy làm theo mọi hướng dẫn "dọn dẹp" ở cuối lớp học lập trình. Người dùng mới của Google Cloud đủ điều kiện tham gia chương trình Dùng thử miễn phí trị giá 300 USD.

Khởi động Cloud Shell

Mặc dù có thể vận hành Google Cloud từ xa trên máy tính xách tay, nhưng trong lớp học lập trình này, bạn sẽ sử dụng Google Cloud Shell, một môi trường dòng lệnh chạy trên Cloud.

Trên Bảng điều khiển GCP, hãy nhấp vào biểu tượng Cloud Shell trên thanh công cụ ở trên cùng bên phải:

bce75f34b2c53987.png

Quá trình này chỉ mất vài phút để cung cấp và kết nối với môi trường. Khi quá trình này kết thúc, bạn sẽ thấy như sau:

f6ef2b5f13479f3a.png

Máy ảo này được trang bị tất cả các công cụ phát triển mà bạn cần. Nó cung cấp một thư mục chính có dung lượng 5 GB và chạy trên Google Cloud, giúp tăng cường đáng kể hiệu suất mạng và hoạt động xác thực. Bạn chỉ cần một trình duyệt là có thể thực hiện mọi thao tác trong phòng thí nghiệm này.

3. Bật API

Trong phòng thí nghiệm này, bạn sẽ cần Cloud Build để tạo hình ảnh vùng chứa và Cloud Run để triển khai vùng chứa.

Bật cả hai API này trong Cloud Shell:

gcloud services enable cloudbuild.googleapis.com \
  run.googleapis.com

Bạn sẽ thấy thao tác hoàn tất thành công:

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

4. Tạo một bộ chứa khác

Bạn sẽ lưu trữ hình thu nhỏ của những bức ảnh đã tải lên trong một nhóm khác. Hãy dùng gsutil để tạo nhóm thứ hai.

Trong Cloud Shell, hãy đặt một biến cho tên bộ chứa duy nhất. Cloud Shell đã đặt GOOGLE_CLOUD_PROJECT thành mã dự án duy nhất của bạn. Bạn có thể thêm thông tin đó vào tên nhóm. Sau đó, hãy tạo một vùng chứa công khai ở nhiều khu vực tại Châu Âu với quyền truy cập ở cấp độ đồng nhất:

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

Cuối cùng, bạn sẽ có một nhóm công khai mới:

8e75c8099938e972.png

5. Sao chép mã

Sao chép mã và chuyển đến thư mục chứa dịch vụ:

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
cd serverless-photosharing-workshop/services/thumbnails/nodejs

Bạn sẽ có bố cục tệp sau cho dịch vụ:

services
 |
 ├── thumbnails
      |
      ├── nodejs
           |
           ├── Dockerfile
           ├── index.js
           ├── package.json

Trong thư mục thumbnails/nodejs, bạn có 3 tệp:

  • index.js chứa mã Node.js
  • package.json xác định các phần phụ thuộc của thư viện
  • Dockerfile xác định hình ảnh vùng chứa

6. Khám phá mã

Để khám phá mã, bạn có thể sử dụng trình chỉnh sửa văn bản tích hợp bằng cách nhấp vào nút Open Editor ở đầu cửa sổ Cloud Shell:

3d145fe299dd8b3e.png

Bạn cũng có thể mở trình chỉnh sửa trong một cửa sổ trình duyệt riêng để có thêm không gian trên màn hình.

Phần phụ thuộc

Tệp package.json xác định các phần phụ thuộc cần thiết của thư viện:

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

Thư viện Cloud Storage được dùng để đọc và lưu các tệp hình ảnh trong Cloud Storage. Firestore để cập nhật siêu dữ liệu của hình ảnh. Express là một khung web JavaScript / Node. Mô-đun body-parser được dùng để dễ dàng phân tích cú pháp các yêu cầu đến. Bluebird được dùng để xử lý các promise và Imagemagick là một thư viện để thao tác với hình ảnh.

tệp Docker

Dockerfile xác định hình ảnh vùng chứa cho ứng dụng:

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

Hình ảnh cơ sở là Node 14 và thư viện imagemagick được dùng để thao tác với hình ảnh. Một số thư mục tạm thời được tạo để lưu trữ các tệp ảnh gốc và thu nhỏ. Sau đó, các mô-đun NPM mà mã của chúng ta cần sẽ được cài đặt trước khi bắt đầu mã bằng npm start.

index.js

Hãy khám phá mã theo từng phần để hiểu rõ hơn về những gì chương trình này đang làm.

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());

Trước tiên, chúng ta cần các phần phụ thuộc cần thiết và tạo ứng dụng web Express, cũng như cho biết rằng chúng ta muốn sử dụng trình phân tích cú pháp nội dung JSON, vì các yêu cầu đến thực ra chỉ là tải trọng JSON được gửi qua yêu cầu POST đến ứng dụng của chúng ta.

app.post('/', async (req, res) => {
    try {
        // ...
    } catch (err) {
        console.log(`Error: creating the thumbnail: ${err}`);
        console.error(err);
        res.status(500).send(err);
    }
});

Chúng tôi đang nhận những tải trọng đến trên URL cơ sở /, đồng thời bao bọc mã của mình bằng một số logic xử lý lỗi để có thông tin chính xác hơn về lý do có thể khiến một số nội dung không hoạt động trong mã của chúng tôi bằng cách xem nhật ký sẽ xuất hiện trên giao diện Stackdriver Logging trong bảng điều khiển web Google Cloud.

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}`);

Trên nền tảng Cloud Run, thông báo Pub/Sub được gửi qua các yêu cầu HTTP POST, dưới dạng tải trọng JSON có dạng:

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

Nhưng điều thực sự thú vị trong tài liệu JSON này là nội dung có trong thuộc tính message.data. Đây chỉ là một chuỗi nhưng mã hoá tải trọng thực tế thành Base 64. Đó là lý do mã ở trên của chúng tôi đang giải mã nội dung Base 64 của thuộc tính này. Thuộc tính data sau khi được giải mã sẽ chứa một tài liệu JSON khác biểu thị thông tin chi tiết về sự kiện Cloud Storage. Trong số các siêu dữ liệu khác, tài liệu này cho biết tên tệp và tên vùng chứa.

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

Chúng ta cần biết tên hình ảnh và tên nhóm, vì mã của chúng ta sẽ tìm nạp hình ảnh đó từ nhóm để xử lý hình thu nhỏ:

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}`);

Chúng ta đang truy xuất tên của vùng lưu trữ đầu ra từ một biến môi trường.

Chúng ta có bộ chứa nguồn mà việc tạo tệp đã kích hoạt dịch vụ Cloud Run và bộ chứa đích nơi chúng ta sẽ lưu trữ hình ảnh kết quả. Chúng tôi đang sử dụng API path tích hợp để xử lý tệp cục bộ, vì thư viện imagemagick sẽ tạo hình thu nhỏ cục bộ trong thư mục tạm thời /tmp. Chúng tôi await cho một lệnh gọi không đồng bộ để tải tệp hình ảnh đã tải lên xuống.

const resizeCrop = Promise.promisify(im.crop);
await resizeCrop({
        srcPath: originalFile,
        dstPath: thumbFile,
        width: 400,
        height: 400         
});
console.log(`Created local thumbnail in ${thumbFile}`);

Mô-đun imagemagick không thân thiện với async / await, vì vậy, chúng tôi sẽ gói mô-đun này trong một lời hứa của Javascript (do mô-đun Bluebird cung cấp). Sau đó, chúng ta sẽ gọi hàm thay đổi kích thước / cắt không đồng bộ mà chúng ta đã tạo bằng các tham số cho tệp nguồn và đích, cũng như kích thước của hình thu nhỏ mà chúng ta muốn tạo.

await thumbBucket.upload(thumbFile);
console.log(`Uploaded thumbnail to Cloud Storage bucket ${process.env.BUCKET_THUMBNAILS}`);

Sau khi tệp hình thu nhỏ được tải lên Cloud Storage, chúng ta cũng sẽ cập nhật siêu dữ liệu trong Cloud Firestore để thêm một cờ boolean cho biết rằng hình thu nhỏ của hình ảnh này thực sự được tạo:

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`);

Sau khi yêu cầu của chúng tôi kết thúc, chúng tôi sẽ trả lời yêu cầu HTTP POST rằng tệp đã được xử lý đúng cách.

const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
    console.log(`Started thumbnail generator on port ${PORT}`);
});

Ở cuối tệp nguồn, chúng ta có hướng dẫn để Express thực sự khởi động ứng dụng web trên cổng mặc định 8080.

7. Kiểm thử cục bộ

Kiểm thử mã cục bộ để đảm bảo mã hoạt động trước khi triển khai lên đám mây.

Trong thư mục thumbnails/nodejs, hãy cài đặt các phần phụ thuộc npm và khởi động máy chủ:

npm install; npm start

Nếu mọi thứ diễn ra suôn sẻ, thì máy chủ sẽ khởi động trên cổng 8080:

Started thumbnail generator on port 8080

Sử dụng CTRL-C để thoát.

8. Tạo và xuất bản hình ảnh vùng chứa

Cloud Run chạy các vùng chứa nhưng trước tiên bạn cần tạo hình ảnh vùng chứa (được xác định trong Dockerfile). Bạn có thể dùng Google Cloud Build để tạo hình ảnh vùng chứa rồi lưu trữ vào Google Container Registry.

Bên trong thư mục thumbnails/nodejsDockerfile, hãy phát lệnh sau để tạo hình ảnh vùng chứa:

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

Sau một hoặc hai phút, bản dựng sẽ thành công:

b354b3a9a3631097.png

Phần "nhật ký" của Cloud Build cũng sẽ cho thấy bản dựng thành công:

df00f198dd2bf6bf.png

Nhấp vào mã nhận dạng bản dựng để xem chế độ xem chi tiết. Trong thẻ "cấu phần phần mềm của bản dựng", bạn sẽ thấy rằng hình ảnh vùng chứa đã được tải lên Cloud Registry (GCR):

a4577ce0744f73e2.png

Nếu muốn, bạn có thể kiểm tra kỹ để đảm bảo rằng hình ảnh vùng chứa chạy cục bộ trong Cloud Shell:

docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service

Thao tác này sẽ khởi động máy chủ trên cổng 8080 trong vùng chứa:

Started thumbnail generator on port 8080

Sử dụng CTRL-C để thoát.

9. Triển khai lên Cloud Run

Trước khi triển khai lên Cloud Run, hãy đặt khu vực Cloud Run thành một trong các khu vực được hỗ trợ và nền tảng thành managed:

gcloud config set run/region europe-west1
gcloud config set run/platform managed

Bạn có thể kiểm tra để đảm bảo rằng cấu hình đã được thiết lập:

gcloud config list

...
[run]
platform = managed
region = europe-west1

Chạy lệnh sau để triển khai hình ảnh vùng chứa trên 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

Lưu ý cờ --no-allow-unauthenticated. Điều này khiến dịch vụ Cloud Run trở thành một dịch vụ nội bộ và chỉ được kích hoạt bởi các tài khoản dịch vụ cụ thể.

Nếu triển khai thành công, bạn sẽ thấy kết quả sau:

c0f28e7d6de0024.png

Nếu truy cập vào giao diện người dùng bảng điều khiển đám mây, bạn cũng sẽ thấy rằng dịch vụ đã được triển khai thành công:

9bfe48e3c8b597e5.png

10. Sự kiện Cloud Storage đến Cloud Run thông qua Pub/Sub

Dịch vụ đã sẵn sàng, nhưng bạn vẫn cần tạo các sự kiện Cloud Storage cho dịch vụ Cloud Run mới tạo. Cloud Storage có thể gửi các sự kiện tạo tệp thông qua Cloud Pub/Sub, nhưng bạn cần thực hiện một số bước để tính năng này hoạt động.

Tạo một chủ đề Pub/Sub làm đường ống giao tiếp:

TOPIC_NAME=cloudstorage-cloudrun-topic
gcloud pubsub topics create $TOPIC_NAME

Tạo thông báo Pub/Sub khi tệp được lưu trữ trong vùng lưu trữ:

BUCKET_PICTURES=uploaded-pictures-$GOOGLE_CLOUD_PROJECT
gsutil notification create -t $TOPIC_NAME -f json gs://$BUCKET_PICTURES

Tạo một tài khoản dịch vụ cho gói thuê bao Pub/Sub mà chúng ta sẽ tạo sau:

SERVICE_ACCOUNT=$TOPIC_NAME-sa
gcloud iam service-accounts create $SERVICE_ACCOUNT \
     --display-name "Cloud Run Pub/Sub Invoker"

Cấp cho tài khoản dịch vụ quyền gọi một dịch vụ 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

Nếu bạn đã bật tài khoản dịch vụ Pub/Sub vào hoặc trước ngày 8 tháng 4 năm 2021, hãy cấp vai trò iam.serviceAccountTokenCreator cho tài khoản dịch vụ 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

Có thể mất vài phút để các thay đổi về IAM có hiệu lực.

Cuối cùng, hãy tạo một gói thuê bao Pub/Sub bằng tài khoản dịch vụ:

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

Bạn có thể kiểm tra để đảm bảo rằng gói thuê bao đã được tạo. Chuyển đến Pub/Sub trong bảng điều khiển, chọn chủ đề gcs-events và ở dưới cùng, bạn sẽ thấy gói thuê bao:

e8ab86dccb8d890.png

11. Kiểm thử dịch vụ

Để kiểm thử xem chế độ thiết lập có hoạt động hay không, hãy tải một hình ảnh mới lên vùng chứa uploaded-pictures và kiểm tra trong vùng chứa thumbnails để xem hình ảnh mới được đổi kích thước có xuất hiện như mong đợi hay không.

Bạn cũng có thể kiểm tra nhật ký để xem các thông báo ghi nhật ký xuất hiện khi các bước khác nhau của dịch vụ Cloud Run đang diễn ra:

42c025e2d7d6ca3a.png

12. Dọn dẹp (Không bắt buộc)

Nếu không có ý định tiếp tục với các phòng thí nghiệm khác trong loạt bài này, bạn có thể dọn dẹp tài nguyên để tiết kiệm chi phí và trở thành một công dân đám mây tốt. Bạn có thể dọn dẹp từng tài nguyên riêng lẻ như sau.

Xoá bộ chứa:

gsutil rb gs://$BUCKET_THUMBNAILS

Xoá dịch vụ:

gcloud run services delete $SERVICE_NAME -q

Xoá chủ đề Pub/Sub:

gcloud pubsub topics delete $TOPIC_NAME

Ngoài ra, bạn có thể xoá toàn bộ dự án:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

13. Xin chúc mừng!

Mọi thứ hiện đã sẵn sàng:

  • Tạo một thông báo trong Cloud Storage để gửi thông báo Pub/Sub về một chủ đề khi có ảnh mới được tải lên.
  • Xác định các liên kết và tài khoản IAM bắt buộc (không giống như Cloud Functions, nơi mọi thứ đều được tự động hoá, ở đây, bạn phải định cấu hình theo cách thủ công).
  • Tạo một gói thuê bao để dịch vụ Cloud Run của chúng tôi nhận được thông báo Pub/Sub.
  • Bất cứ khi nào một bức ảnh mới được tải lên nhóm, bức ảnh đó sẽ được đổi kích thước nhờ dịch vụ Cloud Run mới.

Nội dung đã đề cập

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Các bước tiếp theo