Pic-a-daily: armazene e analise imagens com as bibliotecas de cliente Java nativas do Google

1. Visão geral

No primeiro codelab, você vai armazenar fotos em um bucket. Isso vai gerar um evento de criação de arquivo que será processado por um serviço implantado no Cloud Run. O serviço fará uma chamada para a API Vision para analisar imagens e salvar os resultados em um datastore.

427de3100de3a61e.png

O que você vai aprender

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

2. Configuração e requisitos

Configuração de ambiente autoguiada

  1. Faça login no Console do Google Cloud e crie um novo projeto ou reutilize um existente. Crie uma conta do Gmail ou do Google Workspace, se ainda não tiver uma.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • O Nome do projeto é o nome de exibição para os participantes do projeto. É uma string de caracteres não usada pelas APIs do Google É possível atualizar o local a qualquer momento.
  • O ID do projeto precisa ser exclusivo em todos os projetos do Google Cloud e não pode ser alterado após a definição. O console do Cloud gera automaticamente uma string exclusiva. Em geral, não importa o que seja. Na maioria dos codelabs, é necessário fazer referência ao ID do projeto, normalmente identificado como PROJECT_ID. Se você não gostar do ID gerado, crie outro aleatório. Se preferir, teste o seu e confira se ele está disponível. Ele não pode ser mudado após essa etapa e permanece durante o projeto.
  • Para sua informação, há um terceiro valor, um Número do projeto, que algumas APIs usam. Saiba mais sobre esses três valores na documentação.
  1. Em seguida, ative o faturamento no console do Cloud para usar os recursos/APIs do Cloud. A execução deste codelab não será muito cara, se tiver algum custo. Para encerrar os recursos e evitar cobranças além deste tutorial, exclua os recursos criados ou o projeto inteiro. Novos usuários do Google Cloud estão qualificados para o programa de US$ 300 de avaliação sem custos.

Inicie o Cloud Shell

Embora o Google Cloud e o Spanner possam ser operados remotamente do seu laptop, neste codelab usaremos o Google Cloud Shell, um ambiente de linha de comando executado no Cloud.

No Console do Google Cloud, clique no ícone do Cloud Shell na barra de ferramentas superior à direita:

55efc1aaa7a4d3ad.png

O provisionamento e a conexão com o ambiente levarão apenas alguns instantes para serem concluídos: Quando o processamento for concluído, você verá algo como:

7ffe5cbb04455448.png

Essa máquina virtual contém todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Neste codelab, todo o trabalho pode ser feito com um navegador. Você não precisa instalar nada.

3. Ativar APIs

Neste laboratório, você vai usar o Cloud Functions e a API Vision, mas primeiro eles precisam ser ativados no console do Cloud ou com gcloud.

Para ativar a API Vision no Console do Cloud, pesquise Cloud Vision API na barra de pesquisa:

cf48b1747ba6a6fb.png

Você vai acessar a página da API Cloud Vision:

ba4af419e6086fbb.png

Clique no botão ENABLE.

Como alternativa, você também pode ativar o Cloud Shell usando a ferramenta de linha de comando gcloud.

No Cloud Shell, execute este comando:

gcloud services enable vision.googleapis.com

A operação será concluída com sucesso:

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

Ative também o Cloud Run e o Cloud Build:

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

4. Criar o bucket (console)

Crie um bucket de armazenamento para as fotos. É possível fazer isso no console do Google Cloud Platform ( console.cloud.google.com) ou com a ferramenta de linha de comando gsutil no Cloud Shell ou no seu ambiente de desenvolvimento local.

No menu "hambúrguer" (☰), navegue até a página Storage.

1930e055d138150a.png

Nomeie seu bucket

Clique no botão CREATE BUCKET.

34147939358517f8.png

Clique em CONTINUE.

Escolher local

197817f20be07678.png

Crie um bucket multirregional na região de sua escolha (aqui Europe).

Clique em CONTINUE.

Escolher a classe de armazenamento padrão

53cd91441c8caf0e.png

Escolha a classe de armazenamento Standard para seus dados.

Clique em CONTINUE.

Definir controle de acesso

8c2b3b459d934a51.png

Como você vai trabalhar com imagens acessíveis ao público, é importante que todas as fotos armazenadas nesse bucket tenham o mesmo controle de acesso uniforme.

Escolha a opção de controle de acesso Uniform.

Clique em CONTINUE.

Definir proteção/criptografia

d931c24c3e705a68.png

