1. סקירה כללית
בשיעור הראשון של Lab, תאחסנו תמונות בדלי. כך ייצור אירוע של יצירת קובץ שיטופל על ידי שירות שנפרס ב-Cloud Run. השירות יבצע קריאה ל-Vision API כדי לבצע ניתוח תמונות ולשמור את התוצאות במאגר נתונים.

מה תלמדו
- Cloud Storage
- Cloud Run
- Cloud Vision API
- Cloud Firestore
2. הגדרה ודרישות
הגדרת סביבה בקצב אישי
- נכנסים ל-מסוף Google Cloud ויוצרים פרויקט חדש או משתמשים בפרויקט קיים. אם עדיין אין לכם חשבון Gmail או Google Workspace, אתם צריכים ליצור חשבון.



- שם הפרויקט הוא השם המוצג למשתתפים בפרויקט. זו מחרוזת תווים שלא נמצאת בשימוש ב-Google APIs. אפשר לעדכן את המיקום הזה בכל שלב.
- מזהה הפרויקט חייב להיות ייחודי לכל הפרויקטים ב-Google Cloud, והוא קבוע (אי אפשר לשנות אותו אחרי שהוא מוגדר). מסוף Cloud יוצר באופן אוטומטי מחרוזת ייחודית, ובדרך כלל לא צריך לדעת מה היא. ברוב ה-Codelabs, תצטרכו להפנות למזהה הפרויקט (בדרך כלל הוא מזוהה כ-
PROJECT_ID). אם אתם לא אוהבים את המזהה שנוצר, אתם יכולים ליצור מזהה אקראי אחר. אפשר גם לנסות שם משתמש משלכם ולבדוק אם הוא זמין. אי אפשר לשנות את ההגדרה הזו אחרי השלב הזה, והיא תישאר כזו למשך הפרויקט. - לידיעתכם, יש ערך שלישי, מספר פרויקט, שחלק מממשקי ה-API משתמשים בו. מידע נוסף על שלושת הערכים האלה מופיע במאמרי העזרה.
- בשלב הבא, תצטרכו להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבי Cloud או בממשקי API של Cloud. העלות של התרגול הזה לא אמורה להיות גבוהה, ואולי לא תהיה עלות בכלל. כדי להשבית את המשאבים ולא לחייב אתכם אחרי שתסיימו את המדריך הזה, תוכלו למחוק את המשאבים שיצרתם או למחוק את כל הפרויקט. משתמשים חדשים ב-Google Cloud זכאים לתוכנית תקופת ניסיון בחינם בשווי 300$.
מפעילים את Cloud Shell
אפשר להפעיל את Google Cloud מרחוק מהמחשב הנייד, אבל ב-Codelab הזה נשתמש ב-Google Cloud Shell, סביבת שורת פקודה שפועלת בענן.
ב-מסוף Google Cloud, לוחצים על סמל Cloud Shell בסרגל הכלים שבפינה הימנית העליונה:

הקצאת המשאבים והחיבור לסביבה יימשכו רק כמה רגעים. בסיום התהליך, אמור להופיע משהו כזה:

המכונה הווירטואלית הזו כוללת את כל הכלים שדרושים למפתחים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר מאוד את הביצועים והאימות ברשת. אפשר לבצע את כל העבודה ב-codelab הזה בדפדפן. לא צריך להתקין שום דבר.
3. הפעלת ממשקי ה-API
בשיעור ה-Lab הזה תשתמשו ב-Cloud Functions וב-Vision API, אבל קודם צריך להפעיל אותם במסוף Cloud או באמצעות gcloud.
כדי להפעיל את Vision API במסוף Cloud, מחפשים את Cloud Vision API בסרגל החיפוש:

יוצג הדף Cloud Vision API:

לוחצים על הלחצן ENABLE.
אפשר גם להפעיל אותו ב-Cloud Shell באמצעות כלי שורת הפקודה gcloud.
ב-Cloud Shell, מריצים את הפקודה הבאה:
gcloud services enable vision.googleapis.com
הפעולה אמורה להסתיים בהצלחה:
Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.
צריך להפעיל גם את Cloud Run ו-Cloud Build:
gcloud services enable cloudbuild.googleapis.com \ run.googleapis.com
4. יצירת הקטגוריה (מסוף)
יוצרים קטגוריית אחסון לתמונות. אפשר לעשות את זה דרך קונסולת Google Cloud Platform ( console.cloud.google.com) או באמצעות כלי שורת הפקודה gsutil מ-Cloud Shell או מסביבת הפיתוח המקומית.
ניווט לאחסון
בתפריט ההמבורגר (☰), עוברים לדף Storage.

