Pic-a-daily: Google ネイティブ Java クライアント ライブラリを使用した画像の保存と分析

1. 概要

最初の Codelab では、画像をバケットに保存します。これにより、Cloud Run にデプロイされたサービスによって処理されるファイル作成イベントが生成されます。このサービスは Vision API を呼び出して画像分析を行い、結果をデータストアに保存します。

427de3100de3a61e.png

学習内容

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore

2. 設定と要件

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

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

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列です。この値はいつでも更新できます。
  • プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud コンソールでは一意の文字列が自動生成されます。通常、それが何であるかは関係ありません。ほとんどの Codelab では、プロジェクト ID を参照する必要があります(通常は PROJECT_ID として識別されます)。生成された ID が気に入らない場合は、別のランダムな ID を生成できます。または、ご自身でお試しになることもできます。このステップを終えた後は変更できず、プロジェクト期間中は維持されます。
  • なお、3 つ目の値は、一部の API で使用される [プロジェクト番号] です。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
  1. 次に、Cloud のリソースや API を使用するために、Cloud コンソールで課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアル以降課金が発生しないようにリソースをシャットダウンするには、作成したリソースを削除するか、プロジェクト全体を削除します。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloud Shell の起動

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

Google Cloud Console で、右上のツールバーにある Cloud Shell アイコンをクリックします。

55efc1aaa7a4d3ad.png

プロビジョニングと環境への接続にはそれほど時間はかかりません。完了すると、次のように表示されます。

7ffe5cbb04455448.png

この仮想マシンには、必要な開発ツールがすべて用意されています。永続的なホーム ディレクトリが 5 GB 用意されており、Google Cloud で稼働します。そのため、ネットワークのパフォーマンスと認証機能が大幅に向上しています。この Codelab での作業はすべて、ブラウザ内から実行できます。インストールは不要です。

3. API を有効にする

このラボでは Cloud Functions と Vision API を使用しますが、まず、これらを Cloud コンソールまたは gcloud で有効にする必要があります。

Cloud コンソールで Vision API を有効にするには、検索バーで「Cloud Vision API」を検索します。

cf48b1747ba6a6fb.png

Cloud Vision API のページが表示されます。

ba4af419e6086fbb.png

[ENABLE] ボタンをクリックします。

または、gcloud コマンドライン ツールを使用して Cloud Shell で有効にすることもできます。

Cloud Shell で、次のコマンドを実行します。

gcloud services enable vision.googleapis.com

オペレーションが正常に完了することを確認できます。

Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.

Cloud Run と Cloud Build も有効にします。

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

4. バケットを作成する(コンソール)

画像用の Storage バケットを作成します。これを行うには、Google Cloud Platform コンソール(console.cloud.google.com)を使用するか、Cloud Shell またはローカル開発環境の gsutil コマンドライン ツールを使用します。

「ハンバーガー」から(✱)メニューから [Storage] ページに移動します。

1930e055d138150a.png

バケットに名前を付ける

[CREATE BUCKET] ボタンをクリックします。

34147939358517f8.png

[CONTINUE] をクリックします。

ロケーションを選択

197817f20be07678.png

任意のリージョン(ここでは Europe)にマルチリージョン バケットを作成します。

[CONTINUE] をクリックします。

デフォルトのストレージ クラスを選択する

53cd91441c8caf0e.png

データのストレージ クラスとして Standard を選択します。

[CONTINUE] をクリックします。

アクセス制御の設定

8c2b3b459d934a51.png

一般公開されているイメージを扱うため、このバケットに保存されているすべての写真に同じ均一なアクセス制御を適用する必要があります。

Uniform アクセス制御オプションを選択します。

[CONTINUE] をクリックします。

保護/暗号化の設定

d931c24c3e705a68.png

デフォルトのままにします(独自の暗号鍵は使用しないため、Google-managed key)

CREATE をクリックして、最終的にバケットの作成を完了します。

allUsers をストレージ閲覧者として追加する

[Permissions] タブに移動します。

d0ecfdcff730ea51.png

次のように、Storage > Storage Object Viewer のロールを持つ allUsers メンバーをバケットに追加します。

e9f25ec1ea0b6cc6.png

[SAVE] をクリックします。

5. バケットを作成する(gsutil)

Cloud Shell の gsutil コマンドライン ツールを使用してバケットを作成することもできます。

