Pic-a-daily: Lab 1 – Bilder speichern und analysieren (Native Java)

1. Übersicht

Im ersten Codelab speichern Sie Bilder in einem Bucket. Dadurch wird ein Ereignis zum Erstellen einer Datei generiert, das von einem in Cloud Run bereitgestellten Dienst verarbeitet wird. Der Dienst ruft die Vision API auf, um eine Bildanalyse durchzuführen und die Ergebnisse in einem Datenspeicher zu speichern.

c0650ee4a76db35e.png

Lerninhalte

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

2. Einrichtung und Anforderungen

Umgebung zum selbstbestimmten Lernen einrichten

  1. Melden Sie sich in der Google Cloud Console an und erstellen Sie ein neues Projekt oder verwenden Sie ein vorhandenes. Wenn Sie noch kein Gmail- oder Google Workspace-Konto haben, müssen Sie eines erstellen.

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • Der Projektname ist der Anzeigename für die Teilnehmer dieses Projekts. Es handelt sich um einen String, der nicht von Google APIs verwendet wird. Sie können sie jederzeit aktualisieren.
  • Die Projekt-ID ist für alle Google Cloud-Projekte eindeutig und unveränderlich (kann nach dem Festlegen nicht mehr geändert werden). In der Cloud Console wird automatisch ein eindeutiger String generiert. Normalerweise ist es nicht wichtig, wie dieser String aussieht. In den meisten Codelabs müssen Sie auf Ihre Projekt-ID verweisen (in der Regel als PROJECT_ID angegeben). Wenn Ihnen die generierte ID nicht gefällt, können Sie eine andere zufällige ID generieren. Alternativ können Sie es mit einem eigenen Namen versuchen und sehen, ob er verfügbar ist. Sie kann nach diesem Schritt nicht mehr geändert werden und bleibt für die Dauer des Projekts bestehen.
  • Zur Information: Es gibt einen dritten Wert, die Projektnummer, die von einigen APIs verwendet wird. Weitere Informationen zu diesen drei Werten
  1. Als Nächstes müssen Sie die Abrechnung in der Cloud Console aktivieren, um Cloud-Ressourcen/-APIs zu verwenden. Die Durchführung dieses Codelabs kostet wenig oder gar nichts. Wenn Sie Ressourcen herunterfahren möchten, um Kosten zu vermeiden, die über diese Anleitung hinausgehen, können Sie die erstellten Ressourcen oder das Projekt löschen. Neue Google Cloud-Nutzer können am kostenlosen Testzeitraum mit einem Guthaben von 300$ teilnehmen.

Cloud Shell starten

Während Sie Google Cloud von Ihrem Laptop aus per Fernzugriff nutzen können, wird in diesem Codelab Google Cloud Shell verwendet, eine Befehlszeilenumgebung, die in der Cloud ausgeführt wird.

Klicken Sie in der Google Cloud Console rechts oben in der Symbolleiste auf das Cloud Shell-Symbol:

84688aa223b1c3a2.png

Die Bereitstellung und Verbindung mit der Umgebung sollte nur wenige Augenblicke dauern. Anschließend sehen Sie in etwa Folgendes:

320e18fedb7fbe0.png

Diese virtuelle Maschine verfügt über sämtliche Entwicklertools, die Sie benötigen. Sie bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und läuft in Google Cloud, was die Netzwerkleistung und Authentifizierung erheblich verbessert. Alle Aufgaben in diesem Codelab können in einem Browser ausgeführt werden. Sie müssen nichts installieren.

3. APIs aktivieren

In diesem Lab verwenden Sie Cloud Functions und die Vision API. Diese müssen jedoch zuerst in der Cloud Console oder mit gcloud aktiviert werden.

So aktivieren Sie die Vision API in der Cloud Console: Suchen Sie in der Suchleiste nach Cloud Vision API:

8f3522d790bb026c.png

Sie gelangen auf die Seite der Cloud Vision API:

d785572fa14c87c2.png

Klicken Sie auf ENABLE.

Alternativ können Sie sie auch in Cloud Shell mit dem gcloud-Befehlszeilentool aktivieren.

Führen Sie in Cloud Shell den folgenden Befehl aus:

gcloud services enable vision.googleapis.com

Der Vorgang sollte erfolgreich abgeschlossen werden:

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

