Pic-a-daily: Guarda y analiza imágenes con las bibliotecas cliente nativas de Java de Google

1. Descripción general

En el primer lab de programación, almacenarás imágenes en un bucket. Esto generará un evento de creación de archivos que controlará un servicio implementado en Cloud Run. El servicio realizará una llamada a la API de Vision para analizar la imagen y guardar los resultados en un almacén de datos.

427de3100de3a61e.png

Qué aprenderás

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

2. Configuración y requisitos

Configuración del entorno de autoaprendizaje

  1. Accede a Google Cloud Console y crea un proyecto nuevo o reutiliza uno existente. Si aún no tienes una cuenta de Gmail o de Google Workspace, debes crear una.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • El Nombre del proyecto es el nombre visible de los participantes de este proyecto. Es una cadena de caracteres que no se utiliza en las APIs de Google. Puedes actualizarla en cualquier momento.
  • El ID del proyecto debe ser único en todos los proyectos de Google Cloud y es inmutable (no se puede cambiar después de configurarlo). La consola de Cloud genera automáticamente una cadena única. Por lo general, no importa cuál sea. En la mayoría de los codelabs, deberás hacer referencia al ID del proyecto (suele identificarse como PROJECT_ID). Si no te gusta el ID que se generó, podrías generar otro aleatorio. También puedes probar uno propio y ver si está disponible. No se puede cambiar después de este paso y se usará el mismo durante todo el proyecto.
  • Recuerda que hay un tercer valor, un número de proyecto, que usan algunas APIs. Obtén más información sobre estos tres valores en la documentación.
  1. A continuación, deberás habilitar la facturación en la consola de Cloud para usar las APIs o los recursos de Cloud. Ejecutar este codelab no debería costar mucho, tal vez nada. Para cerrar recursos y evitar que se generen cobros más allá de este instructivo, puedes borrar los recursos que creaste o borrar todo el proyecto. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de USD 300.

Inicia Cloud Shell

Si bien Google Cloud y Spanner se pueden operar de manera remota desde tu laptop, en este codelab usarás Google Cloud Shell, un entorno de línea de comandos que se ejecuta en la nube.

En Google Cloud Console, haz clic en el ícono de Cloud Shell en la barra de herramientas en la parte superior derecha:

55efc1aaa7a4d3ad.png

El aprovisionamiento y la conexión al entorno deberían tomar solo unos minutos. Cuando termine el proceso, debería ver algo como lo siguiente:

7ffe5cbb04455448.png

Esta máquina virtual está cargada con todas las herramientas de desarrollo que necesitarás. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Todo tu trabajo en este codelab se puede hacer en un navegador. No es necesario que instales nada.

3. Habilita las APIs

En este lab, usarás Cloud Functions y la API de Vision, pero primero debes habilitarlas en la consola de Cloud o con gcloud.

Para habilitar la API de Vision en Cloud Console, busca Cloud Vision API en la barra de búsqueda:

cf48b1747ba6a6fb.png

Llegarás a la página de la API de Cloud Vision:

ba4af419e6086fbb.png

Haz clic en el botón ENABLE.

Como alternativa, también puedes habilitar Cloud Shell con la herramienta de línea de comandos de gcloud.

En Cloud Shell, ejecuta el siguiente comando:

gcloud services enable vision.googleapis.com

Deberías ver que la operación finaliza correctamente:

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

También habilita Cloud Run y Cloud Build:

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

4. Crea el bucket (consola)

Crea un bucket de almacenamiento para las imágenes. Puedes hacerlo desde la consola de Google Cloud Platform ( console.cloud.google.com) o con la herramienta de línea de comandos de gsutil desde Cloud Shell o tu entorno de desarrollo local.

En el menú de hamburguesa (☰), navega a la página Storage.

1930e055d138150a.png

Asigna un nombre a tu bucket

Haz clic en el botón CREATE BUCKET.

34147939358517f8.png

Haz clic en CONTINUE.

Elegir ubicación

197817f20be07678.png

Crea un bucket multirregional en la región que elijas (aquí Europe).

Haz clic en CONTINUE.

Elige la clase de almacenamiento predeterminada

53cd91441c8caf0e.png

Elige la clase de almacenamiento Standard para tus datos.

Haz clic en CONTINUE.

Establecer control de acceso

8c2b3b459d934a51.png

Como trabajarás con imágenes de acceso público, quieres que todas las imágenes almacenadas en este bucket tengan el mismo control de acceso uniforme.

Elige la opción de control de acceso Uniform.

Haz clic en CONTINUE.

Establecer protección/encriptación

d931c24c3e705a68.png