Cloud Shell で、一意のバケット名の変数を設定します。Cloud Shell には、GOOGLE_CLOUD_PROJECT に一意のプロジェクト ID がすでに設定されています。これをバケット名に追加できます。

例:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

ヨーロッパに標準マルチリージョン ゾーンを作成します。

gsutil mb -l EU gs://${BUCKET_PICTURES}

均一なバケットレベルのアクセスを確保する:

gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}

バケットを公開します。

gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}

コンソールの Cloud Storage セクションに移動すると、公開されている uploaded-pictures バケットがあるはずです。

a98ed4ba17873e40.png

前のステップで説明したように、バケットに画像をアップロードできることと、アップロードされた画像が一般公開されることをテストします。

6. バケットへの公開アクセスをテストする

Storage ブラウザに戻ると、バケットに [Public] と表示されているバケットがリストに表示されています。アクセス(そのバケットのコンテンツには誰でもアクセスできることを示す警告マークを含む)を付与する必要があります。

89e7a4d2c80a0319.png

これで、バケットで写真を受け取る準備が整いました。

バケット名をクリックすると、バケットの詳細が表示されます。

131387f12d3eb2d3.png

そこで Upload files ボタンをクリックして、バケットに画像を追加できるかどうかテストできます。ファイル選択ツールのポップアップが表示され、ファイルの選択を求められます。選択すると、ファイルがバケットにアップロードされ、この新しいファイルに自動的に割り当てられた public アクセス権が再び表示されます。

e87584471a6e9c6d.png

Public アクセスラベルの横には、小さなリンクアイコンも表示されます。その画像をクリックすると、ブラウザはその画像の公開 URL(次の形式)に移動します。

https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png

BUCKET_NAME はバケットに選択したグローバルに一意の名前と、画像のファイル名です。

画像名の横にあるチェックボックスをオンにすると、[DELETE] ボタンが有効になり、この最初の画像を削除できます。

7. データベースを準備する

Vision API から提供された写真に関する情報を Cloud Firestore データベースに保存します。Cloud Firestore データベースは、高速、フルマネージド、サーバーレス、クラウドネイティブの NoSQL ドキュメント データベースです。Cloud コンソールの Firestore セクションに移動して、データベースを準備します。

9e4708d2257de058.png

Native mode または Datastore mode の 2 つのオプションがあります。オフライン サポートやリアルタイム同期などの追加機能を備えたネイティブ モードを使用する。

SELECT NATIVE MODE をクリックします。

9449ace8cc84de43.png

マルチリージョンを選択します(ここではヨーロッパですが、関数とストレージ バケットと同じリージョンが理想的です)。

[CREATE DATABASE] ボタンをクリックします。

データベースが作成されると、次のように表示されます。

56265949a124819e.png

[+ START COLLECTION] ボタンをクリックして、新しいコレクションを作成します。

コレクション pictures に名前を付けます。

75806ee24c4e13a7.png

ドキュメントを作成する必要はありません。新しい画像が Cloud Storage に保存され、Vision API によって分析されるときに、これらの画像をプログラムで追加します。

[Save] をクリックします。

Firestore は、新しく作成されたコレクションに最初のデフォルト ドキュメントを作成します。このドキュメントには有用な情報が含まれていないため、安全に削除できます。

5c2f1e17ea47f48f.png