Aktivieren Sie auch Cloud Run und Cloud Build:

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

4. Bucket erstellen (Konsole)

Erstellen Sie einen Storage-Bucket für die Bilder. Dies ist über die Google Cloud Console ( console.cloud.google.com) oder mit dem gsutil-Befehlszeilentool in Cloud Shell oder Ihrer lokalen Entwicklungsumgebung möglich.

Rufen Sie über das Dreistrich-Menü (☰) die Seite Storage auf.

d08ecb0ae29330a1.png

Bucket benennen

Klicke auf die Schaltfläche CREATE BUCKET.

8951851554a430d2.png

Klicken Sie auf CONTINUE.

Standort auswählen

24b24625157ab467.png

Erstellen Sie einen multiregionalen Bucket in der Region Ihrer Wahl (hier Europe).

Klicken Sie auf CONTINUE.

Standardspeicherklasse auswählen

9e7bd365fa94a2e0.png

Wählen Sie die Speicherklasse Standard für Ihre Daten aus.

Klicken Sie auf CONTINUE.

Zugriffssteuerung festlegen

1ff4a1f6e57045f5.png

Da Sie mit öffentlich zugänglichen Bildern arbeiten, möchten Sie, dass alle in diesem Bucket gespeicherten Bilder dieselbe einheitliche Zugriffssteuerung haben.

Wählen Sie die Option Uniform aus.

Klicken Sie auf CONTINUE.

Schutz/Verschlüsselung einrichten

2d469b076029d365.png

Behalten Sie die Standardeinstellung (Google-managed key)) bei, da Sie keine eigenen Verschlüsselungsschlüssel verwenden.

Klicken Sie auf CREATE, um die Bucket-Erstellung abzuschließen.

„allUsers“ als Speicherbetrachter hinzufügen

Rufen Sie den Tab Permissions auf:

19564b3ad8688ae8.png

Fügen Sie dem Bucket ein allUsers-Mitglied mit der Rolle Storage > Storage Object Viewer hinzu:

d655e760c76d62c1.png

Klicken Sie auf SAVE.

5. Bucket erstellen (gsutil)

Sie können auch das gsutil-Befehlszeilentool in Cloud Shell verwenden, um Buckets zu erstellen.

Legen Sie in Cloud Shell eine Variable für den eindeutigen Bucket-Namen fest. In Cloud Shell ist GOOGLE_CLOUD_PROJECT bereits auf Ihre eindeutige Projekt-ID festgelegt. Sie können das an den Bucket-Namen anhängen.

Beispiel:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

So erstellen Sie eine Standard-Multiregionenzone in Europa:

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

Einheitlichen Zugriff auf Bucket-Ebene sicherstellen:

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

Veröffentlichen Sie den Bucket:

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

Wenn Sie den Bereich Cloud Storage der Console aufrufen, sollte ein öffentlicher uploaded-pictures-Bucket vorhanden sein:

65c63ef4a6eb30ad.png

Testen Sie, ob Sie Bilder in den Bucket hochladen können und ob die hochgeladenen Bilder öffentlich verfügbar sind, wie im vorherigen Schritt beschrieben.

6. Öffentlichen Zugriff auf den Bucket testen

Wenn Sie zum Speicherbrowser zurückkehren, sehen Sie Ihren Bucket in der Liste mit dem Zugriff „Öffentlich“ (einschließlich eines Warnzeichens, das Sie daran erinnert, dass jeder auf den Inhalt dieses Buckets zugreifen kann).

e639a9ba625b71a6.png

Ihr Bucket kann jetzt Bilder empfangen.

Wenn Sie auf den Bucket-Namen klicken, werden die Bucket-Details angezeigt.

1f88a2290290aba8.png

Dort können Sie die Schaltfläche Upload files ausprobieren, um zu testen, ob Sie dem Bucket ein Bild hinzufügen können. In einem Pop‑up-Fenster werden Sie aufgefordert, eine Datei auszuwählen. Nach der Auswahl wird die Datei in Ihren Bucket hochgeladen und Sie sehen wieder den public-Zugriff, der dieser neuen Datei automatisch zugewiesen wurde.

1209e7ebe1f63b10.png

