1. Przegląd
W pierwszym module przechowasz zdjęcia w zasobniku. Spowoduje to wygenerowanie zdarzenia utworzenia pliku, które zostanie obsłużone przez usługę wdrożoną w Cloud Run. Usługa wywoła interfejs Vision API, aby przeprowadzić analizę obrazu i zapisać wyniki w magazynie danych.

Czego się nauczysz
- Cloud Storage
- Cloud Run
- Cloud Vision API
- Cloud Firestore
2. Konfiguracja i wymagania
Samodzielne konfigurowanie środowiska
- Zaloguj się w konsoli Google Cloud i utwórz nowy projekt lub użyj istniejącego. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.



- Nazwa projektu to wyświetlana nazwa uczestników tego projektu. Jest to ciąg znaków, który nie jest używany przez interfejsy API Google. Możesz ją zaktualizować w dowolnym momencie.
- Identyfikator projektu musi być unikalny we wszystkich projektach Google Cloud i jest niezmienny (nie można go zmienić po ustawieniu). Konsola Cloud automatycznie generuje unikalny ciąg znaków. Zwykle nie musisz się nim przejmować. W większości ćwiczeń z programowania musisz odwoływać się do identyfikatora projektu (zwykle jest on oznaczony jako
PROJECT_ID). Jeśli wygenerowany identyfikator Ci się nie podoba, możesz wygenerować inny losowy identyfikator. Możesz też spróbować własnej nazwy i sprawdzić, czy jest dostępna. Po tym kroku nie można go zmienić i będzie obowiązywać przez cały czas trwania projektu. - Warto wiedzieć, że istnieje też trzecia wartość, czyli numer projektu, z której korzystają niektóre interfejsy API. Więcej informacji o tych 3 wartościach znajdziesz w dokumentacji.
- Następnie musisz włączyć płatności w konsoli Cloud, aby korzystać z zasobów i interfejsów API Google Cloud. Ukończenie tego laboratorium nie powinno wiązać się z dużymi kosztami, a nawet z żadnymi. Aby wyłączyć zasoby i uniknąć naliczania opłat po zakończeniu tego samouczka, możesz usunąć utworzone zasoby lub cały projekt. Nowi użytkownicy Google Cloud mogą skorzystać z bezpłatnego okresu próbnego, w którym mają do dyspozycji środki w wysokości 300 USD.
Uruchamianie Cloud Shell
Z Google Cloud można korzystać zdalnie na laptopie, ale w tym module praktycznym będziesz używać Google Cloud Shell, czyli środowiska wiersza poleceń działającego w chmurze.
W konsoli Google Cloud kliknij ikonę Cloud Shell na pasku narzędzi w prawym górnym rogu:

Uzyskanie dostępu do środowiska i połączenie się z nim powinno zająć tylko kilka chwil. Po zakończeniu powinno wyświetlić się coś takiego:

Ta maszyna wirtualna zawiera wszystkie potrzebne narzędzia dla programistów. Zawiera również stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i usprawnia proces uwierzytelniania. Wszystkie zadania w tym laboratorium możesz wykonać w przeglądarce. Nie musisz niczego instalować.
3. Włącz interfejsy API
W tym module będziesz korzystać z Cloud Functions i interfejsu Vision API, ale najpierw musisz je włączyć w konsoli Google Cloud lub za pomocą gcloud.
Aby włączyć interfejs Vision API w konsoli Cloud, na pasku wyszukiwania wpisz Cloud Vision API:

Wyświetli się strona Cloud Vision API:

Kliknij przycisk ENABLE.
Możesz też włączyć go w Cloud Shell za pomocą narzędzia wiersza poleceń gcloud.
W Cloud Shell uruchom to polecenie:
gcloud services enable vision.googleapis.com
Operacja powinna zakończyć się powodzeniem:
Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.
Włącz też Cloud Run i Cloud Build:
gcloud services enable cloudbuild.googleapis.com \ run.googleapis.com
4. Tworzenie zasobnika (konsola)
Utwórz zasobnik na zdjęcia. Możesz to zrobić w konsoli Google Cloud Platform ( console.cloud.google.com) lub za pomocą narzędzia wiersza poleceń gsutil w Cloud Shell lub lokalnym środowisku programistycznym.
Otwórz Miejsce na dane
W menu „hamburger” (☰) otwórz stronę Storage.