Mantén la opción predeterminada (Google-managed key)), ya que no usarás tus propias claves de encriptación.

Haz clic en CREATE para finalizar la creación del bucket.

Agrega allUsers como visualizador de almacenamiento

Ve a la pestaña Permissions:

d0ecfdcff730ea51.png

Agrega un miembro allUsers al bucket con el rol Storage > Storage Object Viewer de la siguiente manera:

e9f25ec1ea0b6cc6.png

Haz clic en SAVE.

5. Crea el bucket (gsutil)

También puedes usar la herramienta de línea de comandos de gsutil en Cloud Shell para crear buckets.

En Cloud Shell, configura una variable para el nombre único del bucket. Cloud Shell ya tiene GOOGLE_CLOUD_PROJECT establecido en tu ID del proyecto único. Puedes agregar ese valor al nombre del bucket.

Por ejemplo:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Crea una zona estándar multirregional en Europa:

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

Asegúrate de que el acceso uniforme a nivel de bucket esté habilitado:

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

Configura el bucket como público:

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

Si vas a la sección Cloud Storage de la consola, deberías tener un bucket uploaded-pictures público:

a98ed4ba17873e40.png

Comprueba que puedes subir fotos al bucket y que las fotos subidas están disponibles de forma pública, como se explicó en el paso anterior.

6. Prueba el acceso público al bucket

Si vuelves al navegador de almacenamiento, verás tu bucket en la lista, con acceso "Público" (incluido un signo de advertencia que te recuerda que cualquier persona tiene acceso al contenido de ese bucket).

89e7a4d2c80a0319.png

Tu bucket ahora está listo para recibir imágenes.

Si haces clic en el nombre del bucket, verás sus detalles.

131387f12d3eb2d3.png

Allí, puedes probar el botón Upload files para verificar que puedes agregar una imagen al bucket. Aparecerá una ventana emergente del selector de archivos en la que se te pedirá que selecciones un archivo. Una vez que lo selecciones, se subirá a tu bucket y volverás a ver el acceso public que se atribuyó automáticamente a este archivo nuevo.

e87584471a6e9c6d.png

Junto a la etiqueta de acceso Public, también verás un pequeño ícono de vínculo. Cuando hagas clic en ella, tu navegador navegará a la URL pública de esa imagen, que tendrá el siguiente formato:

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

donde BUCKET_NAME es el nombre único a nivel global que elegiste para tu bucket y, luego, el nombre de archivo de tu imagen.

Si haces clic en la casilla de verificación junto al nombre de la imagen, se habilitará el botón DELETE, y podrás borrar la primera imagen.

7. Prepara la base de datos

Almacenarás la información sobre la imagen proporcionada por la API de Vision en la base de datos de Cloud Firestore, una base de datos de documentos NoSQL nativa de la nube, rápida, completamente administrada y sin servidores. Para preparar tu base de datos, ve a la sección Firestore de Cloud Console:

9e4708d2257de058.png

Se ofrecen dos opciones: Native mode o Datastore mode. Usa el modo nativo, que ofrece funciones adicionales, como asistencia sin conexión y sincronización en tiempo real.

Haz clic en SELECT NATIVE MODE.

9449ace8cc84de43.png

Elige una región múltiple (aquí en Europa, pero lo ideal sería que fuera al menos la misma región en la que se encuentran tu función y tu bucket de almacenamiento).

Haz clic en el botón CREATE DATABASE.

Una vez que se cree la base de datos, deberías ver lo siguiente:

56265949a124819e.png

Haz clic en el botón + START COLLECTION para crear una colección nueva.

Colección de nombres pictures.

75806ee24c4e13a7.png

No es necesario que crees un documento. Los agregarás de forma programática a medida que se almacenen imágenes nuevas en Cloud Storage y se analicen con la API de Vision.

Haz clic en Save.

Firestore crea un primer documento predeterminado en la colección recién creada. Puedes borrarlo de forma segura, ya que no contiene información útil:

5c2f1e17ea47f48f.png