Neben dem Zugriffslabel Public sehen Sie auch ein kleines Linksymbol. Wenn Sie darauf klicken, wird in Ihrem Browser die öffentliche URL des Bildes aufgerufen, die so aussieht:

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

BUCKET_NAME ist der global eindeutige Name, den Sie für Ihren Bucket ausgewählt haben, gefolgt vom Dateinamen Ihres Bildes.

Wenn Sie auf das Kästchen neben dem Bildnamen klicken, wird die Schaltfläche DELETE aktiviert und Sie können das erste Bild löschen.

7. Datenbank vorbereiten

Sie speichern Informationen zum Bild, die von der Vision API bereitgestellt werden, in der Cloud Firestore-Datenbank. Das ist eine schnelle, vollständig verwaltete, serverlose, cloudnative NoSQL-Dokumentendatenbank. Bereiten Sie Ihre Datenbank vor, indem Sie in der Cloud Console den Bereich Firestore aufrufen:

e57a673537b5deca.png

Es gibt zwei Optionen: Native mode und Datastore mode. Verwenden Sie den nativen Modus, der zusätzliche Funktionen wie Offlineunterstützung und Echtzeitsynchronisierung bietet.

Klicken Sie auf SELECT NATIVE MODE.

1a2e363fae5c7e96.png

Wählen Sie eine Multi-Region aus (hier in Europa, aber idealerweise mindestens dieselbe Region wie Ihre Funktion und Ihr Speicher-Bucket).

Klicken Sie auf CREATE DATABASE.

Nachdem die Datenbank erstellt wurde, sollten Sie Folgendes sehen:

7dcc82751ed483fb.png

Erstellen Sie eine neue Sammlung, indem Sie auf die Schaltfläche + START COLLECTION klicken.

Name der Sammlung: pictures

dce3d73884ac8c83.png

Sie müssen kein Dokument erstellen. Sie fügen sie programmatisch hinzu, wenn neue Bilder in Cloud Storage gespeichert und von der Vision API analysiert werden.

Klicken Sie auf Save.

Firestore erstellt ein erstes Standarddokument in der neu erstellten Sammlung. Sie können dieses Dokument bedenkenlos löschen, da es keine nützlichen Informationen enthält:

63e95c844b3f79d3.png

