Pic-a-daily: archivia e analizza immagini con le librerie client Java native di Google

1. Panoramica

Nel primo codelab, memorizzerai le immagini in un bucket. Verrà generato un evento di creazione di file che verrà gestito da un servizio di cui è stato eseguito il deployment in Cloud Run. Il servizio effettuerà una chiamata all'API Vision per eseguire l'analisi delle immagini e salvare i risultati in un datastore.

427de3100de3a61e.png

Obiettivi didattici

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

2. Configurazione e requisiti

Configurazione dell'ambiente autonomo

  1. Accedi alla console Google Cloud e crea un nuovo progetto o riutilizzane uno esistente. Se non hai ancora un account Gmail o Google Workspace, devi crearne uno.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Il nome del progetto è il nome visualizzato per i partecipanti a questo progetto. È una stringa di caratteri non utilizzata dalle API di Google. Puoi aggiornarlo in qualsiasi momento.
  • L'ID progetto deve essere univoco in tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo essere stato impostato). La console Cloud genera automaticamente una stringa univoca, di solito non ti interessa di cosa si tratta. Nella maggior parte dei codelab, devi fare riferimento all'ID progetto (in genere è identificato come PROJECT_ID). Se non ti piace l'ID generato, puoi generarne un altro casuale. In alternativa, puoi provare a crearne uno e vedere se è disponibile. Non può essere modificato dopo questo passaggio e rimarrà per tutta la durata del progetto.
  • Per tua informazione, esiste un terzo valore, un numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
  1. Successivamente, devi abilitare la fatturazione in Cloud Console per utilizzare le risorse/API Cloud. L'esecuzione di questo codelab non dovrebbe costare molto, se non nulla. Per arrestare le risorse in modo da non incorrere in costi di fatturazione al termine di questo tutorial, puoi eliminare le risorse che hai creato o l'intero progetto. I nuovi utenti di Google Cloud possono beneficiare del programma prova senza costi di 300$.

Avvia Cloud Shell

Sebbene Google Cloud possa essere gestito da remoto dal tuo laptop, in questo codelab utilizzerai Google Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.

Nella console Google Cloud, fai clic sull'icona di Cloud Shell nella barra degli strumenti in alto a destra:

55efc1aaa7a4d3ad.png

Bastano pochi istanti per eseguire il provisioning e connettersi all'ambiente. Al termine, dovresti vedere un risultato simile a questo:

7ffe5cbb04455448.png

Questa macchina virtuale è caricata con tutti gli strumenti per sviluppatori di cui avrai bisogno. Offre una home directory permanente da 5 GB e viene eseguita su Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. Tutto il lavoro in questo codelab può essere svolto all'interno di un browser. Non devi installare nulla.

3. Abilita API

Per questo lab, utilizzerai Cloud Functions e l'API Vision, ma prima devi abilitarle in Cloud Console o con gcloud.

Per abilitare l'API Vision in Cloud Console, cerca Cloud Vision API nella barra di ricerca:

cf48b1747ba6a6fb.png

Verrà visualizzata la pagina dell'API Cloud Vision:

ba4af419e6086fbb.png

Fai clic sul pulsante ENABLE.

In alternativa, puoi abilitarlo anche in Cloud Shell utilizzando lo strumento a riga di comando gcloud.

In Cloud Shell, esegui questo comando:

gcloud services enable vision.googleapis.com

Dovresti vedere che l'operazione è stata completata correttamente:

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

Abilita anche Cloud Run e Cloud Build:

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

4. Crea il bucket (console)

Crea un bucket di archiviazione per le immagini. Puoi farlo dalla console Google Cloud ( console.cloud.google.com) o con lo strumento a riga di comando gsutil da Cloud Shell o dal tuo ambiente di sviluppo locale.

Dal menu "hamburger" (☰), vai alla pagina Storage.

1930e055d138150a.png

Assegna un nome al bucket

