Pic-a-daily: Lab 1: Cómo almacenar y analizar imágenes (Java nativo)

1. Descripción general

En el primer codelab, almacenarás imágenes en un bucket. Se generará un evento de creación de archivos que controlará un servicio implementado en Cloud Run. El servicio llamará a la API de Vision para realizar un análisis de imágenes y guardar los resultados en un almacén de datos.

c0650ee4a76db35e.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.

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.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 cuando quieras.
  • El ID del proyecto es ú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 de tu 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 usa 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 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 el proyecto. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de $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:

84688aa223b1c3a2.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:

320e18fedb7fbe0.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 deberás habilitarlas en la consola de Cloud o con gcloud.

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

8f3522d790bb026c.png

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

d785572fa14c87c2.png

Haz clic en el botón ENABLE.

Como alternativa, también puedes habilitarlo en 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)

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

De la “hamburguesa” (⁕), navega a la página Storage.

d08ecb0ae29330a1.png

Asígnale un nombre al bucket

Haz clic en el botón CREATE BUCKET.

8951851554a430d2.png

Haz clic en CONTINUE.

Elige la ubicación

24b24625157ab467.png

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

Haz clic en CONTINUE.

Elige la clase de almacenamiento predeterminada

9e7bd365fa94a2e0.png

Elige la clase de almacenamiento Standard para tus datos.

Haz clic en CONTINUE.

Configura el control de acceso

1ff4a1f6e57045f5.png

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

Elige la opción de control de acceso Uniform.

Haz clic en CONTINUE.

Configuración de protección/encriptación

2d469b076029d365.png

Mantén la configuració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:

19564b3ad8688ae8.png

Agrega un miembro allUsers al bucket, con una función de Storage > Storage Object Viewer, de la siguiente manera:

d655e760c76d62c1.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, establece una variable para el nombre único del bucket. Cloud Shell ya tiene establecido GOOGLE_CLOUD_PROJECT en el ID único de tu proyecto. Puedes agregarlo al nombre del bucket.

Por ejemplo:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Crea una zona multirregional estándar en Europa:

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

Garantiza el acceso uniforme a nivel de bucket:

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 público uploaded-pictures:

65c63ef4a6eb30ad.png

Prueba que puedes subir fotos al bucket y que estén disponibles públicamente, como se explicó en el paso anterior.

6. Prueba el acceso público al bucket

Si regresas al navegador de Storage, verás tu bucket en la lista, que dice "Público". (incluso una señal de advertencia que te recuerda que cualquier persona tiene acceso al contenido de ese bucket).

e639a9ba625b71a6.png

Tu bucket ya está listo para recibir fotos.

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

1f88a2290290aba8.png

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

1209e7ebe1f63b10.png

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

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

BUCKET_NAME es el nombre global único que elegiste para tu bucket y, luego, el nombre de archivo de tu foto.

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

7. Prepara la base de datos

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

e57a673537b5deca.png

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

Haz clic en SELECT NATIVE MODE.

1a2e363fae5c7e96.png

Elige una multirregión (aquí en Europa, pero idealmente que sea, al menos, la misma región que la función y el bucket de almacenamiento).

Haz clic en el botón CREATE DATABASE.

Una vez creada la base de datos, deberías ver lo siguiente:

7dcc82751ed483fb.png

Para crear una colección nueva, haz clic en el botón + START COLLECTION.

Colección de nombres pictures.

dce3d73884ac8c83.png

No es necesario que crees un documento. Las agregarás de manera programática a medida que las fotos nuevas se almacenen 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:

63e95c844b3f79d3.png

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

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

Debido a que buscaremos en Firestore para encontrar imágenes que tengan miniaturas disponibles y ordenaremos según la fecha de creación, necesitaremos 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 la consola de Cloud. 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:

2236d3a024a59232.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 archivo para el servicio:

4c2a18a2c8b69dc5.png

9. Explora el código de servicio

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

Primero, abre el archivo pom.xml, que enumera las dependencias de nuestra app de Java. se centra en el uso de las APIs de Vision, Cloud Storage y Firestore

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

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 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 de esas solicitudes para enviarse al 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 fotos
  • Propiedades de imágenes: Para proporcionar atributos interesantes de la imagen (nos interesa el color dominante de la imagen)
  • Búsqueda segura: para saber si una imagen es segura (no debe incluir contenido para adultos, médico, subido de tono ni violento)