Die Dokumente, die programmatisch in unserer Sammlung erstellt werden, enthalten vier Felder:

  • name (String): Der Dateiname des hochgeladenen Bildes, der auch der Schlüssel des Dokuments ist.
  • labels (Array von Strings): die Labels der von der Vision API erkannten Elemente
  • color (String): Der hexadezimale Farbcode der dominanten Farbe (z. B. #ab12ef)
  • created (Datum): Der Zeitstempel, zu dem die Metadaten dieses Bildes gespeichert wurden.
  • thumbnail (boolescher Wert): Ein optionales Feld, das vorhanden ist und „true“ enthält, wenn für dieses Bild eine Miniaturansicht generiert wurde.

Da wir in Firestore nach Bildern mit verfügbaren Thumbnails suchen und nach dem Erstellungsdatum sortieren, müssen wir einen Suchindex erstellen.

Sie können den Index mit dem folgenden Befehl in Cloud Shell erstellen:

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

Sie können dies auch in der Cloud Console tun. Klicken Sie dazu in der Navigationsspalte links auf Indexes und erstellen Sie dann einen zusammengesetzten Index, wie unten dargestellt:

2236d3a024a59232.png

Klicken Sie auf Create. Die Indexerstellung kann einige Minuten dauern.

8. Code klonen

Klonen Sie den Code, falls Sie das im vorherigen Codelab noch nicht getan haben:

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

Sie können dann das Verzeichnis mit dem Dienst aufrufen, um mit dem Erstellen des Labs zu beginnen:

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

Der Dienst hat das folgende Dateilayout:

4c2a18a2c8b69dc5.png

9. Servicecode ansehen

Zuerst sehen Sie sich an, wie die Java-Clientbibliotheken in pom.xml mithilfe einer BOM aktiviert werden:

Öffnen Sie zuerst die Datei pom.xml, in der die Abhängigkeiten unserer Java-App aufgeführt sind. Der Fokus liegt auf der Verwendung der Vision-, Cloud Storage- und Firestore-APIs.

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0-M3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>services</groupId>
        <artifactId>image-analysis</artifactId>
        <version>0.0.1</version>
        <name>image-analysis</name>
        <description>Spring App for Image Analysis</description>
    <properties>
        <java.version>17</java.version>
        <maven.compiler.target>17</maven.compiler.target>
        <maven.compiler.source>17</maven.compiler.source>        
        <spring-cloud.version>2023.0.0-M2</spring-cloud.version>
        <testcontainers.version>1.19.1</testcontainers.version>
    </properties>
...
  <dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>libraries-bom</artifactId>
            <version>26.24.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
  </dependencyManagement>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
                <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-function-web</artifactId>
                </dependency>
        <dependency>
            <groupId>com.google.cloud.functions</groupId>
            <artifactId>functions-framework-api</artifactId>
            <version>1.1.0</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>        

Die Funktion wird in der Klasse EventController implementiert. Jedes Mal, wenn ein neues Bild in den Bucket hochgeladen wird, erhält der Dienst eine Benachrichtigung zur Verarbeitung:

@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 {
...
}

Der Code validiert dann die Cloud Events-Header:

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

Jetzt kann eine Anfrage erstellt werden. Der Code bereitet eine solche Anfrage vor, die an Vision API gesendet werden soll:

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

Wir bitten um drei wichtige Funktionen der Vision API:

  • Labelerkennung: um zu verstehen, was auf den Bildern zu sehen ist
  • Bildattribute: um interessante Attribute des Bildes zu liefern (wir sind an der dominanten Farbe des Bildes interessiert)
  • SafeSearch: um zu wissen, ob das Bild sicher angezeigt werden kann (es sollte keine Inhalte nur für Erwachsene, medizinische, anstößige oder gewalttätige Inhalte enthalten)

An dieser Stelle können wir den Aufruf an die Vision API senden:

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

Zur Referenz sehen Sie hier, wie die Antwort der Vision API aussieht:

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

Wenn kein Fehler zurückgegeben wird, können wir fortfahren. Deshalb haben wir diesen if-Block:

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

Wir rufen die Labels der Dinge, Kategorien oder Themen ab, die auf dem Bild erkannt wurden:

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

Wir möchten wissen, welche Farbe im Bild dominiert:

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

Prüfen wir, ob das Bild sicher angezeigt werden kann:

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

Wir prüfen, ob die Merkmale „nicht jugendfrei“, „Parodie“, „medizinisch“, „Gewalt“ oder „freizügig“ wahrscheinlich oder sehr wahrscheinlich sind.

Wenn das Ergebnis der SafeSearch-Erkennung in Ordnung ist, können wir Metadaten in Firestore speichern:

// Saving result to Firestore
if (isSafe) {
          ApiFuture<WriteResult> writeResult = 
               eventService.storeImage(fileName, labels,
                                       mainColor);
          logger.info("Picture metadata saved in Firestore at " + 
               writeResult.get().getUpdateTime());
}
...
  public ApiFuture<WriteResult> storeImage(String fileName, 
                                           List<String> labels, 
                                           String mainColor) {
    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());

    return doc.set(data, SetOptions.merge());
  }

10. App-Images mit GraalVM erstellen

In diesem optionalen Schritt erstellen Sie mit GraalVM zuerst eine JIT based app image und dann eine Native Java app image.

Damit Sie den Build ausführen können, müssen Sie ein geeignetes JDK und den native-image-Builder installiert und konfiguriert haben. Dafür gibt es mehrere Möglichkeiten.

To start. Laden Sie die GraalVM 22.3.x Community Edition herunter und folgen Sie der Anleitung auf der Seite GraalVM-Installation.

Dieser Prozess lässt sich mit SDKMAN! erheblich vereinfachen.

Um die entsprechende JDK-Distribution mit SDKman zu installieren, verwenden Sie zuerst den Befehl „install“:

sdk install java 17.0.8-graal

Weisen Sie SDKman an, diese Version sowohl für JIT- als auch für AOT-Builds zu verwenden:

sdk use java 17.0.8-graal

In Cloudshell können Sie GraalVM und das native-image-Dienstprogramm mit diesen einfachen Befehlen installieren:

# download GraalVM
wget https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_linux-x64_bin.tar.gz 
tar -xzf graalvm-jdk-17_linux-x64_bin.tar.gz

ls -lart