Fai clic sul pulsante CREATE BUCKET.

34147939358517f8.png

Fai clic su CONTINUE.

Scegliere la posizione

197817f20be07678.png

Crea un bucket multiregionale nella regione che preferisci (in questo caso Europe).

Fai clic su CONTINUE.

Scegli la classe di archiviazione predefinita

53cd91441c8caf0e.png

Scegli la classe di archiviazione Standard per i tuoi dati.

Fai clic su CONTINUE.

Impostare il controllo dell'accesso

8c2b3b459d934a51.png

Poiché lavorerai con immagini accessibili pubblicamente, vuoi che tutte le immagini archiviate in questo bucket abbiano lo stesso controllo dell'accesso uniforme.

Scegli l'opzione di controllo dell'accesso Uniform.

Fai clic su CONTINUE.

Impostare la protezione/crittografia

d931c24c3e705a68.png

Mantieni il valore predefinito (Google-managed key)), in quanto non utilizzerai le tue chiavi di crittografia.

Fai clic su CREATE per completare la creazione del bucket.

Aggiungere allUsers come visualizzatore dello spazio di archiviazione

Vai alla scheda Permissions:

d0ecfdcff730ea51.png

Aggiungi un membro allUsers al bucket con il ruolo Storage > Storage Object Viewer nel seguente modo:

e9f25ec1ea0b6cc6.png

Fai clic su SAVE.

5. Crea il bucket (gsutil)

Puoi anche utilizzare lo strumento a riga di comando gsutil in Cloud Shell per creare bucket.

In Cloud Shell, imposta una variabile per il nome univoco del bucket. Cloud Shell ha già impostato GOOGLE_CLOUD_PROJECT sul tuo ID progetto univoco. Puoi aggiungerlo al nome del bucket.

Ad esempio:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Crea una zona standard multi-regione in Europa:

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

Assicurati che l'accesso uniforme a livello di bucket sia abilitato:

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

Rendi pubblico il bucket:

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

Se vai alla sezione Cloud Storage della console, dovresti avere un bucket uploaded-pictures pubblico:

a98ed4ba17873e40.png

Verifica di poter caricare immagini nel bucket e che le immagini caricate siano disponibili pubblicamente, come spiegato nel passaggio precedente.

6. Testa l'accesso pubblico al bucket

Tornando al browser di archiviazione, vedrai il tuo bucket nell'elenco, con accesso "Pubblico" (inclusa un'icona di avviso che ti ricorda che chiunque ha accesso ai contenuti del bucket).

89e7a4d2c80a0319.png

Il tuo bucket è ora pronto a ricevere le immagini.

Se fai clic sul nome del bucket, vengono visualizzati i dettagli.

131387f12d3eb2d3.png

Lì puoi provare il pulsante Upload files per verificare di poter aggiungere un'immagine al bucket. Verrà visualizzato un popup di selezione dei file che ti chiederà di selezionare un file. Una volta selezionato, verrà caricato nel bucket e vedrai di nuovo l'accesso publicattribuito automaticamente a questo nuovo file.

e87584471a6e9c6d.png

Accanto all'etichetta di accesso Public, vedrai anche una piccola icona a forma di link. Quando fai clic, il browser si sposta sull'URL pubblico dell'immagine, che avrà il seguente formato:

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

dove BUCKET_NAME è il nome univoco globale che hai scelto per il bucket e il nome del file dell'immagine.

Se fai clic sulla casella di controllo accanto al nome dell'immagine, il pulsante DELETE verrà attivato e potrai eliminare la prima immagine.

7. Prepara il database

Memorizzerai le informazioni sull'immagine fornite dall'API Vision nel database Cloud Firestore, un database di documenti NoSQL veloce, serverless, completamente gestito e cloud-native. Prepara il database andando alla sezione Firestore di Cloud Console:

9e4708d2257de058.png