このコレクションでプログラムによって作成されるドキュメントには、次の 4 つのフィールドが含まれます。

  • name(文字列): アップロードされた画像のファイル名。ドキュメントのキーでもあります。
  • labels(文字列の配列): Vision API によって認識されたアイテムのラベル
  • color(文字列): ドミナント カラーの 16 進数のカラーコード(#ab12ef)
  • 作成日(日付): この画像のメタデータが保存されたときのタイムスタンプ
  • thumbnail(ブール値): この画像のサムネイル画像が生成された場合に true になるオプション フィールド。

Firestore でサムネイルのある画像を検索し、作成日で並べ替えるため、検索インデックスを作成する必要があります。

インデックスを作成するには、Cloud Shell で次のコマンドを使用します。

gcloud firestore indexes composite create \
  --collection-group=pictures \
  --field-config field-path=thumbnail,order=descending \
  --field-config field-path=created,order=descending

または、Cloud コンソールで左側のナビゲーション列で [Indexes] をクリックし、以下のように複合インデックスを作成することでも作成できます。

ecb8b95e3c791272.png

[Create] をクリックします。インデックスの作成には数分かかることがあります。

8. コードのクローンを作成する

前の Codelab をまだ実行していない場合は、コードのクローンを作成します。

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop

次に、サービスを含むディレクトリに移動して、ラボの作成を開始できます。

cd serverless-photosharing-workshop/services/image-analysis/java

サービスのファイル レイアウトは次のようになります。

f79613aff479d8ad.png

9. サービスコードを確認する

まず、BOM を使用して、pom.xml で Java クライアント ライブラリを有効にする方法を確認します。

まず、Java 関数の依存関係のリストを含む pom.xml ファイルを編集します。コードを更新して、Cloud Vision API Maven 依存関係を追加します。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>cloudfunctions</groupId>
  <artifactId>gcs-function</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.target>11</maven.compiler.target>
    <maven.compiler.source>11</maven.compiler.source>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>26.1.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>com.google.cloud.functions</groupId>
      <artifactId>functions-framework-api</artifactId>
      <version>1.0.4</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-firestore</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-storage</artifactId>
    </dependency>
  </dependencies>

この機能は EventController クラスに実装されています。新しい画像がバケットにアップロードされるたびに、サービスは次の処理を求める通知を受け取ります。

@RestController
public class EventController {
  private static final Logger logger = Logger.getLogger(EventController.class.getName());
    
  private static final List<String> requiredFields = Arrays.asList("ce-id", "ce-source", "ce-type", "ce-specversion");

  @RequestMapping(value = "/", method = RequestMethod.POST)
  public ResponseEntity<String> receiveMessage(
    @RequestBody Map<String, Object> body, @RequestHeader Map<String, String> headers) throws IOException, InterruptedException, ExecutionException {
...
}

コードは Cloud Events ヘッダーの検証に進みます。

System.out.println("Header elements");
for (String field : requiredFields) {
    if (headers.get(field) == null) {
    String msg = String.format("Missing expected header: %s.", field);
    System.out.println(msg);
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
    } else {
    System.out.println(field + " : " + headers.get(field));
    }
}

System.out.println("Body elements");
for (String bodyField : body.keySet()) {
    System.out.println(bodyField + " : " + body.get(bodyField));
}

if (headers.get("ce-subject") == null) {
    String msg = "Missing expected header: ce-subject.";
    System.out.println(msg);
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
} 

これでリクエストを作成できるようになりました。コードは、そのようなリクエストの 1 つを準備し、Vision API に送信します。

try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
    List<AnnotateImageRequest> requests = new ArrayList<>();
    
    ImageSource imageSource = ImageSource.newBuilder()
        .setGcsImageUri("gs://" + bucketName + "/" + fileName)
        .build();

    Image image = Image.newBuilder()
        .setSource(imageSource)
        .build();

    Feature featureLabel = Feature.newBuilder()
        .setType(Type.LABEL_DETECTION)
        .build();
    Feature featureImageProps = Feature.newBuilder()
        .setType(Type.IMAGE_PROPERTIES)
        .build();
    Feature featureSafeSearch = Feature.newBuilder()
        .setType(Type.SAFE_SEARCH_DETECTION)
        .build();
        
    AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
        .addFeatures(featureLabel)
        .addFeatures(featureImageProps)
        .addFeatures(featureSafeSearch)
        .setImage(image)
        .build();
    
    requests.add(request);

Vision API には、

  • ラベル検出: 画像の内容を把握
  • 画像プロパティ: 画像の興味深い属性を指定します(画像の主な色を確認できます)。
  • セーフサーチ: 画像が表示しても安全かどうかを知ることができます(アダルト / 医療 / 際どい / 暴力的なコンテンツを含む画像は使用できません)。

この時点で、Vision API を呼び出せます。

...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...

参考までに、Vision API からのレスポンスは次のようになります。

{
  "faceAnnotations": [],
  "landmarkAnnotations": [],
  "logoAnnotations": [],
  "labelAnnotations": [
    {
      "locations": [],
      "properties": [],
      "mid": "/m/01yrx",
      "locale": "",
      "description": "Cat",
      "score": 0.9959855675697327,
      "confidence": 0,
      "topicality": 0.9959855675697327,
      "boundingPoly": null
    },
    ✄ - - - ✄
  ],
  "textAnnotations": [],
  "localizedObjectAnnotations": [],
  "safeSearchAnnotation": {
    "adult": "VERY_UNLIKELY",
    "spoof": "UNLIKELY",
    "medical": "VERY_UNLIKELY",
    "violence": "VERY_UNLIKELY",
    "racy": "VERY_UNLIKELY",
    "adultConfidence": 0,
    "spoofConfidence": 0,
    "medicalConfidence": 0,
    "violenceConfidence": 0,
    "racyConfidence": 0,
    "nsfwConfidence": 0
  },
  "imagePropertiesAnnotation": {
    "dominantColors": {
      "colors": [
        {
          "color": {
            "red": 203,
            "green": 201,
            "blue": 201,
            "alpha": null
          },
          "score": 0.4175916016101837,
          "pixelFraction": 0.44456374645233154
        },
        ✄ - - - ✄
      ]
    }
  },
  "error": null,
  "cropHintsAnnotation": {
    "cropHints": [
      {
        "boundingPoly": {
          "vertices": [
            { "x": 0, "y": 118 },
            { "x": 1177, "y": 118 },
            { "x": 1177, "y": 783 },
            { "x": 0, "y": 783 }
          ],
          "normalizedVertices": []
        },
        "confidence": 0.41695669293403625,
        "importanceFraction": 1
      }
    ]
  },
  "fullTextAnnotation": null,
  "webDetection": null,
  "productSearchResults": null,
  "context": null
}

エラーが返されない場合は先に進みます。これが if ブロックになっている理由です。

if (responses.size() == 0) {
    logger.info("No response received from Vision API.");
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}

AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
    logger.info("Error: " + response.getError().getMessage());
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}

写真内で認識されたもの、カテゴリ、テーマのラベルを入手します。

List<String> labels = response.getLabelAnnotationsList().stream()
    .map(annotation -> annotation.getDescription())
    .collect(Collectors.toList());
logger.info("Annotations found:");
for (String label: labels) {
    logger.info("- " + label);
}

写真の主な色を知りたいのです。

String mainColor = "#FFFFFF";
ImageProperties imgProps = response.getImagePropertiesAnnotation();
if (imgProps.hasDominantColors()) {
    DominantColorsAnnotation colorsAnn = imgProps.getDominantColors();
    ColorInfo colorInfo = colorsAnn.getColors(0);

    mainColor = rgbHex(
        colorInfo.getColor().getRed(), 
        colorInfo.getColor().getGreen(), 
        colorInfo.getColor().getBlue());

    logger.info("Color: " + mainColor);
}

画像が安全に見せることができるか確認しましょう。

boolean isSafe = false;
if (response.hasSafeSearchAnnotation()) {
    SafeSearchAnnotation safeSearch = response.getSafeSearchAnnotation();

    isSafe = Stream.of(
        safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
        safeSearch.getSpoof(), safeSearch.getViolence())
    .allMatch( likelihood -> 
        likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
    );

    logger.info("Safe? " + isSafe);
}

成人 / なりすまし / 医療 / 暴力 / 際どい特徴について、その可能性非常に高いかどうかを確認しています。

セーフサーチの結果に問題がなければ、メタデータを Firestore に保存できます。

// Saving result to Firestore
if (isSafe) {
    FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
    Firestore pictureStore = firestoreOptions.getService();

    DocumentReference doc = pictureStore.collection("pictures").document(fileName);

    Map<String, Object> data = new HashMap<>();
    data.put("labels", labels);
    data.put("color", mainColor);
    data.put("created", new Date());

    ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());

    logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
}