# configure Java 17 and GraalVM for Java 17
# note the name of the latest GraalVM version, as unpacked by the tar command
echo Existing JVM: $JAVA_HOME
cd graalvm-jdk-17.0.8+9.1

export JAVA_HOME=$PWD
cd bin
export PATH=$PWD:$PATH

echo JAVA HOME: $JAVA_HOME
echo PATH: $PATH

cd ../..

# validate the version with
java -version 

# observe
Java(TM) SE Runtime Environment Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14, mixed mode, sharing)

Legen Sie zuerst die Umgebungsvariablen für das GCP-Projekt fest:

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

Sie können dann das Verzeichnis mit dem Dienst aufrufen, um mit dem Erstellen des Labs zu beginnen:

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

Erstellen Sie das JIT-Anwendungs-Image:

./mvnw package

Sehen Sie sich das Build-Log im Terminal an:

...
[INFO] Results:
[INFO] 
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- maven-jar-plugin:3.3.0:jar (default-jar) @ image-analysis ---
[INFO] Building jar: /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:3.2.0-M3:repackage (repackage) @ image-analysis ---
[INFO] Replacing main artifact /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar.original
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  15.335 s
[INFO] Finished at: 2023-10-10T19:33:25Z
[INFO] ------------------------------------------------------------------------

Erstellen Sie das Native-Image(AOT wird verwendet):

./mvnw native:compile -Pnative

Sehen Sie sich das Build-Log im Terminal an, einschließlich der Build-Logs für native Images:

Je nach Computer, auf dem Sie den Test ausführen, dauert der Build etwas länger.

...
[2/7] Performing analysis...  [*********]                                                              (124.5s @ 4.53GB)
  29,732 (93.19%) of 31,905 classes reachable
  60,161 (70.30%) of 85,577 fields reachable
 261,973 (67.29%) of 389,319 methods reachable
   2,940 classes, 2,297 fields, and 97,421 methods registered for reflection
      81 classes,    90 fields, and    62 methods registered for JNI access
       4 native libraries: dl, pthread, rt, z
[3/7] Building universe...                                                                              (11.7s @ 4.67GB)
[4/7] Parsing methods...      [***]                                                                      (6.1s @ 5.91GB)
[5/7] Inlining methods...     [****]                                                                     (4.5s @ 4.39GB)
[6/7] Compiling methods...    [******]                                                                  (35.3s @ 4.60GB)
[7/7] Creating image...                                                                                 (12.9s @ 4.61GB)
  80.08MB (47.43%) for code area:   190,483 compilation units
  73.81MB (43.72%) for image heap:  660,125 objects and 189 resources
  14.95MB ( 8.86%) for other data
 168.84MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area:                               Top 10 object types in image heap:
   2.66MB com.google.cloud.vision.v1p4beta1                   18.51MB byte[] for code metadata
   2.60MB com.google.cloud.vision.v1                           9.27MB java.lang.Class
   2.49MB com.google.protobuf                                  7.34MB byte[] for reflection metadata
   2.40MB com.google.cloud.vision.v1p3beta1                    6.35MB byte[] for java.lang.String
   2.17MB com.google.storage.v2                                5.72MB java.lang.String
   2.12MB com.google.firestore.v1                              4.46MB byte[] for embedded resources
   1.64MB sun.security.ssl                                     4.30MB c.oracle.svm.core.reflect.SubstrateMethodAccessor
   1.51MB i.g.xds.shaded.io.envoyproxy.envoy.config.core.v3    4.27MB byte[] for general heap data
   1.47MB com.google.cloud.vision.v1p2beta1                    2.50MB com.oracle.svm.core.hub.DynamicHubCompanion
   1.34MB i.g.x.shaded.io.envoyproxy.envoy.config.route.v3     1.17MB java.lang.Object[]
  58.34MB for 977 more packages                                9.19MB for 4667 more object types
------------------------------------------------------------------------------------------------------------------------
                        13.5s (5.7% of total time) in 75 GCs | Peak RSS: 9.44GB | CPU load: 6.13
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis (executable)
 /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis.build_artifacts.txt (txt)
========================================================================================================================
Finished generating '/home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis' in 3m 57s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  04:28 min
[INFO] Finished at: 2023-10-10T19:53:30Z
[INFO] ------------------------------------------------------------------------