Sono disponibili due opzioni: Native mode o Datastore mode. Utilizza la modalità nativa, che offre funzionalità aggiuntive come il supporto offline e la sincronizzazione in tempo reale.

Fai clic su SELECT NATIVE MODE.

9449ace8cc84de43.png

Scegli una regione multipla (in questo caso in Europa, ma idealmente almeno la stessa regione della funzione e del bucket di archiviazione).

Fai clic sul pulsante CREATE DATABASE.

Una volta creato il database, dovresti visualizzare quanto segue:

56265949a124819e.png

Crea una nuova raccolta facendo clic sul pulsante + START COLLECTION.

Raccolta di nomi pictures.

75806ee24c4e13a7.png

Non è necessario creare un documento. Le aggiungerai in modo programmatico man mano che le nuove immagini vengono archiviate in Cloud Storage e analizzate dall'API Vision.

Fai clic su Save.

Firestore crea un primo documento predefinito nella raccolta appena creata. Puoi eliminarlo in sicurezza perché non contiene informazioni utili:

5c2f1e17ea47f48f.png

I documenti che verranno creati a livello di programmazione nella nostra raccolta conterranno quattro campi:

  • name (stringa): il nome del file dell'immagine caricata, che è anche la chiave del documento
  • labels (array di stringhe): le etichette degli elementi riconosciuti dall'API Vision
  • color (stringa): il codice colore esadecimale del colore dominante (ad es. #ab12ef)
  • created (data): il timestamp di quando sono stati archiviati i metadati di questa immagine
  • thumbnail (booleano): un campo facoltativo che sarà presente e sarà true se è stata generata un'immagine miniatura per questa immagine

Poiché eseguiremo la ricerca in Firestore per trovare le immagini per cui sono disponibili miniature e l'ordinamento in base alla data di creazione, dovremo creare un indice di ricerca.

Puoi creare l'indice con il seguente comando in Cloud Shell:

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

In alternativa, puoi farlo anche da Cloud Console, facendo clic su Indexes nella colonna di navigazione a sinistra e poi creando un indice composito come mostrato di seguito:

ecb8b95e3c791272.png

Fai clic su Create. La creazione dell'indice può richiedere alcuni minuti.

8. Clona il codice

Clona il codice, se non l'hai già fatto nel lab precedente:

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

A questo punto puoi andare alla directory contenente il servizio per iniziare a creare il lab:

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

Per il servizio avrai il seguente layout dei file:

f79613aff479d8ad.png

9. Esplora il codice del servizio

Inizia esaminando come vengono abilitate le librerie client Java in pom.xml utilizzando un BOM:

Innanzitutto, modifica il file pom.xml che elenca le dipendenze della nostra funzione Java. Aggiorna il codice per aggiungere la dipendenza Maven dell'API Cloud Vision:

<?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>

La funzionalità è implementata nella classe EventController. Ogni volta che viene caricata una nuova immagine nel bucket, il servizio riceve una notifica per l'elaborazione:

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

Il codice procederà alla convalida delle intestazioni 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);
} 

Ora è possibile creare una richiesta e il codice ne preparerà una da inviare a 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);

Ti chiediamo di utilizzare tre funzionalità chiave dell'API Vision:

  • Rilevamento delle etichette: per capire cosa c'è nelle foto
  • Proprietà dell'immagine: per fornire attributi interessanti dell'immagine (ci interessa il colore dominante dell'immagine)
  • SafeSearch: per sapere se l'immagine è sicura da mostrare (non deve contenere contenuti per adulti, medici, osé o violenti)

A questo punto, possiamo effettuare la chiamata all'API Vision:

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

Per riferimento, ecco come appare la risposta dell'API Vision:

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

Se non viene restituito alcun errore, possiamo andare avanti, ecco perché abbiamo questo blocco 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);
}

Recupereremo le etichette delle cose, delle categorie o dei temi riconosciuti nell'immagine:

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