נותנים שם לקטגוריה
לוחצים על הלחצן CREATE BUCKET.

לוחצים על CONTINUE.
בחירת מיקום

יוצרים קטגוריה במספר אזורים באזור שבחרתם (בדוגמה הזו Europe).
לוחצים על CONTINUE.
בחירת סוג האחסון (storage class) שמוגדר כברירת מחדל

בוחרים את Standard סוג האחסון (storage class) של הנתונים.
לוחצים על CONTINUE.
הגדרת בקרת גישה

אתם עובדים עם תמונות שנגישות לכולם, ולכן אתם רוצים שכל התמונות שמאוחסנות בדלי הזה יהיו עם אותה בקרת גישה אחידה.
בוחרים את האפשרות Uniform בקרת גישה.
לוחצים על CONTINUE.
הגדרת הגנה/הצפנה

משאירים את ברירת המחדל (Google-managed key)), כי לא תשתמשו במפתחות הצפנה משלכם.
לוחצים על CREATE כדי לסיים את יצירת הקטגוריה.
הוספת allUsers כצופים בנפח האחסון
עוברים לכרטיסייה Permissions:

מוסיפים את חבר הקבוצה allUsers לקטגוריה, עם התפקיד Storage > Storage Object Viewer, באופן הבא:

לוחצים על SAVE.
5. יצירת הקטגוריה (gsutil)
אפשר גם להשתמש בכלי שורת הפקודה gsutil ב-Cloud Shell כדי ליצור מאגרי מידע.
ב-Cloud Shell, מגדירים משתנה לשם הייחודי של הקטגוריה. ב-Cloud Shell, הערך GOOGLE_CLOUD_PROJECT כבר מוגדר למזהה הפרויקט הייחודי שלכם. אפשר להוסיף את זה לשם הקטגוריה.
לדוגמה:
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
יצירת אזור רגיל במספר אזורים באירופה:
gsutil mb -l EU gs://${BUCKET_PICTURES}
מוודאים שיש גישה אחידה ברמת הקטגוריה:
gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}
הופכים את הקטגוריה לקטגוריה גלויה לכולם:
gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}
אם נכנסים לקטע Cloud Storage במסוף, אמורה להיות קטגוריה uploaded-pictures שגלוי לכולם:

בודקים שאפשר להעלות תמונות לקטגוריה ושהתמונות שהועלו גלויות לכולם, כמו שמוסבר בשלב הקודם.
6. בדיקת הגישה הציבורית לקטגוריה
אם חוזרים לדפדפן האחסון, רואים את הדלי ברשימה עם גישה 'ציבורית' (כולל סימן אזהרה שמזכיר לכם שלכל אחד יש גישה לתוכן של הדלי הזה).

המאגר מוכן עכשיו לקבל תמונות.
אם לוחצים על שם הקטגוריה, מוצגים פרטי הקטגוריה.

אפשר לנסות ללחוץ על הלחצן Upload files כדי לבדוק שאפשר להוסיף תמונה למאגר. יופיע חלון קופץ לבחירת קובץ. אחרי הבחירה, הקובץ יועלה למאגר, ותוכלו לראות שוב את public הגישה שהוקצתה אוטומטית לקובץ החדש הזה.

לצד תווית הגישה Public, יופיע גם סמל קטן של קישור. כשלוחצים על התמונה, הדפדפן עובר לכתובת ה-URL הציבורית של התמונה, שתהיה מהצורה:
https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png
כאשר BUCKET_NAME הוא השם הייחודי הגלובלי שבחרתם לקטגוריה, ואחריו שם הקובץ של התמונה.
כדי למחוק את התמונה הראשונה, לוחצים על תיבת הסימון לצד שם התמונה ואז על הלחצן DELETE.
7. הכנת מסד הנתונים
תאחסנו מידע על התמונה שמתקבל מ-Vision API במסד הנתונים Cloud Firestore, שהוא מסד נתונים מהיר, מנוהל, בלי שרת (serverless) ולא יחסי (NoSQL) של מסמכים מבוסס-ענן. כדי להכין את מסד הנתונים, עוברים לקטע Firestore ב-Cloud Console:

יש שתי אפשרויות: Native mode או Datastore mode. משתמשים במצב המקורי, שמציע תכונות נוספות כמו תמיכה באופליין וסנכרון בזמן אמת.
לוחצים על SELECT NATIVE MODE.