10. GraalVM を使用してアプリイメージをビルドする(省略可)

このオプションのステップでは、GraalVM を使用して JIT(JVM) based app image をビルドし、次に AOT(Native) Java app image をビルドします。

ビルドを実行するには、適切な JDK とネイティブ イメージ ビルダーがインストールされ、構成されていることを確認する必要があります。いくつかのオプションがあります。

To start の場合は、GraalVM 22.2.x Community Edition をダウンロードし、GraalVM インストール ページの手順に沿って操作します。

このプロセスは、SDKMAN!

SDKman を使用して適切な JDK ディストリビューションをインストールするには、まず install コマンドを使用します。

sdk install java 22.2.r17-grl

JIT ビルドと AOT ビルドの両方でこのバージョンを使用するように SDKman に指示します。

sdk use java 22.2.0.r17-grl

GraalVM 用の native-image utility をインストールします。

gu install native-image

Cloudshell では、次の簡単なコマンドを使用して GraalVM とネイティブ イメージ ユーティリティをインストールできます。

# install GraalVM in your home directory
cd ~

# download GraalVM
wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.2.0/graalvm-ce-java17-linux-amd64-22.2.0.tar.gz
ls
tar -xzvf graalvm-ce-java17-linux-amd64-22.2.0.tar.gz