En este punto, podemos realizar la siguiente llamada a la API de Vision:

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

A modo de referencia, así es como 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 un error, podemos continuar. Por eso 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);
}

Vamos a obtener las etiquetas de las cosas, categorías o temas reconocidos 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 es seguro mostrar la imagen:

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

Estamos verificando las características de la adultez, la falsificación de identidad, la medicina, la violencia o el subido de tono para determinar si no son probables o muy probables.

Si el resultado de la búsqueda segura es aceptable, podemos almacenar metadatos en Firestore:

// 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. Compila imágenes de apps con GraalVM

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

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

To start, descarga GraalVM 22.3.x Community Edition y sigue las instrucciones de la página de instalación de GraalVM.

Este proceso se puede simplificar considerablemente con la ayuda de SDKMAN!

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

sdk install java 17.0.8-graal

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

sdk use java 17.0.8-graal

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

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

Primero, configura 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 con JIT:

./mvnw package

Observa el registro de compilación en la terminal:

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

Compila la imagen nativa(usa AOT):

./mvnw native:compile -Pnative

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 un poco más, según la máquina en la que realices la prueba.

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

Compilemos una imagen de contenedor en dos versiones diferentes: una como JIT image y la otra como Native Java image.

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

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

Compila la imagen de JIT:

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

Observa el registro de compilación en la terminal:

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

Compila la imagen AOT(nativa):

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

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

Nota:

  • que la compilación tarda bastante más, según la máquina en la que realices la prueba
  • Las imágenes se pueden comprimir aún más con UPX; sin embargo, tienen un pequeño impacto negativo en el rendimiento del inicio, por lo que esta compilación no utiliza UPX; siempre es una leve compensación.
...
[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] ------------------------------------------------------------------------

Verifica que las imágenes se hayan compilado:

docker images | grep image-analysis

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

Es hora de implementar el servicio.

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

Primero, configura 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 y observa el registro de implementación en la consola:

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

Implementa la imagen 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-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. 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 a 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 manejo de errores por ti.

Puedes crear un activador de Eventarc para que tu servicio de Cloud Run reciba notificaciones de un evento específico o de un conjunto de eventos. 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, configura 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 imágenes de servicio nativas y de JIT a fin de procesar la imagen:

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    

Observa que se crearon los dos activadores:

gcloud eventarc triggers list --location=eu

14. Versiones del servicio de prueba

Una vez que las implementaciones del servicio se realicen correctamente, publicarás una foto en Cloud Storage para ver si se invocaron nuestros servicios, qué muestra la API de Vision y si los metadatos están almacenados en Firestore.

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

33442485a1d76921.png

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

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

d57529452f62bd32.png

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

De la “hamburguesa” (⁕), navega al servicio de Cloud Run > image-analysis-jit.

Haz clic en Registros y observa el resultado:

ae1a4a94c7c7a166.png

De hecho, en la lista de registros, puedo ver que se invocó el servicio JIT image-analysis-jit.

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

  • Los detalles del evento que activa la función
  • Los resultados sin procesar de la llamada a la API de Vision
  • Las etiquetas que se encontraron en la foto que subimos
  • La información de los colores dominantes,
  • Si es seguro mostrar la imagen,
  • Finalmente, esos metadatos sobre la foto se almacenaron en Firestore.

Deberás repetir el proceso para el servicio image-analysis-native.

De la “hamburguesa” (⁕), navega al servicio de Cloud Run > image-analysis-native.

Haz clic en Registros y observa el resultado:

4afe22833c1fd14c.png

Ahora, querrá observar si los metadatos de la imagen se almacenaron en Fiorestore.

Una vez más, desde el punto de vista 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:

82d6c468956e7cfc.png

15. Limpieza (opcional)

Si no pretendes continuar con los otros labs de la serie, puedes liberar recursos para ahorrar costos y ser un buen ciudadano de la nube en general. 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 de la colección:

6cc86a7b88fdb4d3.png

También puedes borrar todo el proyecto:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. ¡Felicitaciones!

¡Felicitaciones! Implementaste con éxito el primer servicio de claves del proyecto.

Temas abordados

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

Próximos pasos