Ci interessa conoscere il colore dominante dell'immagine:

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

Controlliamo se l'immagine è sicura da mostrare:

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

Stiamo verificando le caratteristiche relative a contenuti per adulti, parodie, contenuti medici, violenza e contenuti allusivi per capire se non sono probabili o molto probabili.

Se il risultato della ricerca sicura è accettabile, possiamo archiviare i metadati in 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. (Facoltativo) Crea immagini dell'app con GraalVM

In questo passaggio facoltativo, creerai un JIT(JVM) based app image e poi un AOT(Native) Java app image utilizzando GraalVM.

Per eseguire la build, devi assicurarti di avere una JDK appropriata e che il builder native-image sia installato e configurato. Sono disponibili diverse opzioni.

To start, scarica GraalVM 22.2.x Community Edition e segui le istruzioni nella pagina Installazione di GraalVM.

Questo processo può essere notevolmente semplificato con l'aiuto di SDKMAN!

Per installare la distribuzione JDK appropriata con SDKman, inizia utilizzando il comando di installazione:

sdk install java 22.2.r17-grl

Chiedi a SDKman di utilizzare questa versione, sia per le build JIT che AOT:

sdk use java 22.2.0.r17-grl

Installa native-image utility per GraalVM:

gu install native-image

In Cloudshell, per comodità, puoi installare GraalVM e l'utilità native-image con questi semplici comandi:

# 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 ../..

Innanzitutto, imposta le variabili di ambiente del progetto GCP:

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

A questo punto puoi andare alla directory contenente il servizio per iniziare a creare il lab:

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

Crea l'immagine dell'applicazione JIT(JVM):

./mvnw package -Pjvm

Osserva il log di build nel terminale:

...
[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] ------------------------------------------------------------------------

Crea l'immagine AOT(Native):

./mvnw package -Pnative -DskipTests

Osserva il log di build nel terminale, inclusi i log di build delle immagini native:

Tieni presente che la build richiede molto più tempo, a seconda della macchina su cui esegui il test.