Mantenha o padrão (Google-managed key)), já que você não vai usar suas próprias chaves de criptografia.

Clique em CREATE para finalizar a criação do bucket.

Adicionar allUsers como leitor de armazenamento

Acesse a guia Permissions:

d0ecfdcff730ea51.png

Adicione um membro allUsers ao bucket com o papel Storage > Storage Object Viewer da seguinte maneira:

e9f25ec1ea0b6cc6.png

Clique em SAVE.

5. Criar o bucket (gsutil)

Também é possível usar a ferramenta de linha de comando gsutil no Cloud Shell para criar buckets.

No Cloud Shell, defina uma variável para o nome exclusivo do bucket. O Cloud Shell já tem GOOGLE_CLOUD_PROJECT definido como o ID exclusivo do seu projeto. Você pode anexar isso ao nome do bucket.

Exemplo:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Crie uma zona padrão multirregional na Europa:

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

Verifique se o acesso uniforme no nível do bucket está ativado:

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

Torne o bucket público:

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

Se você acessar a seção Cloud Storage do console, vai encontrar um bucket uploaded-pictures público:

a98ed4ba17873e40.png

Teste se é possível fazer upload de fotos para o bucket e se elas estão disponíveis publicamente, conforme explicado na etapa anterior.

6. Testar o acesso público ao bucket

Voltando ao navegador de armazenamento, você vai ver seu bucket na lista, com acesso "Público" (incluindo um sinal de alerta lembrando que qualquer pessoa tem acesso ao conteúdo desse bucket).

89e7a4d2c80a0319.png

Seu bucket está pronto para receber fotos.

Se você clicar no nome do bucket, vai ver os detalhes dele.

131387f12d3eb2d3.png

Lá, você pode clicar no botão Upload files para testar se é possível adicionar uma imagem ao bucket. Uma janela pop-up vai pedir para você selecionar um arquivo. Depois de selecionado, ele será enviado por upload para seu bucket, e você verá novamente o acesso public atribuído automaticamente a esse novo arquivo.

e87584471a6e9c6d.png

Ao lado do marcador de acesso Public, você também vai encontrar um pequeno ícone de link. Ao clicar nele, seu navegador vai navegar até o URL público da imagem, que terá o seguinte formato:

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

Em que BUCKET_NAME é o nome globalmente exclusivo escolhido para o bucket e o nome do arquivo da imagem.

Ao clicar na caixa de seleção ao lado do nome da imagem, o botão DELETE será ativado, e você poderá excluir a primeira imagem.

7. Preparar o banco de dados

Você vai armazenar informações sobre a imagem fornecida pela API Vision no banco de dados Cloud Firestore, um banco de dados de documentos NoSQL rápido, totalmente gerenciado, sem servidor e nativo da nuvem. Prepare seu banco de dados acessando a seção Firestore do Console do Cloud:

9e4708d2257de058.png

Há duas opções: Native mode ou Datastore mode. Use o modo nativo, que oferece recursos extras, como suporte off-line e sincronização em tempo real.

Clique em SELECT NATIVE MODE.

9449ace8cc84de43.png

Escolha uma multirregião (aqui na Europa, mas o ideal é pelo menos a mesma região da função e do bucket de armazenamento).

Clique no botão CREATE DATABASE.

Depois que o banco de dados for criado, você verá o seguinte:

56265949a124819e.png

Crie uma coleção clicando no botão + START COLLECTION.

Nomeie a coleção pictures.

75806ee24c4e13a7.png

Não é necessário criar um documento. Você vai adicioná-los de forma programática à medida que novas imagens forem armazenadas no Cloud Storage e analisadas pela API Vision.

Clique em Save.

O Firestore cria um primeiro documento padrão na coleção recém-criada. Você pode excluir esse documento com segurança, porque ele não contém informações úteis:

5c2f1e17ea47f48f.png