11. Container-Images erstellen und veröffentlichen

Wir erstellen ein Container-Image in zwei verschiedenen Versionen: einmal als JIT image und einmal als Native Java image.

Legen Sie zuerst die Umgebungsvariablen für das GCP-Projekt fest:

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

Erstellen Sie das JIT-Image:.

./mvnw spring-boot:build-image -Pji

Sehen Sie sich das Build-Log im Terminal an:

[INFO]     [creator]     Timer: Saving docker.io/library/image-analysis-maven-jit:latest... started at 2023-10-10T20:00:31Z
[INFO]     [creator]     *** Images (4c84122a1826):
[INFO]     [creator]           docker.io/library/image-analysis-maven-jit:latest
[INFO]     [creator]     Timer: Saving docker.io/library/image-analysis-maven-jit:latest... ran for 6.975913605s and ended at 2023-10-10T20:00:38Z
[INFO]     [creator]     Timer: Exporter ran for 8.068588001s and ended at 2023-10-10T20:00:38Z
[INFO]     [creator]     Timer: Cache started at 2023-10-10T20:00:38Z
[INFO]     [creator]     Reusing cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
[INFO]     [creator]     Timer: Cache ran for 200.449002ms and ended at 2023-10-10T20:00:38Z
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-maven-jit:latest'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  43.887 s
[INFO] Finished at: 2023-10-10T20:00:39Z
[INFO] ------------------------------------------------------------------------

Erstellen Sie das AOT-Image(nativ):

./mvnw spring-boot:build-image -Pnative

Sehen Sie sich das Build-Log im Terminal an, einschließlich der Build-Logs für das native Image.

Hinweis:

  • Der Build dauert je nach Computer, auf dem Sie testen, deutlich länger.
  • Die Bilder können mit UPX weiter komprimiert werden, was sich jedoch geringfügig negativ auf die Startleistung auswirkt. Daher wird UPX in diesem Build nicht verwendet. Das ist immer ein kleiner Kompromiss.