# configure Java 17 and GraalVM 22.2
echo Existing JVM: $JAVA_HOME
cd graalvm-ce-java17-22.2.0
export JAVA_HOME=$PWD
cd bin
export PATH=$PWD:$PATH

echo JAVA HOME: $JAVA_HOME
echo PATH: $PATH

# install the native image utility
java -version
gu install native-image

cd ../..

まず、GCP プロジェクトの環境変数を設定します。

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)

次に、サービスを含むディレクトリに移動して、ラボの作成を開始できます。

cd serverless-photosharing-workshop/services/image-analysis/java

JIT(JVM)アプリケーション イメージをビルドします。

./mvnw package -Pjvm

ターミナルでビルドログを確認します。

...
[INFO] --- spring-boot-maven-plugin:2.7.3:repackage (repackage) @ image-analysis ---
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  24.009 s
[INFO] Finished at: 2022-09-26T22:17:32-04:00
[INFO] ------------------------------------------------------------------------

AOT(ネイティブ)イメージをビルドします。

./mvnw package -Pnative -DskipTests

ターミナルで、ネイティブ イメージのビルドログを含むビルドログを確認します。

テストに使用するマシンによっては、ビルドにかなりの時間がかかります。

...
[2/7] Performing analysis...  [**********]                                                              (95.4s @ 3.57GB)
  23,346 (94.42%) of 24,725 classes reachable
  44,625 (68.71%) of 64,945 fields reachable
 163,759 (70.79%) of 231,322 methods reachable
     989 classes, 1,402 fields, and 11,032 methods registered for reflection
      63 classes,    69 fields, and    55 methods registered for JNI access
       5 native libraries: -framework CoreServices, -framework Foundation, dl, pthread, z
[3/7] Building universe...                                                                              (10.0s @ 5.35GB)
[4/7] Parsing methods...      [***]                                                                      (9.7s @ 3.13GB)
[5/7] Inlining methods...     [***]                                                                      (4.5s @ 3.29GB)
[6/7] Compiling methods...    [[6/7] Compiling methods...    [********]                                                                (67.6s @ 5.72GB)
[7/7] Creating image...                                                                                  (8.7s @ 4.59GB)
  62.21MB (54.80%) for code area:   100,371 compilation units
  50.98MB (44.91%) for image heap:  465,035 objects and 365 resources
 337.09KB ( 0.29%) for other data
 113.52MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area:                               Top 10 object types in image heap:
   2.36MB com.google.protobuf                                 12.70MB byte[] for code metadata
   1.90MB i.g.xds.shaded.io.envoyproxy.envoy.config.core.v3    6.66MB java.lang.Class
   1.73MB i.g.x.shaded.io.envoyproxy.envoy.config.route.v3     6.47MB byte[] for embedded resources
   1.67MB sun.security.ssl                                     4.61MB byte[] for java.lang.String
   1.54MB com.google.cloud.vision.v1                           4.37MB java.lang.String
   1.46MB com.google.firestore.v1                              3.38MB byte[] for general heap data
   1.37MB io.grpc.xds.shaded.io.envoyproxy.envoy.api.v2.core   1.96MB com.oracle.svm.core.hub.DynamicHubCompanion
   1.32MB i.g.xds.shaded.io.envoyproxy.envoy.api.v2.route      1.80MB byte[] for reflection metadata
   1.09MB java.util                                          911.80KB java.lang.String[]
   1.08MB com.google.re2j                                    826.48KB c.o.svm.core.hub.DynamicHub$ReflectionMetadata
  45.91MB for 772 more packages                                6.45MB for 3913 more object types
------------------------------------------------------------------------------------------------------------------------
                        15.1s (6.8% of total time) in 56 GCs | Peak RSS: 7.72GB | CPU load: 4.37
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /Users/ddobrin/work/dan/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis (executable)
 /Users/ddobrin/work/dan/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis.build_artifacts.txt (txt)
========================================================================================================================
Finished generating 'image-analysis' in 3m 41s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:56 min
[INFO] Finished at: 2022-09-26T22:22:29-04:00
[INFO] ------------------------------------------------------------------------

11. コンテナ イメージをビルドして公開する

コンテナ イメージを 2 つの異なるバージョン(JIT(JVM) imageAOT(Native) Java image)でビルドします。

まず、GCP プロジェクトの環境変数を設定します。

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)