...
[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. Creare e pubblicare immagini container

Creiamo un'immagine container in due versioni diverse: una come JIT(JVM) image e l'altra come AOT(Native) Java image.

Innanzitutto, imposta le variabili di ambiente del progetto GCP:

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

Crea l'immagine JIT(JVM):

./mvnw package -Pjvm-image

Osserva il log di build nel terminale:

[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] ------------------------------------------------------------------------

Crea l'immagine AOT(Native):

./mvnw package -Pnative-image

Osserva il log della build nel terminale, inclusi i log della build dell'immagine nativa e la compressione dell'immagine utilizzando UPX.

Tieni presente che la build richiede molto più tempo, a seconda della macchina su cui esegui il test

...
[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] ------------------------------------------------------------------------

Verifica che le immagini siano state create:

docker images | grep image-analysis

Tagga ed esegui il push delle due immagini su GCR:

# 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. Esegui il deployment in Cloud Run

È ora di eseguire il deployment del servizio.

Esegui il deployment del servizio due volte: una volta utilizzando l'immagine JIT(JVM) e la seconda volta utilizzando l'immagine AOT(Native). Entrambi i deployment del servizio elaboreranno la stessa immagine dal bucket in parallelo, a scopo di confronto.

Innanzitutto, imposta le variabili di ambiente del progetto 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

Esegui il deployment dell'immagine JIT(JVM) e osserva il log di deployment nella console:

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

Esegui il deployment dell'immagine AOT(nativa) e osserva il log di deployment nella console:

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. Configura i trigger Eventarc

Eventarc offre una soluzione standardizzata per gestire il flusso delle modifiche dello stato, chiamate eventi, tra microservizi disaccoppiati. Quando viene attivato, Eventarc indirizza questi eventi tramite le sottoscrizioni Pub/Sub a varie destinazioni (in questo documento, vedi Destinazioni degli eventi) e gestisce per te la distribuzione, la sicurezza, l'autorizzazione, l'osservabilità e la gestione degli errori.

Puoi creare un trigger Eventarc in modo che il tuo servizio Cloud Run riceva notifiche di un evento o di un insieme di eventi specifici. Specificando i filtri per il trigger, puoi configurare il routing dell'evento, inclusi l'origine evento e il servizio Cloud Run di destinazione.

Innanzitutto, imposta le variabili di ambiente del progetto 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

Concedi pubsub.publisher al service account Cloud Storage:

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'

Configura i trigger Eventarc per le immagini del servizio JVM(JIT) e AOT(nativo) per elaborare l'immagine:

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    

Nota che sono stati creati due trigger:

gcloud eventarc triggers list --location=eu

14. Versioni del servizio di test

Una volta completati i deployment dei servizi, pubblica un'immagine in Cloud Storage, verifica se i nostri servizi sono stati richiamati, cosa restituisce l'API Vision e se i metadati sono archiviati in Firestore.

Torna a Cloud Storage e fai clic sul bucket che abbiamo creato all'inizio del lab:

ff8a6567afc76235.png

Una volta nella pagina dei dettagli del bucket, fai clic sul pulsante Upload files per caricare un'immagine.

Ad esempio, un'immagine GeekHour.jpeg viene fornita con il codebase in /services/image-analysis/java. Seleziona un'immagine e premi Open button:

347b76e8b775f2f5.png

Ora puoi controllare l'esecuzione del servizio, a partire da image-analysis-jvm, seguito da image-analysis-native.

Dal menu "hamburger" (☰), vai al servizio Cloud Run > image-analysis-jvm.

Fai clic su Log e osserva l'output:

810a8684414ceafa.png

E infatti, nell'elenco dei log, posso vedere che è stato richiamato il servizio JIT(JVM) image-analysis-jvm.

I log indicano l'inizio e la fine dell'esecuzione del servizio. Nel mezzo, possiamo vedere i log inseriti nella funzione con le istruzioni di log a livello INFO. Vediamo:

  • I dettagli dell'evento che attiva la nostra funzione.
  • I risultati non elaborati della chiamata API Vision,
  • Le etichette trovate nell'immagine che abbiamo caricato,
  • Le informazioni sui colori dominanti,
  • Se l'immagine è sicura da mostrare.
  • e alla fine i metadati dell'immagine sono stati archiviati in Firestore.

Ripeti la procedura per il servizio image-analysis-native.

Dal menu "hamburger" (☰), vai al servizio Cloud Run > image-analysis-native.

Fai clic su Log e osserva l'output:

b80308c7d0f55a3.png

Ora devi verificare se i metadati dell'immagine sono stati archiviati in Fiorestore.

Sempre dal menu "hamburger" (☰), vai alla sezione Firestore. Nella sottosezione Data (visualizzata per impostazione predefinita), dovresti vedere la raccolta pictures con un nuovo documento aggiunto, corrispondente all'immagine che hai appena caricato:

933a20a9709cb006.png

15. Pulizia (facoltativo)

Se non intendi continuare con gli altri lab della serie, puoi eseguire la pulizia delle risorse per risparmiare sui costi e per essere un buon cittadino del cloud. Puoi ripulire le risorse singolarmente nel seguente modo.

Elimina il bucket:

gsutil rb gs://${BUCKET_PICTURES}

Elimina la funzione:

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

Elimina la raccolta Firestore selezionando Elimina raccolta dalla raccolta:

410b551c3264f70a.png

In alternativa, puoi eliminare l'intero progetto:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. Complimenti!

Complimenti! Hai implementato correttamente il primo servizio chiave del progetto.

Argomenti trattati

  • Cloud Storage
  • Cloud Run
  • API Cloud Vision
  • Cloud Firestore
  • Immagini Java native

Passaggi successivi