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

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

この Codelab について

subject最終更新: 11月 14, 2021
account_circle作成者: Guillaume Laforge, Mete Atamel

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 ウェブ フレームワークです。ボディパーサー モジュールを使用すると、受信リクエストを簡単に解析できます。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);
   
}
});

これらの受信ペイロードを / ベース 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 属性の内容です。これは単なる文字列ですが、実際のペイロードを 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 の 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

次のステップ