Pic-a-daily: ラボ 2 - 写真のサムネイルを作成する

1. 概要

この Codelab では、前のラボを基に、サムネイル サービスを追加します。サムネイル サービスは、大きな画像を受け取ってサムネイルを作成するウェブ コンテナです。

写真が Cloud Storage にアップロードされると、Cloud Pub/Sub 経由で Cloud Run ウェブ コンテナに通知が送信されます。このコンテナは、画像のサイズを変更して、Cloud Storage の別のバケットに保存します。

31fa4f8a294d90df.png

学習内容

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. 設定と要件

セルフペース型の環境設定

  1. Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列で、いつでも更新できます。
  • プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud Console により一意の文字列が自動生成されます(通常は内容を意識する必要はありません)。ほとんどの Codelab では、プロジェクト ID を参照する必要があります(通常、プロジェクト ID は「PROJECT_ID」の形式です)。好みの文字列でない場合は、別のランダムな ID を生成するか、独自の ID を試用して利用可能であるかどうかを確認することができます。プロジェクトの作成後、ID は「フリーズ」されます。
  • 3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
  1. 次に、Cloud のリソースや API を使用するために、Cloud Console で課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルを終了した後に課金が発生しないようにリソースをシャットダウンするには、Codelab の最後にある「クリーンアップ」の手順を行います。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloud Shell の起動

Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では、Google Cloud Shell(Cloud 上で動作するコマンドライン環境)を使用します。

GCP Console で右上のツールバーにある 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 を使用して 2 番目のバケットを作成しましょう。

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 ウェブ フレームワークです。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 ウェブ アプリケーションを作成します。また、受信リクエストは実際には POST リクエストでアプリケーションに送信される JSON ペイロードであるため、JSON 本文パーサーを使用することも指定します。

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

これらの受信ペイロードは / ベース URL で受信され、コードはエラー ロジック処理でラップされています。これにより、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 属性をデコードすると、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 でウェブ アプリケーションを実際に起動する手順があります。

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

1 ~ 2 分後にビルドが成功します。

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 サービス アカウントを有効にした場合は、Pub/Sub サービス アカウントに iam.serviceAccountTokenCreator ロールを付与します。

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

次のコマンドで Service を削除します。

gcloud run services delete $SERVICE_NAME -q

Pub/Sub トピックを削除します。

gcloud pubsub topics delete $TOPIC_NAME

または、プロジェクト全体を削除することもできます。

gcloud projects delete $GOOGLE_CLOUD_PROJECT

13. 完了

これで、すべての準備が整いました。

  • 新しい写真がアップロードされたときに、トピックで Pub/Sub メッセージを送信する Cloud Storage の通知を作成しました。
  • 必要な IAM バインディングとアカウントを定義しました(すべてが自動化されている Cloud Functions とは異なり、ここでは手動で構成します)。
  • Cloud Run サービスが Pub/Sub メッセージを受信するようにサブスクリプションを作成しました。
  • 新しい写真がバケットにアップロードされるたびに、新しい Cloud Run サービスによって写真のサイズが変更されます。

学習した内容

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

次のステップ