JIT(JVM)イメージをビルドします。

./mvnw package -Pjvm-image

ターミナルでビルドログを確認します。

[INFO]     [creator]     Adding layer 'process-types'
[INFO]     [creator]     Adding label 'io.buildpacks.lifecycle.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.build.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.project.metadata'
[INFO]     [creator]     Adding label 'org.opencontainers.image.title'
[INFO]     [creator]     Adding label 'org.opencontainers.image.version'
[INFO]     [creator]     Adding label 'org.springframework.boot.version'
[INFO]     [creator]     Setting default process type 'web'
[INFO]     [creator]     Saving docker.io/library/image-analysis-jvm:r17...
[INFO]     [creator]     *** Images (03a44112456e):
[INFO]     [creator]           docker.io/library/image-analysis-jvm:r17
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-jvm:r17'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:11 min
[INFO] Finished at: 2022-09-26T13:09:34-04:00
[INFO] ------------------------------------------------------------------------

AOT(ネイティブ)イメージをビルドします。

./mvnw package -Pnative-image

ターミナルで、ネイティブ イメージのビルドログや UPX を使用したイメージ圧縮などのビルドログを確認します。

テストに使用するマシンによっては、ビルドにかなりの時間がかかることに注意してください。

...
[INFO]     [creator]     [2/7] Performing analysis...  [***********]                    (147.6s @ 3.10GB)
[INFO]     [creator]       23,362 (94.34%) of 24,763 classes reachable
[INFO]     [creator]       44,657 (68.67%) of 65,029 fields reachable
[INFO]     [creator]      163,926 (70.76%) of 231,656 methods reachable
[INFO]     [creator]          981 classes, 1,402 fields, and 11,026 methods registered for reflection
[INFO]     [creator]           63 classes,    68 fields, and    55 methods registered for JNI access
[INFO]     [creator]            4 native libraries: dl, pthread, rt, z
[INFO]     [creator]     [3/7] Building universe...                                      (21.1s @ 2.66GB)
[INFO]     [creator]     [4/7] Parsing methods...      [****]                            (13.7s @ 4.16GB)
[INFO]     [creator]     [5/7] Inlining methods...     [***]                              (9.6s @ 4.20GB)
[INFO]     [creator]     [6/7] Compiling methods...    [**********]                     (107.6s @ 3.36GB)
[INFO]     [creator]     [7/7] Creating image...                                         (14.7s @ 4.87GB)
[INFO]     [creator]       62.24MB (51.35%) for code area:   100,499 compilation units
[INFO]     [creator]       51.99MB (42.89%) for image heap:  473,948 objects and 473 resources
[INFO]     [creator]        6.98MB ( 5.76%) for other data
[INFO]     [creator]      121.21MB in total
[INFO]     [creator]     --------------------------------------------------------------------------------
[INFO]     [creator]     Top 10 packages in code area:           Top 10 object types in image heap:
[INFO]     [creator]        2.36MB com.google.protobuf             12.71MB byte[] for code metadata
[INFO]     [creator]        1.90MB i.g.x.s.i.e.e.config.core.v3     7.59MB byte[] for embedded resources
[INFO]     [creator]        1.73MB i.g.x.s.i.e.e.config.route.v3    6.66MB java.lang.Class
[INFO]     [creator]        1.67MB sun.security.ssl                 4.62MB byte[] for java.lang.String
[INFO]     [creator]        1.54MB com.google.cloud.vision.v1       4.39MB java.lang.String
[INFO]     [creator]        1.46MB com.google.firestore.v1          3.66MB byte[] for general heap data
[INFO]     [creator]        1.37MB i.g.x.s.i.e.envoy.api.v2.core    1.96MB c.o.s.c.h.DynamicHubCompanion
[INFO]     [creator]        1.32MB i.g.x.s.i.e.e.api.v2.route       1.80MB byte[] for reflection metadata
[INFO]     [creator]        1.09MB java.util                      910.41KB java.lang.String[]
[INFO]     [creator]        1.08MB com.google.re2j                826.95KB c.o.s.c.h.DynamicHu~onMetadata
[INFO]     [creator]       45.94MB for 776 more packages            6.69MB for 3916 more object types
[INFO]     [creator]     --------------------------------------------------------------------------------
[INFO]     [creator]         20.4s (5.6% of total time) in 81 GCs | Peak RSS: 6.75GB | CPU load: 4.53
[INFO]     [creator]     --------------------------------------------------------------------------------
[INFO]     [creator]     Produced artifacts:
[INFO]     [creator]      /layers/paketo-buildpacks_native-image/native-image/services.ImageAnalysisApplication (executable)
[INFO]     [creator]      /layers/paketo-buildpacks_native-image/native-image/services.ImageAnalysisApplication.build_artifacts.txt (txt)
[INFO]     [creator]     ================================================================================
[INFO]     [creator]     Finished generating '/layers/paketo-buildpacks_native-image/native-image/services.ImageAnalysisApplication' in 5m 59s.
[INFO]     [creator]         Executing upx to compress native image
[INFO]     [creator]                            Ultimate Packer for eXecutables
[INFO]     [creator]                               Copyright (C) 1996 - 2020
[INFO]     [creator]     UPX 3.96        Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020
[INFO]     [creator]     
[INFO]     [creator]             File size         Ratio      Format      Name
[INFO]     [creator]        --------------------   ------   -----------   -----------
 127099880 ->  32416676   25.50%   linux/amd64   services.ImageAnalysisApplication