Nazwij zasobnik
Kliknij przycisk CREATE BUCKET.

Kliknij CONTINUE.
Wybierz lokalizację

Utwórz zasobnik z wieloma regionami w wybranym regionie (w tym przypadku Europe).
Kliknij CONTINUE.
Wybierz domyślną klasę pamięci masowej

Wybierz klasę pamięci masowej Standard dla swoich danych.
Kliknij CONTINUE.
Ustawianie kontroli dostępu

Będziesz pracować z publicznie dostępnymi obrazami, więc chcesz, aby wszystkie zdjęcia przechowywane w tym zasobniku miały takie same, jednolite ustawienia kontroli dostępu.
Wybierz opcję kontroli dostępu Uniform.
Kliknij CONTINUE.
Ustawianie ochrony/szyfrowania

Zachowaj ustawienie domyślne (Google-managed key)), ponieważ nie będziesz używać własnych kluczy szyfrowania.
Kliknij CREATE, aby zakończyć tworzenie zasobnika.
Dodawanie allUsers jako przeglądającego miejsce na dane
Otwórz kartę Permissions:

Dodaj do zasobnika użytkownika allUsers z rolą Storage > Storage Object Viewer w ten sposób:

Kliknij SAVE.
5. Tworzenie zasobnika (gsutil)
Do tworzenia zasobników możesz też używać narzędzia wiersza poleceń gsutil w Cloud Shell.
W Cloud Shell ustaw zmienną dla unikalnej nazwy zasobnika. W Cloud Shell zmienna GOOGLE_CLOUD_PROJECT jest już ustawiona na Twój unikalny identyfikator projektu. Możesz dodać go do nazwy zasobnika.
Na przykład:
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
Utwórz standardową strefę wieloregionową w Europie:
gsutil mb -l EU gs://${BUCKET_PICTURES}
Sprawdź, czy jest włączony jednolity dostęp na poziomie zasobnika:
gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}
Ustaw zasobnik jako publiczny:
gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}
Jeśli przejdziesz do sekcji Cloud Storage w konsoli, powinien być tam publiczny uploaded-pictures:

Sprawdź, czy możesz przesyłać zdjęcia do zasobnika i czy przesłane zdjęcia są dostępne publicznie, jak opisano w poprzednim kroku.
6. Testowanie dostępu publicznego do zasobnika
Wracając do przeglądarki pamięci, zobaczysz na liście swój zasobnik z dostępem „Publiczny” (wraz z ostrzeżeniem, że każdy ma dostęp do zawartości tego zasobnika).

Zasobnik jest gotowy do odbierania zdjęć.
Jeśli klikniesz nazwę zasobnika, zobaczysz jego szczegóły.

Możesz tam kliknąć przycisk Upload files, aby sprawdzić, czy możesz dodać obraz do zasobnika. Pojawi się wyskakujące okienko z prośbą o wybranie pliku. Po wybraniu plik zostanie przesłany do zasobnika i ponownie zobaczysz public dostęp, który został automatycznie przypisany do tego nowego pliku.

Obok etykiety dostępu Public zobaczysz też małą ikonę linku. Po kliknięciu przeglądarka przejdzie do publicznego adresu URL tego obrazu, który będzie miał postać:
https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png
gdzie BUCKET_NAME to niepowtarzalna globalnie nazwa zasobnika, a następnie nazwa pliku obrazu.
Kliknięcie pola wyboru obok nazwy obrazu spowoduje włączenie przycisku DELETE, dzięki czemu możesz usunąć pierwszy obraz.
7. Przygotowywanie bazy danych
Informacje o zdjęciu podane przez interfejs Vision API zapiszesz w bazie danych Cloud Firestore, czyli szybkiej, w pełni zarządzanej, bezserwerowej, chmurowej bazie danych dokumentów NoSQL. Przygotuj bazę danych, przechodząc do sekcji Firestore w konsoli Cloud:

Dostępne są 2 opcje: Native mode lub Datastore mode. Używaj trybu natywnego, który oferuje dodatkowe funkcje, takie jak obsługa offline i synchronizacja w czasie rzeczywistym.
Kliknij SELECT NATIVE MODE.