Os documentos que serão criados de forma programática na nossa coleção vão conter quatro campos:

  • name (string): o nome do arquivo da imagem enviada, que também é a chave do documento.
  • labels (matriz de strings): os rótulos dos itens reconhecidos pela API Vision.
  • color (string): o código hexadecimal da cor dominante (por exemplo, #ab12ef)
  • created (data): o carimbo de data/hora de quando os metadados da imagem foram armazenados.
  • thumbnail (booleano): um campo opcional que estará presente e será verdadeiro se uma imagem em miniatura tiver sido gerada para essa foto.

Como vamos pesquisar no Firestore para encontrar fotos com miniaturas disponíveis e classificar pela data de criação, precisamos criar um índice de pesquisa.

É possível criar o índice com o seguinte comando no Cloud Shell:

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

Ou você pode fazer isso no console do Cloud. Clique em Indexes na coluna de navegação à esquerda e crie um índice composto, conforme mostrado abaixo:

ecb8b95e3c791272.png

Clique em Create. A criação do índice pode levar alguns minutos.

8. Clonar o código

Clone o código, caso ainda não tenha feito isso no codelab anterior:

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

Em seguida, acesse o diretório que contém o serviço para começar a criar o laboratório:

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

Você terá o seguinte layout de arquivo para o serviço:

f79613aff479d8ad.png

9. Conhecer o código do serviço

Primeiro, veja como as bibliotecas de cliente Java são ativadas no pom.xml usando uma BOM:

Primeiro, edite o arquivo pom.xml, que lista as dependências da função Java. Atualize o código para adicionar a dependência do Maven da 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>

A funcionalidade é implementada na classe EventController. Toda vez que uma nova imagem é enviada para o bucket, o serviço recebe uma notificação para processar:

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

O código vai validar os cabeçalhos 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);
} 

Agora, uma solicitação pode ser criada, e o código vai preparar uma dessas solicitações para ser enviada ao 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);

Estamos pedindo três recursos principais da API Vision:

  • Detecção de rótulos: para entender o que há nas fotos
  • Propriedades da imagem: para fornecer atributos interessantes da imagem (a cor dominante)
  • Pesquisa segura: para saber se a imagem é segura para exibição (não deve conter conteúdo adulto, médico, sugestivo ou violento)

Neste ponto, podemos fazer a chamada para a API Vision:

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

Para referência, confira como é a resposta da 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 não houver um erro retornado, podemos continuar. Por isso, temos este bloco "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 receber os rótulos das coisas, categorias ou temas reconhecidos na imagem:

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

Queremos saber a cor dominante da imagem:

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

Vamos verificar se a imagem é segura para mostrar:

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 as características de conteúdo adulto, falsificação, médico, violência e conteúdo sugestivo para saber se elas não são prováveis ou muito prováveis.

Se o resultado da pesquisa segura for positivo, podemos armazenar metadados no 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. Criar imagens de app com o GraalVM (opcional)

Nesta etapa opcional, você vai criar um JIT(JVM) based app image e um AOT(Native) Java app image usando o GraalVM.

Para executar o build, verifique se você tem um JDK adequado e o criador de native-image instalado e configurado. Há várias opções disponíveis.

To start, faça o download da GraalVM 22.2.x Community Edition e siga as instruções na página Instalação do GraalVM.

Esse processo pode ser muito simplificado com a ajuda do SDKMAN!

Para instalar a distribuição do JDK adequada com SDKman, comece usando o comando de instalação:

sdk install java 22.2.r17-grl

Instrua o SDKman a usar essa versão para builds JIT e AOT:

sdk use java 22.2.0.r17-grl

Instale o native-image utility para GraalVM:

gu install native-image

No Cloudshell, para sua conveniência, é possível instalar o GraalVM e o utilitário native-image com estes comandos simples:

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

Primeiro, defina as variáveis de ambiente do projeto do GCP:

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

Em seguida, acesse o diretório que contém o serviço para começar a criar o laboratório:

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

Crie a imagem do aplicativo JIT(JVM):

./mvnw package -Pjvm

Observe o registro de build no 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] ------------------------------------------------------------------------

Crie a imagem AOT(nativa):

./mvnw package -Pnative -DskipTests

Observe o registro do build no terminal, incluindo os registros do build de imagem nativa:

A compilação leva um pouco mais de tempo, dependendo da máquina em que você está testando.

...
[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. Criar e publicar imagens de contêiner

Vamos criar uma imagem do contêiner em duas versões diferentes: uma como JIT(JVM) image e outra como AOT(Native) Java image.

Primeiro, defina as variáveis de ambiente do projeto do GCP:

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

Crie a imagem JIT(JVM):

./mvnw package -Pjvm-image

Observe o registro de build no 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] ------------------------------------------------------------------------

Crie a imagem AOT(nativa):

./mvnw package -Pnative-image

Observe o registro de build no terminal, incluindo os registros de build de imagem nativa e a compactação de imagem usando UPX.

A compilação leva um pouco mais de tempo, dependendo da máquina em que você está testando.

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

Valide se as imagens foram criadas:

