Pic-a-daily: ラボ 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.pngb9a10ebdf5b5a448.pnga1e3c01a38fa61c2.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 には、一意のプロジェクト ID に設定された GOOGLE_CLOUD_PROJECT がすでにあります。これをバケット名に追加できます。次に、均一なレベルのアクセス権を持つ公開マルチリージョン バケットをヨーロッパに作成します。

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 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 ウェブ アプリケーションを作成します。また、受信リクエストは実際にはアプリケーションへの 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 メッセージは次の形式の 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 属性に含まれるものです。これは単なる文字列ですが、実際のペイロードを Base64 にエンコードします。そのため、上記のコードでは、この属性の 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 サービスがトリガーされた元のバケットと、結果の画像を保存する宛先バケットがあります。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 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 Console UI に移動すると、サービスが正常にデプロイされたことも確認できます。

9bfe48e3c8b597e5.png

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 サービス アカウントを有効にした場合は、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

次のステップ