Wybierz region obejmujący wiele lokalizacji (w tym przypadku w Europie, ale najlepiej co najmniej ten sam region, w którym znajdują się Funkcje i zasobnik pamięci).
Kliknij przycisk CREATE DATABASE.
Po utworzeniu bazy danych zobaczysz te informacje:

Utwórz nową kolekcję, klikając przycisk + START COLLECTION.
Nazwij kolekcję pictures.

Nie musisz tworzyć dokumentu. Dodasz je programowo, gdy nowe zdjęcia będą przechowywane w Cloud Storage i analizowane przez interfejs Vision API.
Kliknij Save.
Firestore tworzy pierwszy domyślny dokument w nowo utworzonej kolekcji. Możesz go bezpiecznie usunąć, ponieważ nie zawiera żadnych przydatnych informacji:

Dokumenty, które zostaną utworzone programowo w naszej kolekcji, będą zawierać 4 pola:
- name (string): nazwa pliku przesłanego zdjęcia, która jest też kluczem dokumentu.
- labels (tablica ciągów znaków): etykiety rozpoznanych elementów przez Vision API.
- color (string): szesnastkowy kod koloru dominującego (np. #ab12ef)
- created (data): sygnatura czasowa wskazująca, kiedy metadane tego obrazu zostały zapisane.
- thumbnail (wartość logiczna): pole opcjonalne, które będzie obecne i będzie miało wartość „true”, jeśli dla tego zdjęcia została wygenerowana miniatura.
Będziemy wyszukiwać w Firestore zdjęcia, dla których dostępne są miniatury, i sortować je według daty utworzenia, więc musimy utworzyć indeks wyszukiwania.
Możesz utworzyć indeks za pomocą tego polecenia w Cloud Shell:
gcloud firestore indexes composite create \
--collection-group=pictures \
--field-config field-path=thumbnail,order=descending \
--field-config field-path=created,order=descending
Możesz też to zrobić w konsoli Google Cloud, klikając Indexes w kolumnie nawigacyjnej po lewej stronie, a następnie tworząc indeks złożony w sposób pokazany poniżej:

Kliknij Create. Tworzenie indeksu może potrwać kilka minut.
8. Klonowanie kodu
Sklonuj kod, jeśli nie masz go jeszcze z poprzedniego modułu:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
Następnie możesz przejść do katalogu zawierającego usługę, aby rozpocząć tworzenie modułu:
cd serverless-photosharing-workshop/services/image-analysis/java
Usługa będzie miała ten układ pliku:

9. Poznaj kod usługi
Zacznij od sprawdzenia, jak włączane są biblioteki klienta Java w pom.xmlza pomocą BOM:
Najpierw zmień plik pom.xml, który zawiera listę zależności naszej funkcji w Javie. Zaktualizuj kod, aby dodać zależność Maven interfejsu Cloud Vision API:
<?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>
Funkcja jest zaimplementowana w klasie EventController. Za każdym razem, gdy do zasobnika przesyłany jest nowy obraz, usługa otrzymuje powiadomienie o konieczności przetworzenia:
@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 {
...
}
Kod przejdzie do weryfikacji nagłówków 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);
}
Możesz teraz utworzyć żądanie, a kod przygotuje je do wysłania do 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);
Prosimy o 3 kluczowe funkcje interfejsu Vision API:
- Wykrywanie etykiet: aby zrozumieć, co znajduje się na zdjęciach.
- Właściwości obrazu: aby podać interesujące atrybuty obrazu (interesuje nas dominujący kolor obrazu).
- Bezpieczne wyszukiwanie: aby sprawdzić, czy obraz jest bezpieczny do wyświetlenia (nie powinien zawierać treści dla dorosłych, medycznych, o charakterze seksualnym ani treści przedstawiających przemoc).
W tym momencie możemy wywołać interfejs Vision API:
...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...
Oto przykład odpowiedzi interfejsu Vision API:
{
"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
}
Jeśli nie zostanie zwrócony żaden błąd, możemy przejść dalej. Dlatego mamy ten blok 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);
}
Pobierzemy etykiety rzeczy, kategorii lub motywów rozpoznanych na zdjęciu:
List<String> labels = response.getLabelAnnotationsList().stream()
.map(annotation -> annotation.getDescription())
.collect(Collectors.toList());
logger.info("Annotations found:");
for (String label: labels) {
logger.info("- " + label);
}
Chcemy poznać dominujący kolor zdjęcia:
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);
}
Sprawdźmy, czy zdjęcie jest bezpieczne:
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);
}
Sprawdzamy, czy treści nie zawierają prawdopodobnie lub bardzo prawdopodobnie treści dla dorosłych, parodii, treści medycznych, przemocy lub treści o charakterze erotycznym.
Jeśli wynik bezpiecznego wyszukiwania jest prawidłowy, możemy zapisać metadane w 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. Tworzenie obrazów aplikacji za pomocą GraalVM (opcjonalnie)
W tym opcjonalnym kroku utworzysz JIT(JVM) based app image, a następnie AOT(Native) Java app image za pomocą GraalVM.
Aby uruchomić kompilację, musisz mieć zainstalowany i skonfigurowany odpowiedni pakiet JDK oraz narzędzie do tworzenia obrazów natywnych. Dostępnych jest kilka opcji.
To start pobierz GraalVM 22.2.x Community Edition i postępuj zgodnie z instrukcjami na stronie instalacji GraalVM.
Ten proces można znacznie uprościć za pomocą narzędzia SDKMAN!
Aby zainstalować odpowiednią dystrybucję JDK za pomocą SDKman, zacznij od użycia polecenia instalacji:
sdk install java 22.2.r17-grl
Poinstruuj SDKman, aby używał tej wersji w przypadku kompilacji JIT i AOT:
sdk use java 22.2.0.r17-grl
Zainstaluj native-image utility dla GraalVM:
gu install native-image
W Cloudshell możesz zainstalować GraalVM i narzędzie native-image za pomocą tych prostych poleceń:
# 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 ../..
Najpierw ustaw zmienne środowiskowe projektu GCP:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
Następnie możesz przejść do katalogu zawierającego usługę, aby rozpocząć tworzenie modułu:
cd serverless-photosharing-workshop/services/image-analysis/java
Utwórz obraz aplikacji JIT(JVM):
./mvnw package -Pjvm
Sprawdź log kompilacji w terminalu:
... [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] ------------------------------------------------------------------------
Utwórz obraz AOT(natywny):
./mvnw package -Pnative -DskipTests
Obserwuj log kompilacji w terminalu, w tym logi kompilacji obrazu natywnego:
Pamiętaj, że kompilacja trwa dość długo, w zależności od urządzenia, na którym testujesz.
...
[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. Tworzenie i publikowanie obrazów kontenerów
Utwórzmy obraz kontenera w 2 wersjach: jednej jako JIT(JVM) image, a drugiej jako AOT(Native) Java image.
Najpierw ustaw zmienne środowiskowe projektu GCP:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
Utwórz obraz JIT(JVM):
./mvnw package -Pjvm-image
Sprawdź log kompilacji w terminalu:
[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] ------------------------------------------------------------------------
Utwórz obraz AOT(natywny):
./mvnw package -Pnative-image
Obserwuj log kompilacji w terminalu, w tym logi kompilacji obrazu natywnego i kompresji obrazu za pomocą UPX.
Pamiętaj, że kompilacja trwa dość długo, w zależności od urządzenia, na którym testujesz.
... [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] ------------------------------------------------------------------------
Sprawdź, czy obrazy zostały utworzone:
docker images | grep image-analysis
Dodaj tagi do 2 obrazów i prześlij je do 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. Wdrożenie w Cloud Run
Czas wdrożyć usługę.
Usługę wdrożysz 2 razy: raz przy użyciu obrazu JIT(JVM), a drugi raz przy użyciu obrazu AOT(natywnego). Oba wdrożenia usługi będą równolegle przetwarzać ten sam obraz z zasobnika na potrzeby porównania.
Najpierw ustaw zmienne środowiskowe projektu 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
Wdróż obraz JIT(JVM) i sprawdź dziennik wdrażania w konsoli:
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
Wdróż obraz AOT(natywny) i sprawdź dziennik wdrażania w konsoli:
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. Konfigurowanie aktywatorów Eventarc
Eventarc oferuje standardowe rozwiązanie do zarządzania przepływem zmian stanu (nazywanych zdarzeniami) między odłączonymi mikroserwisami. Po aktywowaniu Eventarc przekierowuje te zdarzenia przez subskrypcje Pub/Sub do różnych miejsc docelowych (w tym dokumencie zobacz Miejsca docelowe zdarzeń), a jednocześnie zarządza przesyłaniem, zabezpieczeniami, autoryzacją, dostrzegalnością i obsługą błędów.
Możesz utworzyć aktywator Eventarc, aby usługa Cloud Run otrzymywała powiadomienia o określonym zdarzeniu lub zbiorze zdarzeń. Określając filtry wyzwalacza, możesz skonfigurować routing zdarzenia, w tym źródło zdarzenia i docelową usługę Cloud Run.
Najpierw ustaw zmienne środowiskowe projektu 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
Przyznaj rolę pubsub.publisher kontu usługi 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'
Skonfiguruj aktywatory Eventarc dla obrazów usług JVM(JIT) i AOT(natywnych), aby przetwarzać obraz:
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
Zauważ, że utworzone zostały 2 aktywatory:
gcloud eventarc triggers list --location=eu
14. Testowanie wersji usługi
Po pomyślnym wdrożeniu usług opublikujesz obraz w Cloud Storage, sprawdzisz, czy nasze usługi zostały wywołane, co zwraca interfejs Vision API i czy metadane są przechowywane w Firestore.
Wróć do Cloud Storage i kliknij zasobnik utworzony na początku modułu:

Na stronie z informacjami o zasobniku kliknij przycisk Upload files, aby przesłać zdjęcie.
Na przykład GeekHour.jpeg jest dostarczany z bazą kodu w folderze /services/image-analysis/java. Wybierz obraz i naciśnij Open button:

Możesz teraz sprawdzić wykonanie usługi, zaczynając od image-analysis-jvm, a następnie image-analysis-native.
W menu (☰) przejdź do usługi Cloud Run > image-analysis-jvm.
Kliknij Logi i przyjrzyj się wynikom:

Na liście dzienników widać, że usługa JIT(JVM) image-analysis-jvm została wywołana.
Logi wskazują początek i koniec wykonania usługi. Pomiędzy nimi widzimy logi umieszczone w funkcji za pomocą instrukcji logowania na poziomie INFO. Widzimy:
- szczegóły zdarzenia, które wywołało naszą funkcję,
- Nieprzetworzone wyniki wywołania interfejsu Vision API.
- etykiety znalezione na przesłanym przez nas zdjęciu;
- informacje o kolorach dominujących,
- czy obraz jest bezpieczny do wyświetlenia,
- Metadane obrazu zostały ostatecznie zapisane w Firestore.
Powtórz ten proces w przypadku usługi image-analysis-native.
W menu (☰) przejdź do usługi Cloud Run > image-analysis-native.
Kliknij Logi i przyjrzyj się wynikom:

Sprawdź teraz, czy metadane obrazu zostały zapisane w Fiorestore.
Ponownie w menu „hamburger” (☰) otwórz sekcję Firestore. W podsekcji Data (wyświetlanej domyślnie) powinna pojawić się kolekcja pictures z dodanym nowym dokumentem odpowiadającym właśnie przesłanemu zdjęciu:

15. Zwalnianie miejsca (opcjonalnie)
Jeśli nie zamierzasz kontynuować pracy z innymi ćwiczeniami z tej serii, możesz usunąć zasoby, aby zaoszczędzić pieniądze i być dobrym użytkownikiem chmury. Możesz zwolnić miejsce, czyszcząc poszczególne zasoby w ten sposób:
Usuń zasobnik:
gsutil rb gs://${BUCKET_PICTURES}
Usuń funkcję:
gcloud functions delete picture-uploaded --region europe-west1 -q
Usuń kolekcję Firestore, wybierając opcję Usuń kolekcję:

Możesz też usunąć cały projekt:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
16. Gratulacje!
Gratulacje! Udało Ci się wdrożyć pierwszą kluczową usługę projektu.
Omówione zagadnienia
- Cloud Storage
- Cloud Run
- Cloud Vision API
- Cloud Firestore
- Obrazy natywne Javy