Los documentos que se crearán de forma programática en nuestra colección contendrán 4 campos:

  • name (cadena): Es el nombre del archivo de la imagen subida, que también es la clave del documento.
  • labels (array de cadenas): Son las etiquetas de los elementos reconocidos por la API de Vision.
  • color (cadena): Es el código de color hexadecimal del color predominante (p. ej., #ab12ef)
  • created (fecha): Es la marca de tiempo en la que se almacenaron los metadatos de esta imagen.
  • thumbnail (booleano): Es un campo opcional que estará presente y será verdadero si se generó una imagen en miniatura para esta foto.

Como buscaremos en Firestore imágenes que tengan miniaturas disponibles y las ordenaremos según la fecha de creación, deberemos crear un índice de búsqueda.

Puedes crear el índice con el siguiente comando en Cloud Shell:

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

También puedes hacerlo desde Cloud Console. Para ello, haz clic en Indexes en la columna de navegación de la izquierda y, luego, crea un índice compuesto como se muestra a continuación:

ecb8b95e3c791272.png

Haz clic en Create. La creación del índice puede tardar unos minutos.

8. Clona el código

Clona el código si aún no lo hiciste en el codelab anterior:

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

Luego, puedes ir al directorio que contiene el servicio para comenzar a compilar el lab:

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

Tendrás el siguiente diseño de archivos para el servicio:

f79613aff479d8ad.png

9. Explora el código del servicio

Para comenzar, observa cómo se habilitan las bibliotecas cliente de Java en pom.xml con una BOM:

Primero, edita el archivo pom.xml, que enumera las dependencias de nuestra función de Java. Actualiza el código para agregar la dependencia de Maven de la API de 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 funcionalidad se implementa en la clase EventController. Cada vez que se suba una imagen nueva al bucket, el servicio recibirá una notificación para procesarla:

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

El código procederá a validar los encabezados 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);
} 

Ahora se puede compilar una solicitud, y el código preparará una solicitud de ese tipo para enviarla 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);

Solicitamos 3 capacidades clave de la API de Vision:

  • Detección de etiquetas: Para comprender qué hay en esas imágenes
  • Propiedades de la imagen: Para proporcionar atributos interesantes de la imagen (nos interesa el color predominante de la imagen)
  • Búsqueda segura: Para saber si la imagen es segura para mostrar (no debe contener contenido para adultos, médico, subido de tono ni violento)

En este punto, podemos llamar a la API de Vision:

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

Como referencia, así se ve la respuesta de la API de 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
}

Si no se devuelve ningún error, podemos continuar, por lo que tenemos este bloque 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);
}

Obtendremos las etiquetas de las cosas, las categorías o los temas que se reconocen en la imagen:

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

Nos interesa conocer el color dominante de la imagen:

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

Comprobemos si la imagen es segura para mostrarla:

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

Verificamos las características de contenido para adultos, suplantación, médico, violencia y contenido subido de tono para determinar si no son probables o muy probables.

Si el resultado de la búsqueda segura es correcto, podemos almacenar metadatos en 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. Compila imágenes de la app con GraalVM (opcional)

En este paso opcional, compilarás un JIT(JVM) based app image y, luego, un AOT(Native) Java app image con GraalVM.

Para ejecutar la compilación, deberás asegurarte de tener un JDK adecuado y el compilador de native-image instalado y configurado. Hay varias opciones disponibles.

To start, descarga la Edición de la comunidad de GraalVM 22.2.x y sigue las instrucciones de la página de instalación de GraalVM.

Este proceso se puede simplificar en gran medida con la ayuda de SDKMAN!

Para instalar la distribución de JDK adecuada con SDKman, comienza por usar el comando install:

sdk install java 22.2.r17-grl

Indica a SDKman que use esta versión para las compilaciones JIT y AOT:

sdk use java 22.2.0.r17-grl

Instala native-image utility para GraalVM:

gu install native-image

En Cloudshell, para tu comodidad, puedes instalar GraalVM y la utilidad native-image con estos comandos sencillos:

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

Primero, establece las variables de entorno del proyecto de GCP:

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

Luego, puedes ir al directorio que contiene el servicio para comenzar a compilar el lab:

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

Compila la imagen de la aplicación JIT(JVM):

./mvnw package -Pjvm

Observa el registro de compilación en la terminal:

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

Compila la imagen de AOT(nativa):.

./mvnw package -Pnative -DskipTests

Observa el registro de compilación en la terminal, incluidos los registros de compilación de imágenes nativas:

Ten en cuenta que la compilación tarda bastante más, según la máquina en la que realices la prueba.