...
[INFO]     [creator]     ===> EXPORTING
...
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/native-image:native-image'
[INFO]     [creator]     Adding cache layer 'cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-native:r17'
------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  05:28 min
[INFO] Finished at: 2022-09-26T13:19:53-04:00
[INFO] ------------------------------------------------------------------------

イメージがビルドされていることを確認します。

docker images | grep image-analysis

2 つのイメージにタグを付けて GCR に push します。

# JIT(JVM) image
docker tag image-analysis-jvm:r17 gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-jvm:r17
docker push gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-jvm:r17

# AOT(Native) image
docker tag image-analysis-native:r17 gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-native:r17
docker push  gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-native:r17

12. Cloud Run にデプロイする

サービスのデプロイにかかる時間。

サービスを 2 回デプロイします。1 回目は JIT(JVM)イメージを使用し、2 回目は AOT(ネイティブ)イメージを使用します。両方のサービス デプロイが、比較目的でバケットからの同じ画像を並行して処理します。

まず、GCP プロジェクトの環境変数を設定します。

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
gcloud config set project ${GOOGLE_CLOUD_PROJECT}
gcloud config set run/region 
gcloud config set run/platform managed
gcloud config set eventarc/location europe-west1

JIT(JVM)イメージをデプロイし、コンソールでデプロイログを確認します。

gcloud run deploy image-analysis-jvm \
     --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-jvm:r17 \
     --region europe-west1 \
     --memory 2Gi --allow-unauthenticated

...
Deploying container to Cloud Run service [image-analysis-jvm] in project [...] region [europe-west1]
✓ Deploying... Done.                                                                                                                                                               
  ✓ Creating Revision...                                                                                                                                                           
  ✓ Routing traffic...                                                                                                                                                             
  ✓ Setting IAM Policy...                                                                                                                                                          
Done.                                                                                                                                                                              
Service [image-analysis-jvm] revision [image-analysis-jvm-00009-huc] has been deployed and is serving 100 percent of traffic.
Service URL: https://image-analysis-jvm-...-ew.a.run.app

AOT(ネイティブ)イメージをデプロイし、コンソールで Deployment ログを確認します。

gcloud run deploy image-analysis-native \
     --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-native:r17 \
     --region europe-west1 \
     --memory 2Gi --allow-unauthenticated 
...
Deploying container to Cloud Run service [image-analysis-native] in project [...] region [europe-west1]
✓ Deploying... Done.                                                                                                                                                               
  ✓ Creating Revision...                                                                                                                                                           
  ✓ Routing traffic...                                                                                                                                                             
  ✓ Setting IAM Policy...                                                                                                                                                          
Done.                                                                                                                                                                              
Service [image-analysis-native] revision [image-analysis-native-00005-ben] has been deployed and is serving 100 percent of traffic.
Service URL: https://image-analysis-native-...-ew.a.run.app

13. Eventarc トリガーを設定する

Eventarc は、分離されたマイクロサービス間の状態変更(イベント)を管理する標準化されたソリューションを提供します。トリガーされると、Eventarc はこれらのイベントを Pub/Sub サブスクリプションを介してさまざまな宛先(このドキュメントでは「イベントの宛先」を参照)に転送し、配信、セキュリティ、認可、オブザーバビリティ、エラー処理を管理します。