בוחרים אזור גיאוגרפי מרובה (כאן באירופה, אבל עדיף לפחות באותו אזור שבו נמצאים הפונקציה ודלי האחסון).
לוחצים על הלחצן CREATE DATABASE.
אחרי שיוצרים את מסד הנתונים, אמור להופיע המסך הבא:

כדי ליצור אוסף חדש, לוחצים על הלחצן + START COLLECTION.
איסוף שמות pictures.

לא צריך ליצור מסמך. התמונות יתווספו באופן אוטומטי כשהן יישמרו ב-Cloud Storage וינותחו על ידי Vision API.
לוחצים על Save.
Firestore יוצר מסמך ברירת מחדל ראשון באוסף החדש שנוצר. אפשר למחוק את המסמך הזה בבטחה כי הוא לא מכיל מידע שימושי:

המסמכים שייווצרו באופן אוטומטי באוסף שלנו יכללו 4 שדות:
- name (מחרוזת): שם הקובץ של התמונה שהועלתה, שהוא גם המפתח של המסמך
- labels (מערך של מחרוזות): התוויות של פריטים שזוהו על ידי Vision API
- color (מחרוזת): קוד הצבע ההקסדצימלי של הצבע הדומיננטי (למשל #ab12ef)
- created (תאריך): חותמת הזמן של מועד שמירת המטא-נתונים של התמונה
- thumbnail (בוליאני): שדה אופציונלי שיופיע ויקבל את הערך true אם נוצרה תמונה ממוזערת לתמונה הזו
כדי שנוכל לחפש ב-Firestore תמונות שיש להן תמונות ממוזערות ולמיין אותן לפי תאריך היצירה, נצטרך ליצור אינדקס חיפוש.
אפשר ליצור את האינדקס באמצעות הפקודה הבאה ב-Cloud Shell:
gcloud firestore indexes composite create \
--collection-group=pictures \
--field-config field-path=thumbnail,order=descending \
--field-config field-path=created,order=descending
אפשר גם לעשות את זה מ-Cloud Console. לשם כך, לוחצים על Indexes בעמודת הניווט בצד ימין, ואז יוצרים אינדקס מורכב כמו שמוצג בהמשך:

לוחצים על Create. יצירת האינדקס יכולה להימשך כמה דקות.
8. שיבוט הקוד
משכפלים את הקוד, אם עדיין לא עשיתם את זה בסדנת הקוד הקודמת:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
אחר כך אפשר לעבור לספרייה שמכילה את השירות כדי להתחיל לבנות את ה-Lab:
cd serverless-photosharing-workshop/services/image-analysis/java
הפריסה של הקובץ עבור השירות תהיה כזו:

9. בדיקת קוד השירות
מתחילים בבדיקה איך ספריות הלקוח של Java מופעלות ב-pom.xml באמצעות BOM:
קודם כול, עורכים את קובץ pom.xml שבו מפורטים יחסי התלות של פונקציית ה-Java. מעדכנים את הקוד כדי להוסיף את יחסי התלות במאגר Maven של 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>
הפונקציונליות מיושמת במחלקה EventController. בכל פעם שתמונה חדשה תועלה לקטגוריה, השירות יקבל הודעה לעיבוד:
@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 {
...
}
הקוד ימשיך לאמת את הכותרות 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);
}
עכשיו אפשר ליצור בקשה, והקוד יכין בקשה כזו לשליחה אל 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);
אנחנו מבקשים 3 יכולות מרכזיות של Vision API:
- זיהוי תוויות: כדי להבין מה יש בתמונות
- מאפייני התמונה: כדי לספק מאפיינים מעניינים של התמונה (אנחנו מתעניינים בצבע הדומיננטי של התמונה)
- חיפוש בטוח: כדי לדעת אם התמונה בטוחה להצגה (אסור שהיא תכיל תוכן למבוגרים בלבד, תוכן רפואי, תוכן נועז או תוכן אלים)
בשלב הזה, אפשר לשלוח קריאה ל-Vision API:
...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...
לעיון, הנה דוגמה לתשובה מ-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
}
אם לא מוחזרת שגיאה, אפשר להמשיך, ולכן יש לנו את בלוק ה-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);
}
אנחנו הולכים לקבל את התוויות של הדברים, הקטגוריות או הנושאים שזוהו בתמונה:
List<String> labels = response.getLabelAnnotationsList().stream()
.map(annotation -> annotation.getDescription())
.collect(Collectors.toList());
logger.info("Annotations found:");
for (String label: labels) {
logger.info("- " + label);
}
נשמח לדעת מהו הצבע הדומיננטי בתמונה:
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);
}
בוא נבדוק אם אפשר להציג את התמונה:
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);
}
אנחנו בודקים את המאפיינים של תוכן למבוגרים בלבד, תוכן שכולל זיוף, תוכן רפואי, אלימות או תוכן בעל אופי מיני כדי לראות אם הסבירות שהם יופיעו היא נמוכה או נמוכה מאוד.
אם התוצאה של החיפוש הבטוח תקינה, אפשר לאחסן את המטא-נתונים ב-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. יצירת תמונות של אפליקציות באמצעות GraalVM (אופציונלי)
בשלב האופציונלי הזה, תיצרו JIT(JVM) based app image ואז AOT(Native) Java app image באמצעות GraalVM.
כדי להריץ את ה-build, צריך לוודא שיש לכם JDK מתאים ושהתקנתם והגדרתם את כלי ה-build של native-image. יש כמה אפשרויות:
To start, מורידים את GraalVM 22.2.x Community Edition ופועלים לפי ההוראות בדף GraalVM installation.
אפשר לפשט מאוד את התהליך הזה בעזרת SDKMAN!
כדי להתקין את הפצת ה-JDK המתאימה באמצעות SDKman, מתחילים באמצעות פקודת ההתקנה:
sdk install java 22.2.r17-grl
מנחים את SDKman להשתמש בגרסה הזו, גם ב-JIT וגם ב-AOT:
sdk use java 22.2.0.r17-grl
מתקינים את native-image utility ל-GraalVM:
gu install native-image
ב-Cloudshell, לנוחיותכם, אתם יכולים להתקין את GraalVM ואת כלי native-image באמצעות הפקודות הפשוטות הבאות:
# 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 ../..
קודם מגדירים את משתני הסביבה של פרויקט GCP:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
אחר כך אפשר לעבור לספרייה שמכילה את השירות כדי להתחיל לבנות את ה-Lab:
cd serverless-photosharing-workshop/services/image-analysis/java
יוצרים את קובץ האימג' של אפליקציית JIT(JVM):
./mvnw package -Pjvm
בודקים את יומן ה-build בטרמינל:
... [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] ------------------------------------------------------------------------
יוצרים את קובץ האימג' AOT(Native):
./mvnw package -Pnative -DskipTests
מעיינים ביומן ה-build בטרמינל, כולל יומני ה-build של התמונה המקורית:
שימו לב שגרסת ה-build תיצור קובץ גדול יותר, בהתאם למחשב שבו אתם מבצעים את הבדיקה.
...
[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. פיתוח ופרסום של קובצי אימג' של קונטיינרים
ניצור קובץ אימג' של קונטיינר בשתי גרסאות שונות: אחת כ-JIT(JVM) image והשנייה כ-AOT(Native) Java image.
קודם מגדירים את משתני הסביבה של פרויקט GCP:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
יוצרים את תמונת ה-JIT(JVM):
./mvnw package -Pjvm-image
בודקים את יומן ה-build בטרמינל:
[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] ------------------------------------------------------------------------
יוצרים את קובץ האימג' AOT(Native):
./mvnw package -Pnative-image
בודקים את יומן הבנייה במסוף, כולל יומני הבנייה של התמונה המקורית ודחיסת התמונה באמצעות UPX.
שימו לב שגרסת ה-build תיצור קובץ גדול יותר, בהתאם למחשב שבו אתם בודקים
... [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] ------------------------------------------------------------------------
בודקים שהתמונות נוצרו:
docker images | grep image-analysis
מתייגים את שתי התמונות ומעבירים אותן ל-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. פריסה ב-Cloud Run
הגיע הזמן לפרוס את השירות.
תפרסו את השירות פעמיים, פעם אחת באמצעות תמונת JIT(JVM) ופעם שנייה באמצעות תמונת AOT(Native). שני פריסות השירות יעבדו במקביל את אותה תמונה מהמאגר, לצורך השוואה.
קודם מגדירים את משתני הסביבה של פרויקט 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
מבצעים פריסה של תמונת ה-JIT(JVM) וצופים ביומן הפריסה במסוף:
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
מבצעים פריסה של תמונת AOT(Native) ומתבוננים ביומן הפריסה במסוף:
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. הגדרת טריגרים של Eventarc
Eventarc מציע פתרון סטנדרטי לניהול הזרימה של שינויים במצב, שנקראים אירועים, בין מיקרו-שירותים מנותקים. כשאירועים כאלה מופעלים, Eventarc מעביר אותם דרך מינויים ל-Pub/Sub ליעדים שונים (במסמך הזה, ראו יעדי אירועים), תוך ניהול של המסירה, האבטחה, ההרשאה, יכולת הצפייה וטיפול בשגיאות בשבילכם.
אתם יכולים ליצור טריגר Eventarc כדי שהשירות שלכם ב-Cloud Run יקבל התראות על אירוע מסוים או על קבוצה של אירועים. באמצעות הגדרת מסננים לטריגר, אפשר להגדיר את הניתוב של האירוע, כולל מקור האירוע ושירות היעד של Cloud Run.
קודם מגדירים את משתני הסביבה של פרויקט 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
נותנים את ההרשאה pubsub.publisher לחשבון השירות של 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'
מגדירים טריגרים של Eventarc גם לתמונות שירות של JVM (JIT) וגם לתמונות שירות של AOT (Native) כדי לעבד את התמונה:
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
שימו לב שנוצרו שני טריגרים:
gcloud eventarc triggers list --location=eu
14. בדיקה של גרסאות שירות
אחרי שהפריסות של השירות יסתיימו בהצלחה, תעלו תמונה ל-Cloud Storage ותבדקו אם השירותים שלנו הופעלו, מה הוחזר מ-Vision API והאם המטא-נתונים נשמרו ב-Firestore.
חוזרים אל Cloud Storage ולוחצים על הדלי שיצרנו בתחילת שיעור ה-Lab:

בדף הפרטים של הקטגוריה, לוחצים על הלחצן Upload files כדי להעלות תמונה.
לדוגמה, תמונת GeekHour.jpeg מסופקת עם בסיס הקוד שלכם בתיקייה /services/image-analysis/java. בוחרים תמונה ולוחצים על Open button:

עכשיו אפשר לבדוק את ההפעלה של השירות, החל מ-image-analysis-jvm ואז מ-image-analysis-native.
בתפריט ההמבורגר (☰), עוברים לשירות Cloud Run > image-analysis-jvm.
לוחצים על Logs (יומנים) ומתבוננים בפלט:

ואכן, ברשימת היומנים אפשר לראות שהשירות JIT(JVM) image-analysis-jvm הופעל.
היומנים מציינים את ההתחלה והסיום של הפעלת השירות. ובאמצע, אפשר לראות את היומנים שהכנסנו לפונקציה עם הצהרות היומן ברמת INFO. הנתונים שמוצגים הם:
- פרטי האירוע שמפעיל את הפונקציה,
- התוצאות הגולמיות מקריאה ל-Vision API,
- התוויות שנמצאו בתמונה שהעלינו,
- מידע על הצבעים הדומיננטיים,
- אם התמונה בטוחה לצפייה,
- ובסופו של דבר המטא-נתונים האלה לגבי התמונה מאוחסנים ב-Firestore.
תחזרו על התהליך עבור השירות image-analysis-native.
בתפריט ההמבורגר (☰), עוברים לשירות Cloud Run > image-analysis-native.
לוחצים על Logs (יומנים) ומתבוננים בפלט:

עכשיו צריך לבדוק אם המטא-נתונים של התמונה נשמרו ב-Fiorestore.
שוב, בתפריט ההמבורגר (☰), עוברים לקטע Firestore. בקטע המשנה Data (שמוצג כברירת מחדל), אמורה להופיע אוסף pictures עם מסמך חדש שנוסף, שמתאים לתמונה שהעליתם:

15. ניקוי (אופציונלי)
אם אתם לא מתכוונים להמשיך עם שאר המעבדות בסדרה, אתם יכולים לנקות את המשאבים כדי לחסוך בעלויות ולשמור על סביבת ענן נקייה. כדי לנקות משאבים בנפרד, פועלים לפי השלבים הבאים.
מוחקים את הקטגוריה:
gsutil rb gs://${BUCKET_PICTURES}
מוחקים את הפונקציה:
gcloud functions delete picture-uploaded --region europe-west1 -q
כדי למחוק את אוסף Firestore, בוחרים באפשרות 'מחיקת האוסף' מתוך האוסף:

אפשר גם למחוק את כל הפרויקט:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
16. מעולה!
מעולה! הטמעת בהצלחה את שירות המפתח הראשון של הפרויקט.
מה נכלל
- Cloud Storage
- Cloud Run
- Cloud Vision API
- Cloud Firestore
- תמונות מקוריות ב-Java