docker images | grep image-analysis

Marque e envie as duas imagens para o 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. Implantar no Cloud Run

É hora de implantar o serviço.

Você vai implantar o serviço duas vezes, uma usando a imagem JIT(JVM) e a segunda usando a imagem AOT(nativa). As duas implantações de serviço vão processar a mesma imagem do bucket em paralelo para fins de comparação.

Primeiro, defina as variáveis de ambiente do projeto do 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

Implante a imagem JIT(JVM) e observe o registro de implantação no 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

Implante a imagem AOT(nativa) e observe o registro de implantação no 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. Configurar gatilhos do Eventarc

O Eventarc oferece uma solução padronizada para gerenciar o fluxo de mudanças de estado, chamadas de eventos, entre microsserviços separados. Quando acionado, o Eventarc encaminha esses eventos por meio de assinaturas do Pub/Sub para vários destinos (neste documento, consulte Destinos de eventos) enquanto gerencia a entrega, a segurança, a autorização, a observabilidade e o tratamento de erros para você.

É possível criar um gatilho do Eventarc para que o serviço do Cloud Run receba notificações de um evento especificado ou conjunto de eventos. Ao especificar filtros para o gatilho, é possível configurar o roteamento do evento, incluindo a origem do evento e o serviço de destino do Cloud Run.

Primeiro, defina as variáveis de ambiente do projeto do 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

Conceda pubsub.publisher à conta de serviço do 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'

Configure gatilhos do Eventarc para imagens de serviço JVM(JIT) e AOT(nativo) para processar a imagem:

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    

Observe que os dois acionadores foram criados:

gcloud eventarc triggers list --location=eu

14. Testar versões de serviço

Depois que as implantações de serviço forem bem-sucedidas, você vai postar uma foto no Cloud Storage, verificar se os serviços foram invocados, o que a API Vision retorna e se os metadados são armazenados no Firestore.

Volte para Cloud Storage e clique no bucket que criamos no início do laboratório:

ff8a6567afc76235.png

Na página de detalhes do bucket, clique no botão Upload files para fazer upload de uma imagem.

Por exemplo, uma imagem GeekHour.jpeg é fornecida com sua base de código em /services/image-analysis/java. Selecione uma imagem e pressione Open button:

347b76e8b775f2f5.png

Agora é possível verificar a execução do serviço, começando com image-analysis-jvm e depois image-analysis-native.

No menu "hambúrguer" (☰), acesse o serviço Cloud Run > image-analysis-jvm.

Clique em "Registros" e observe a saída:

810a8684414ceafa.png

E, de fato, na lista de registros, posso ver que o serviço JIT(JVM) image-analysis-jvm foi invocado.

Os registros indicam o início e o fim da execução do serviço. Entre eles, podemos ver os registros que colocamos na função com as instruções de registro no nível INFO. Podemos notar que:

  • Os detalhes do evento que aciona nossa função,
  • Os resultados brutos da chamada da API Vision,
  • Os rótulos encontrados na imagem que enviamos,
  • As informações de cores dominantes,
  • Se a imagem é segura para mostrar,
  • E, por fim, esses metadados sobre a imagem foram armazenados no Firestore.

Repita o processo para o serviço image-analysis-native.

No menu "hambúrguer" (☰), acesse o serviço Cloud Run > image-analysis-native.

Clique em "Registros" e observe a saída:

b80308c7d0f55a3.png

Agora, observe se os metadados da imagem foram armazenados no Fiorestore.

No menu "hambúrguer" (☰), acesse a seção Firestore. Na subseção Data (mostrada por padrão), você vai encontrar a coleção pictures com um novo documento adicionado, correspondente à imagem que você acabou de enviar:

933a20a9709cb006.png

15. Limpeza (opcional)

Se você não pretende continuar com os outros laboratórios da série, limpe os recursos para economizar custos e ser um bom usuário da nuvem. É possível limpar os recursos individualmente da seguinte maneira.

Excluir o bucket:

gsutil rb gs://${BUCKET_PICTURES}

Exclua a função:

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

Exclua a coleção do Firestore selecionando "Excluir coleção" na coleção:

410b551c3264f70a.png

Se preferir, exclua todo o projeto:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. Parabéns!

Parabéns! Você implementou o primeiro serviço de chaves do projeto!

O que vimos

  • Cloud Storage
  • Cloud Run
  • API Cloud Vision
  • Cloud Firestore
  • Imagens nativas em Java

Próximas etapas