指定したイベントまたは一連のイベントの通知を Cloud Run サービスが受信するように、Eventarc トリガーを作成できます。トリガーのフィルタを指定することで、イベントソースとターゲット Cloud Run サービスを含むイベントのルーティングを構成できます。

まず、GCP プロジェクトの環境変数を設定します。

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
gcloud config set project ${GOOGLE_CLOUD_PROJECT}
gcloud config set run/region 
gcloud config set run/platform managed
gcloud config set eventarc/location europe-west1

Cloud Storage サービス アカウントに pubsub.publisher を付与します。

SERVICE_ACCOUNT="$(gsutil kms serviceaccount -p ${GOOGLE_CLOUD_PROJECT})"

gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role='roles/pubsub.publisher'

JVM(JIT)と AOT(ネイティブ)の両方のサービス イメージに Eventarc トリガーを設定して、イメージを処理します。

gcloud eventarc triggers list --location=eu

gcloud eventarc triggers create image-analysis-jvm-trigger \
     --destination-run-service=image-analysis-jvm \
     --destination-run-region=europe-west1 \
     --location=eu \
     --event-filters="type=google.cloud.storage.object.v1.finalized" \
     --event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \
     --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com

gcloud eventarc triggers create image-analysis-native-trigger \
     --destination-run-service=image-analysis-native \
     --destination-run-region=europe-west1 \
     --location=eu \
     --event-filters="type=google.cloud.storage.object.v1.finalized" \
     --event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \
     --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com    

2 つのトリガーが作成されたことを確認します。

gcloud eventarc triggers list --location=eu

14. サービスのバージョンをテストする

サービスのデプロイに成功したら、画像を Cloud Storage に投稿して、サービスが呼び出されたかどうか、Vision API が返す内容、メタデータが Firestore に保存されているかどうかを確認します。

Cloud Storage に戻り、ラボの始めに作成したバケットをクリックします。

ff8a6567afc76235.png

バケットの詳細ページが表示されたら、Upload files ボタンをクリックして画像をアップロードします。

たとえば、GeekHour.jpeg イメージは、コードベースで /services/image-analysis/java の下に用意されています。画像を選択して Open button を押します。

347b76e8b775f2f5.png

これで、サービスの実行を確認できます。image-analysis-jvm で始まり、その後に image-analysis-native が続きます。

「ハンバーガー」から(Я)メニューで Cloud Run > image-analysis-jvm サービスに移動します。

[ログ] をクリックして出力を確認します。

810a8684414ceafa.png

ログのリストを見ると、JIT(JVM)サービス image-analysis-jvm が呼び出されたことがわかります。

ログには、サービスの実行の開始と終了が記録されます。その間に、INFO レベルのログ ステートメントを使用して、関数に入力したログを確認できます。確認されたこと:

  • 関数をトリガーするイベントの詳細
  • Vision API 呼び出しの生の結果は、
  • アップロードした写真に見つかったラベル
  • ドミナントカラーの情報は
  • 見せても問題ないか、
  • そして最終的に、写真に関するこれらのメタデータが Firestore に保存されました。

image-analysis-native サービスについても同じプロセスを繰り返します。

「ハンバーガー」から(Я)メニューで Cloud Run > image-analysis-native サービスに移動します。

[ログ] をクリックして出力を確認します。

b80308c7d0f55a3.png

ここで、画像メタデータが Fiorestore に保存されているかどうかを確認します。

ここも「ハンバーガー」から(Я)メニューで Firestore セクションに移動します。Data サブセクション(デフォルト)に、アップロードした画像に対応する新しいドキュメントが追加された pictures コレクションが表示されます。

933a20a9709cb006.png

15. クリーンアップ(省略可)

シリーズの他のラボを継続する予定がない場合は、リソースをクリーンアップすることで費用を節約し、クラウド全般に精通するようにしてください。次のようにして、リソースを個別にクリーンアップできます。

バケットを削除します。

gsutil rb gs://${BUCKET_PICTURES}

関数を削除します。

gcloud functions delete picture-uploaded --region europe-west1 -q

[コレクションからコレクションを削除] を選択して、Firestore コレクションを削除します。

410b551c3264f70a.png

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

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. 完了

これで、プロジェクトの最初の鍵サービスが正常に実装されました。

学習した内容

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore
  • ネイティブ Java イメージ

次のステップ