...
[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. Compila y publica imágenes de contenedores

Compilaremos una imagen de contenedor en dos versiones diferentes: una como JIT(JVM) image y la otra como AOT(Native) Java image.

Primero, establece las variables de entorno del proyecto de GCP:

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

Compila la imagen de JIT(JVM):

./mvnw package -Pjvm-image

Observa el registro de compilación en la terminal:

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

Compila la imagen de AOT(nativa):.

./mvnw package -Pnative-image

Observa el registro de compilación en la terminal, incluidos los registros de compilación de imágenes nativas y la compresión de imágenes con UPX.

Ten en cuenta que la compilación tarda bastante más, según la máquina en la que realices la prueba.

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

Valida que se hayan compilado las imágenes:

docker images | grep image-analysis

Etiqueta y envía las dos imágenes a 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. Implementa en Cloud Run

Es hora de implementar el servicio.

Implementarás el servicio dos veces, una con la imagen JIT(JVM) y la segunda con la imagen AOT(nativa). Ambas implementaciones del servicio procesarán la misma imagen del bucket en paralelo, para fines de comparación.

Primero, establece las variables de entorno del proyecto de 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

Implementa la imagen de JIT(JVM) y observa el registro de implementación en la consola:

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

Implementa la imagen de AOT(nativa) y observa el registro de implementación en la consola:

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 activadores de Eventarc

Eventarc ofrece una solución estandarizada para administrar el flujo de cambios de estado, llamados eventos, entre microservicios separados. Cuando se activa, Eventarc enruta estos eventos a través de suscripciones de Pub/Sub a varios destinos (en este documento, consulta Destinos de eventos) mientras administra la entrega, la seguridad, la autorización, la observabilidad y el control de errores por ti.

Puedes crear un activador de Eventarc para que el servicio de Cloud Run reciba notificaciones de un evento o del conjunto de eventos especificado. Si especificas filtros para el activador, puedes configurar el enrutamiento del evento, incluida la fuente del evento y el servicio de Cloud Run de destino.

Primero, establece las variables de entorno del proyecto de 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

Otorga pubsub.publisher a la cuenta de servicio de 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 activadores de Eventarc para las imágenes de servicio de JVM(JIT) y AOT(nativas) para procesar la imagen:

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    

Observa que se crearon los dos activadores:

gcloud eventarc triggers list --location=eu

14. Versiones de prueba del servicio

Una vez que las implementaciones del servicio se realicen correctamente, publicarás una imagen en Cloud Storage, verificarás si se invocaron nuestros servicios, qué devuelve la API de Vision y si los metadatos se almacenan en Firestore.

Regresa a Cloud Storage y haz clic en el bucket que creamos al comienzo del lab:

ff8a6567afc76235.png

Una vez que estés en la página de detalles del bucket, haz clic en el botón Upload files para subir una imagen.

Por ejemplo, se proporciona una imagen GeekHour.jpeg con tu base de código en /services/image-analysis/java. Selecciona una imagen y presiona Open button:

347b76e8b775f2f5.png

Ahora puedes verificar la ejecución del servicio, comenzando con image-analysis-jvm y, luego, image-analysis-native.

En el menú de opciones (☰), navega al servicio de Cloud Run > image-analysis-jvm.

Haz clic en Registros y observa el resultado:

810a8684414ceafa.png

Y, de hecho, en la lista de registros, puedo ver que se invocó el servicio JIT(JVM) image-analysis-jvm.

Los registros indican el inicio y el final de la ejecución del servicio. Entre ellos, podemos ver los registros que colocamos en nuestra función con las instrucciones de registro en el nivel INFO. Vemos lo siguiente:

  • Son los detalles del evento que activa nuestra función.
  • Son los resultados sin procesar de la llamada a la API de Vision.
  • Las etiquetas que se encontraron en la foto que subimos
  • Es la información de los colores predominantes.
  • Indica si la imagen es apta para mostrarse.
  • Finalmente, esos metadatos sobre la imagen se almacenaron en Firestore.

Repetirás el proceso para el servicio image-analysis-native.

En el menú de opciones (☰), navega al servicio de Cloud Run > image-analysis-native.

Haz clic en Registros y observa el resultado:

b80308c7d0f55a3.png

Ahora, observa si los metadatos de la imagen se almacenaron en Fiorestore.

Nuevamente, desde el menú de hamburguesa (☰), ve a la sección Firestore. En la subsección Data (que se muestra de forma predeterminada), deberías ver la colección pictures con un documento nuevo agregado, que corresponde a la imagen que acabas de subir:

933a20a9709cb006.png

15. Limpieza (opcional)

Si no quieres continuar con los otros labs de la serie, puedes limpiar los recursos para ahorrar costos y ser un buen ciudadano de la nube. Puedes limpiar los recursos de forma individual de la siguiente manera.

Borra el bucket:

gsutil rb gs://${BUCKET_PICTURES}

Borra la función:

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

Para borrar la colección de Firestore, selecciona Borrar colección en la colección:

410b551c3264f70a.png

Como alternativa, puedes borrar todo el proyecto:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. ¡Felicitaciones!

¡Felicitaciones! Implementaste correctamente el primer servicio clave del proyecto.

Temas abordados

  • Cloud Storage
  • Cloud Run
  • API de Cloud Vision
  • Cloud Firestore
  • Imágenes nativas de Java

Próximos pasos