...
[INFO]     [creator]     Saving docker.io/library/image-analysis-maven-native:latest...
[INFO]     [creator]     *** Images (13167702674e):
[INFO]     [creator]           docker.io/library/image-analysis-maven-native:latest
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/bellsoft-liberica:native-image-svm'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/native-image:native-image'
[INFO]     [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-maven-native:latest'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:37 min
[INFO] Finished at: 2023-10-10T20:05:16Z
[INFO] ------------------------------------------------------------------------

Prüfen Sie, ob die Images erstellt wurden:

docker images | grep image-analysis

Taggen Sie die beiden Images und übertragen Sie sie per Push in GCR:

# JIT image
docker tag image-analysis-maven-jit gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit
docker push gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit

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

12. In Cloud Run bereitstellen

Zeit für die Bereitstellung des Dienstes.

Sie stellen den Dienst zweimal bereit: einmal mit dem JIT-Image und einmal mit dem AOT-Image(nativ). Beide Dienstbereitstellungen verarbeiten dasselbe Bild aus dem Bucket parallel zu Vergleichszwecken.

Legen Sie zuerst die Umgebungsvariablen für das GCP-Projekt fest:

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

Stellen Sie das JIT-Image bereit und sehen Sie sich das Bereitstellungslog in der Konsole an:

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

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

Stellen Sie das native Image bereit und sehen Sie sich das Bereitstellungslog in der Konsole an:

gcloud run deploy image-analysis-native \
     --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native \
     --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-Trigger einrichten

Eventarc bietet eine standardisierte Lösung, um den Ablauf von Statusänderungen, die als Ereignisse bezeichnet werden, zwischen entkoppelten Mikrodiensten zu verwalten. Bei Auslösung leitet Eventarc diese Ereignisse über Pub/Sub-Abos an verschiedene Ziele weiter (in diesem Dokument finden Sie weitere Informationen unter Ereignisziele) und verwaltet dabei die Bereitstellung, Sicherheit, Autorisierung, Beobachtbarkeit und Fehlerbehandlung.

Sie können einen Eventarc-Trigger erstellen, damit Ihr Cloud Run-Dienst Benachrichtigungen über ein bestimmtes Ereignis oder eine Reihe von Ereignissen erhält. Durch Angabe von Filtern für den Trigger können Sie das Routing des Ereignisses konfigurieren, einschließlich der Ereignisquelle und des Cloud Run-Zieldienstes.

Legen Sie zuerst die Umgebungsvariablen für das GCP-Projekt fest:

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

Weisen Sie dem Cloud Storage-Dienstkonto pubsub.publisher zu:

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'

Richten Sie Eventarc-Trigger für JIT- und native Dienstbilder ein, um das Bild zu verarbeiten:

gcloud eventarc triggers list --location=eu

gcloud eventarc triggers create image-analysis-jit-trigger \
     --destination-run-service=image-analysis-jit \
     --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    

Prüfen Sie, ob die beiden Trigger erstellt wurden:

gcloud eventarc triggers list --location=eu

14. Dienstversionen testen

Sobald die Dienstbereitstellungen erfolgreich sind, posten Sie ein Bild in Cloud Storage, prüfen, ob unsere Dienste aufgerufen wurden, was die Vision API zurückgibt und ob Metadaten in Firestore gespeichert werden.

Kehren Sie zu Cloud Storage zurück und klicken Sie auf den Bucket, den wir am Anfang des Labs erstellt haben:

33442485a1d76921.png

Klicken Sie auf der Seite mit den Bucket-Details auf die Schaltfläche Upload files, um ein Bild hochzuladen.

Ein GeekHour.jpeg-Image wird beispielsweise mit Ihrem Code unter /services/image-analysis/java bereitgestellt. Wählen Sie ein Bild aus und drücken Sie die Open button:

d57529452f62bd32.png

Sie können jetzt die Ausführung des Dienstes prüfen, beginnend mit image-analysis-jit und dann image-analysis-native.

Rufen Sie über das Dreistrich-Menü (☰) den Dienst Cloud Run > image-analysis-jit auf.

Klicken Sie auf „Logs“ und sehen Sie sich die Ausgabe an:

ae1a4a94c7c7a166.png

In der Liste der Logs sehe ich, dass der JIT-Dienst image-analysis-jit aufgerufen wurde.

Die Logs geben den Beginn und das Ende der Dienstausführung an. Dazwischen sehen wir die Logs, die wir mit den Log-Anweisungen auf INFO-Ebene in unsere Funktion eingefügt haben. Wir sehen:

  • Details zum Ereignis, das unsere Funktion auslöst,
  • Die Rohdaten aus dem Vision API-Aufruf
  • Die Labels, die auf dem von uns hochgeladenen Bild gefunden wurden.
  • Informationen zu dominanten Farben,
  • ob das Bild sicher angezeigt werden kann,
  • Schließlich werden diese Metadaten zum Bild in Firestore gespeichert.

Wiederholen Sie den Vorgang für den Dienst image-analysis-native.

Rufen Sie über das Dreistrich-Menü (☰) den Dienst Cloud Run > image-analysis-native auf.

Klicken Sie auf „Logs“ und sehen Sie sich die Ausgabe an:

4afe22833c1fd14c.png

Prüfen Sie nun, ob die Bildmetadaten in Fiorestore gespeichert wurden.

Rufen Sie noch einmal über das Dreistrich-Menü (☰) den Bereich Firestore auf. Im Unterabschnitt Data (standardmäßig angezeigt) sollte die Sammlung pictures mit einem neuen Dokument angezeigt werden, das dem gerade hochgeladenen Bild entspricht:

82d6c468956e7cfc.png

15. Bereinigen (optional)

Wenn Sie nicht mit den anderen Labs der Reihe fortfahren möchten, können Sie Ressourcen bereinigen, um Kosten zu sparen und nicht mehr benötigte Ressourcen für andere freizugeben. Sie können Ressourcen einzeln so bereinigen:

Löschen Sie den Bucket:

gsutil rb gs://${BUCKET_PICTURES}

Funktion löschen:

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

Löschen Sie die Firestore-Sammlung, indem Sie in der Sammlung „Sammlung löschen“ auswählen:

6cc86a7b88fdb4d3.png

Alternativ können Sie das gesamte Projekt löschen:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. Glückwunsch!

Glückwunsch! Sie haben den ersten wichtigen Dienst des Projekts erfolgreich implementiert.

Behandelte Themen

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore
  • Native Java-Images

Nächste Schritte