1. סקירה כללית
בשיעור ה-Lab הראשון של כתיבת קוד, תאחסנו תמונות בקטגוריה. ייווצר אירוע של יצירת קובץ, שיטופל על ידי שירות שנפרס ב-Cloud Run. השירות יקרא ל-Vision API כדי לבצע ניתוח תמונות ולשמור את התוצאות במאגר נתונים.
מה תלמדו
- Cloud Storage
- Cloud Run
- Cloud Vision API
- Cloud Firestore
2. הגדרה ודרישות
הגדרת סביבה בקצב עצמאי
- נכנסים למסוף Google Cloud ויוצרים פרויקט חדש או עושים שימוש חוזר בפרויקט קיים. אם אין לכם עדיין חשבון Gmail או חשבון Google Workspace, עליכם ליצור חשבון.
- Project name הוא השם המוצג של המשתתפים בפרויקט. זו מחרוזת תווים שלא משמשת את Google APIs. תמיד אפשר לעדכן.
- Project ID הוא ייחודי בכל הפרויקטים ב-Google Cloud ואי אפשר לשנות אותו (אי אפשר לשנות אותו אחרי שמגדירים אותו). מסוף Cloud יוצר מחרוזת ייחודית באופן אוטומטי; בדרך כלל לא מעניין אותך מה זה. ברוב ה-codelabs תצטרכו להפנות למזהה הפרויקט שלכם (בדרך כלל מזוהה כ-
PROJECT_ID
). אם המזהה שנוצר לא מוצא חן בעיניכם, אתם יכולים ליצור מזהה אקראי אחר. לחלופין, אפשר לנסות שם משלך ולראות אם הוא זמין. לא ניתן לשנות אותו אחרי השלב הזה, והוא נשאר למשך הפרויקט. - לידיעתך, יש ערך שלישי, Project Number, שבו משתמשים בחלק מממשקי ה-API. מידע נוסף על כל שלושת הערכים האלה זמין במסמכי התיעוד.
- בשלב הבא צריך להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבים או בממשקי API של Cloud. מעבר ב-Codelab הזה לא יעלה הרבה כסף, אם בכלל. כדי להשבית משאבים ולא לצבור חיובים מעבר למדריך הזה, אתם יכולים למחוק את המשאבים שיצרתם או למחוק את הפרויקט. משתמשים חדשים ב-Google Cloud זכאים להשתתף בתוכנית תקופת ניסיון בחינם בשווי 1,200 ש"ח.
הפעלת Cloud Shell
אומנם אפשר להפעיל את Google Cloud מרחוק מהמחשב הנייד, אבל ב-Codelab הזה משתמשים ב-Google Cloud Shell, סביבת שורת הפקודה שפועלת ב-Cloud.
במסוף Google Cloud, לוחצים על הסמל של Cloud Shell בסרגל הכלים שבפינה השמאלית העליונה:
נדרשים רק כמה דקות כדי להקצות את הסביבה ולהתחבר אליה. בסיום התהליך, אתם אמורים לראות משהו כזה:
למכונה הווירטואלית הזו נטען כל כלי הפיתוח הדרושים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר משמעותית את ביצועי הרשת והאימות. כל העבודה ב-Codelab הזה יכולה להתבצע בתוך דפדפן. אתה לא צריך להתקין שום דבר.
3. הפעלת ממשקי API
בשיעור ה-Lab הזה תשתמשו ב-Cloud Functions וב-Vision API, אבל קודם צריך להפעיל אותם ב-Cloud Console או באמצעות gcloud
.
כדי להפעיל את Vision API במסוף Cloud, מחפשים את Cloud Vision API
בסרגל החיפוש:
בשלב הזה מגיעים לדף Cloud Vision API:
לוחצים על הלחצן ENABLE
.
לחלופין, אפשר גם להפעיל אותו באמצעות Cloud Shell באמצעות כלי שורת הפקודה של Google Cloud.
בתוך 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 או מסביבת הפיתוח המקומית.
ניווט לאחסון
מתוך "המבורגר" (\t) בתפריט, צריך לעבור לדף Storage
.
מתן שם לקטגוריה
לוחצים על הלחצן CREATE BUCKET
.
לוחצים על CONTINUE
.
בחירת מיקום
יוצרים קטגוריה מרובת אזורים באזור הרצוי (כאן Europe
).
לוחצים על CONTINUE
.
בחירת סוג האחסון (storage class) שמוגדר כברירת מחדל
צריך לבחור את סוג האחסון (storage class) Standard
לנתונים.
לוחצים על CONTINUE
.
הגדרה של בקרת גישה
מאחר שעובדים עם תמונות הנגישות לציבור, רצוי שלכל התמונות שלנו המאוחסנות בקטגוריה הזו תהיה אותה בקרת גישה אחידה.
צריך לבחור באפשרות Uniform
של בקרת הגישה.
לוחצים על CONTINUE
.
הגדרת הגנה/הצפנה
אני רוצה להשאיר את ברירת המחדל (Google-managed key)
, כי לא ייעשה שימוש במפתחות ההצפנה שלכם).
לוחצים על CREATE
כדי לסיים את יצירת הקטגוריה.
הוספת כל המשתמשים כמציג של נפח אחסון
מעבר לכרטיסייה Permissions
:
מוסיפים לקטגוריה חבר allUsers
עם התפקיד Storage > Storage Object Viewer
:
לוחצים על SAVE
.
5. יצירת הקטגוריה (gsutil)
אפשר גם להשתמש בכלי שורת הפקודה gsutil
ב-Cloud Shell כדי ליצור קטגוריות.
ב-Cloud Shell, מגדירים משתנה לשם הייחודי של הקטגוריה. השדה GOOGLE_CLOUD_PROJECT
כבר מוגדר ב-Cloud Shell למזהה הפרויקט הייחודי שלכם. אפשר להוסיף אותו לשם הקטגוריה.
לדוגמה:
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. בדיקת הגישה הציבורית לקטגוריה
בחזרה לדפדפן האחסון, הקטגוריה שלך תופיע ברשימה עם הכיתוב 'Public' (ציבורי) גישה (כולל סימן אזהרה שמזכיר שלכל אחד יש גישה לתוכן של הקטגוריה).
הקטגוריה שלכם מוכנה עכשיו לקבל תמונות.
בלחיצה על שם הקטגוריה יוצגו פרטי הקטגוריה.
שם תוכלו לנסות את הלחצן Upload files
, כדי לבדוק אם אפשר להוסיף תמונה לקטגוריה. חלון קופץ של הכלי לבחירת קבצים יבקש מכם לבחור קובץ. אחרי הבחירה, הקובץ יועלה לקטגוריה, ואז תוצג שוב הגישה מסוג public
ששויכו באופן אוטומטי לקובץ החדש.
לצד תווית הגישה Public
, יופיע גם סמל קישור קטן. כשתלחצו עליו, הדפדפן שלכם ינווט לכתובת ה-URL הציבורית של אותה תמונה, שתופיע בתבנית:
https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png
כאשר BUCKET_NAME
הוא השם הייחודי הגלובלי שבחרתם לקטגוריה, ואז שם הקובץ של התמונה שלכם.
על ידי לחיצה על תיבת הסימון לצד שם התמונה, הלחצן DELETE
יופעל, ותהיה לך אפשרות למחוק את התמונה הראשונה.
7. הכנת מסד הנתונים
תוכלו לאחסן מידע על התמונה שסופקה על ידי Vision API במסד הנתונים של Cloud Firestore – מסד נתונים מהיר, מנוהל וללא שרת (serverless), שתומך בענן. כדי להכין את מסד הנתונים, עוברים לקטע Firestore
במסוף Cloud:
מוצעות שתי אפשרויות: 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)
- נוצר (תאריך): חותמת הזמן של התקופה שבה אוחסנו המטא-נתונים של התמונה
- thumbnail (בוליאני): שדה אופציונלי שיוצג אם נוצרה תמונה ממוזערת לתמונה הזו
מכיוון שנחפש ב-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 על ידי לחיצה על Indexes
, בעמודת הניווט שמימין, ולאחר מכן יצירת אינדקס מורכב כמו שמוצג כאן:
לוחצים על Create
. יצירת האינדקס עשויה להימשך מספר דקות.
8. שכפול הקוד
משכפלים את הקוד, אם עדיין לא עשיתם זאת ב-Code Lab הקודם:
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 שלנו. ההתמקדות היא בשימוש בממשקי ה-API של Vision, Cloud Storage ו-Firestore.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0-M3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>services</groupId>
<artifactId>image-analysis</artifactId>
<version>0.0.1</version>
<name>image-analysis</name>
<description>Spring App for Image Analysis</description>
<properties>
<java.version>17</java.version>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<spring-cloud.version>2023.0.0-M2</spring-cloud.version>
<testcontainers.version>1.19.1</testcontainers.version>
</properties>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.24.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
—
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud.functions</groupId>
<artifactId>functions-framework-api</artifactId>
<version>1.1.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-firestore</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-vision</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-storage</artifactId>
</dependency>
הפונקציונליות מוטמעת במחלקה 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 (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) {
ApiFuture<WriteResult> writeResult =
eventService.storeImage(fileName, labels,
mainColor);
logger.info("Picture metadata saved in Firestore at " +
writeResult.get().getUpdateTime());
}
...
public ApiFuture<WriteResult> storeImage(String fileName,
List<String> labels,
String mainColor) {
FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
Firestore pictureStore = firestoreOptions.getService();
DocumentReference doc = pictureStore.collection("pictures").document(fileName);
Map<String, Object> data = new HashMap<>();
data.put("labels", labels);
data.put("color", mainColor);
data.put("created", new Date());
return doc.set(data, SetOptions.merge());
}
10. יצירת תמונות של אפליקציות באמצעות GraalVM
בשלב האופציונלי הזה, תיצרו JIT based app image
, ואז Native Java app image
, באמצעות GraalVM.
כדי להריץ את ה-build, צריך לוודא שמותקנים ב-JDK ושהכלי ליצירת תמונות מקורי מותקן ומוגדר. יש כמה אפשרויות.
To start
, מורידים את GraalVM 22.3.x Community Edition ופועלים לפי ההוראות בדף התקנת GraalVM.
אפשר לפשט את התהליך הזה בעזרת SDKMAN!
כדי להתקין את הפצת ה-JDK המתאימה עם SDKman
, יש להשתמש תחילה בפקודת ההתקנה:
sdk install java 17.0.8-graal
צריך להנחות את SDKman להשתמש בגרסה הזו, גם לגרסאות build של JIT וגם של AOT:
sdk use java 17.0.8-graal
ב-Cloudshell
, לנוחותך, אפשר להתקין את GraalVM ואת כלי העזר לתמונה מותאמת באמצעות הפקודות הפשוטות הבאות:
# download GraalVM wget https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_linux-x64_bin.tar.gz tar -xzf graalvm-jdk-17_linux-x64_bin.tar.gz ls -lart # configure Java 17 and GraalVM for Java 17 # note the name of the latest GraalVM version, as unpacked by the tar command echo Existing JVM: $JAVA_HOME cd graalvm-jdk-17.0.8+9.1 export JAVA_HOME=$PWD cd bin export PATH=$PWD:$PATH echo JAVA HOME: $JAVA_HOME echo PATH: $PATH cd ../.. # validate the version with java -version # observe Java(TM) SE Runtime Environment Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14) Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14, mixed mode, sharing)
קודם כול, מגדירים את משתני הסביבה של הפרויקט ב-GCP:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
עכשיו אפשר לעבור לספרייה שמכילה את השירות כדי להתחיל לבנות את שיעור ה-Lab:
cd serverless-photosharing-workshop/services/image-analysis/java
יוצרים את קובץ האימג' של אפליקציית JIT:
./mvnw package
בודקים את יומן ה-build בטרמינל:
... [INFO] Results: [INFO] [INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] --- maven-jar-plugin:3.3.0:jar (default-jar) @ image-analysis --- [INFO] Building jar: /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar [INFO] [INFO] --- spring-boot-maven-plugin:3.2.0-M3:repackage (repackage) @ image-analysis --- [INFO] Replacing main artifact /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar with repackaged archive, adding nested dependencies in BOOT-INF/. [INFO] The original artifact has been renamed to /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar.original [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 15.335 s [INFO] Finished at: 2023-10-10T19:33:25Z [INFO] ------------------------------------------------------------------------
יוצרים את קובץ האימג' המקורי(באמצעות AOT):
./mvnw native:compile -Pnative
בודקים את יומן ה-build בטרמינל, כולל יומני ה-build של קובצי האימג' המקוריים:
שימו לב שה-build נמשך קצת יותר זמן, בהתאם למכונה שבה בודקים.
... [2/7] Performing analysis... [*********] (124.5s @ 4.53GB) 29,732 (93.19%) of 31,905 classes reachable 60,161 (70.30%) of 85,577 fields reachable 261,973 (67.29%) of 389,319 methods reachable 2,940 classes, 2,297 fields, and 97,421 methods registered for reflection 81 classes, 90 fields, and 62 methods registered for JNI access 4 native libraries: dl, pthread, rt, z [3/7] Building universe... (11.7s @ 4.67GB) [4/7] Parsing methods... [***] (6.1s @ 5.91GB) [5/7] Inlining methods... [****] (4.5s @ 4.39GB) [6/7] Compiling methods... [******] (35.3s @ 4.60GB) [7/7] Creating image... (12.9s @ 4.61GB) 80.08MB (47.43%) for code area: 190,483 compilation units 73.81MB (43.72%) for image heap: 660,125 objects and 189 resources 14.95MB ( 8.86%) for other data 168.84MB in total ------------------------------------------------------------------------------------------------------------------------ Top 10 packages in code area: Top 10 object types in image heap: 2.66MB com.google.cloud.vision.v1p4beta1 18.51MB byte[] for code metadata 2.60MB com.google.cloud.vision.v1 9.27MB java.lang.Class 2.49MB com.google.protobuf 7.34MB byte[] for reflection metadata 2.40MB com.google.cloud.vision.v1p3beta1 6.35MB byte[] for java.lang.String 2.17MB com.google.storage.v2 5.72MB java.lang.String 2.12MB com.google.firestore.v1 4.46MB byte[] for embedded resources 1.64MB sun.security.ssl 4.30MB c.oracle.svm.core.reflect.SubstrateMethodAccessor 1.51MB i.g.xds.shaded.io.envoyproxy.envoy.config.core.v3 4.27MB byte[] for general heap data 1.47MB com.google.cloud.vision.v1p2beta1 2.50MB com.oracle.svm.core.hub.DynamicHubCompanion 1.34MB i.g.x.shaded.io.envoyproxy.envoy.config.route.v3 1.17MB java.lang.Object[] 58.34MB for 977 more packages 9.19MB for 4667 more object types ------------------------------------------------------------------------------------------------------------------------ 13.5s (5.7% of total time) in 75 GCs | Peak RSS: 9.44GB | CPU load: 6.13 ------------------------------------------------------------------------------------------------------------------------ Produced artifacts: /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis (executable) /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis.build_artifacts.txt (txt) ======================================================================================================================== Finished generating '/home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis' in 3m 57s. [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 04:28 min [INFO] Finished at: 2023-10-10T19:53:30Z [INFO] ------------------------------------------------------------------------
11. יצירה ופרסום של תמונות קונטיינרים
ניצור קובץ אימג' בקונטיינר בשתי גרסאות שונות: אחת בפורמט JIT image
והשנייה בפורמט Native Java image
.
קודם כול, מגדירים את משתני הסביבה של הפרויקט ב-GCP:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
יוצרים את קובץ האימג' של ה-JIT:
./mvnw spring-boot:build-image -Pji
בודקים את יומן ה-build בטרמינל:
[INFO] [creator] Timer: Saving docker.io/library/image-analysis-maven-jit:latest... started at 2023-10-10T20:00:31Z [INFO] [creator] *** Images (4c84122a1826): [INFO] [creator] docker.io/library/image-analysis-maven-jit:latest [INFO] [creator] Timer: Saving docker.io/library/image-analysis-maven-jit:latest... ran for 6.975913605s and ended at 2023-10-10T20:00:38Z [INFO] [creator] Timer: Exporter ran for 8.068588001s and ended at 2023-10-10T20:00:38Z [INFO] [creator] Timer: Cache started at 2023-10-10T20:00:38Z [INFO] [creator] Reusing cache layer 'paketo-buildpacks/syft:syft' [INFO] [creator] Adding cache layer 'buildpacksio/lifecycle:cache.sbom' [INFO] [creator] Timer: Cache ran for 200.449002ms and ended at 2023-10-10T20:00:38Z [INFO] [INFO] Successfully built image 'docker.io/library/image-analysis-maven-jit:latest' [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 43.887 s [INFO] Finished at: 2023-10-10T20:00:39Z [INFO] ------------------------------------------------------------------------
יוצרים את קובץ האימג' (Native) ב-AOT:
./mvnw spring-boot:build-image -Pnative
בודקים את יומן ה-build בטרמינל, כולל יומני ה-build של קובצי האימג' המקוריים.
הערה:
- שה-build נמשך קצת יותר זמן, בהתאם למכונה שבודקים
- ניתן לדחוס את התמונות עוד יותר באמצעות UPX, אבל יש להן השפעה שלילית קטנה על ביצועי ההפעלה, לכן ה-build הזה לא משתמש ב-UPX. תמיד מדובר בהבדלים קטנים
... [INFO] [creator] Saving docker.io/library/image-analysis-maven-native:latest... [INFO] [creator] *** Images (13167702674e): [INFO] [creator] docker.io/library/image-analysis-maven-native:latest [INFO] [creator] Adding cache layer 'paketo-buildpacks/bellsoft-liberica:native-image-svm' [INFO] [creator] Adding cache layer 'paketo-buildpacks/syft:syft' [INFO] [creator] Adding cache layer 'paketo-buildpacks/native-image:native-image' [INFO] [creator] Adding cache layer 'buildpacksio/lifecycle:cache.sbom' [INFO] [INFO] Successfully built image 'docker.io/library/image-analysis-maven-native:latest' [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 03:37 min [INFO] Finished at: 2023-10-10T20:05:16Z [INFO] ------------------------------------------------------------------------
בודקים שהתמונות נוצרו:
docker images | grep image-analysis
מתייגים את שתי התמונות ומעבירים אותן ל-GCR:
# JIT image docker tag image-analysis-maven-jit gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit docker push gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit # Native(AOT) image docker tag image-analysis-maven-native gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native docker push gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native
12. פריסה ב-Cloud Run
הגיע הזמן לפרוס את השירות.
את השירות תפרסו פעמיים, פעם אחת באמצעות תמונת JIT ופעם שנייה באמצעות קובץ אימג' של 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 ומעיינים ביומן הפריסה במסוף:
gcloud run deploy image-analysis-jit \ --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit \ --region europe-west1 \ --memory 2Gi --allow-unauthenticated ... Deploying container to Cloud Run service [image-analysis-jit] in project [...] region [europe-west1] ✓ Deploying... Done. ✓ Creating Revision... ✓ Routing traffic... ✓ Setting IAM Policy... Done. Service [image-analysis-jit] revision [image-analysis-jvm-00009-huc] has been deployed and is serving 100 percent of traffic. Service URL: https://image-analysis-jit-...-ew.a.run.app
פורסים את קובץ האימג' המקורי ומעיינים ביומן הפריסה במסוף:
gcloud run deploy image-analysis-native \ --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native \ --region europe-west1 \ --memory 2Gi --allow-unauthenticated ... Deploying container to Cloud Run service [image-analysis-native] in project [...] region [europe-west1] ✓ Deploying... Done. ✓ Creating Revision... ✓ Routing traffic... ✓ Setting IAM Policy... Done. Service [image-analysis-native] revision [image-analysis-native-00005-ben] has been deployed and is serving 100 percent of traffic. Service URL: https://image-analysis-native-...-ew.a.run.app
13. הגדרת טריגרים של Eventarc
חברת Eventarc מציעה פתרון סטנדרטי לניהול זרימת השינויים במצב (State) שנקרא 'אירועים', בין מיקרו-שירותים (microservices) מופרדים. כשהאפשרות מופעלת, Eventarc מנתב את האירועים האלה דרך מינויי Pub/Sub ליעדים שונים (במסמך הזה מידע נוסף על יעדי אירועים) ומנהלת בשבילכם את המסירה, האבטחה, ההרשאות, הניראות (observability) והטיפול בשגיאות.
אתם יכולים ליצור טריגר של 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 לתמונות JIT ו-Native של שירות:
gcloud eventarc triggers list --location=eu gcloud eventarc triggers create image-analysis-jit-trigger \ --destination-run-service=image-analysis-jit \ --destination-run-region=europe-west1 \ --location=eu \ --event-filters="type=google.cloud.storage.object.v1.finalized" \ --event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \ --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com gcloud eventarc triggers create image-analysis-native-trigger \ --destination-run-service=image-analysis-native \ --destination-run-region=europe-west1 \ --location=eu \ --event-filters="type=google.cloud.storage.object.v1.finalized" \ --event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \ --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com
חשוב לבדוק ששני הטריגרים נוצרו:
gcloud eventarc triggers list --location=eu
14. גרסאות של שירות בדיקה
אחרי שפריסות השירות יבוצעו בהצלחה, מפרסמים תמונה ב-Cloud Storage, בדקו אם השירותים שלנו הופעלו, מה מחזיר Vision API ואם המטא-נתונים מאוחסנים ב-Firestore.
חוזרים חזרה אל Cloud Storage
ולוחצים על הקטגוריה שיצרנו בתחילת שיעור ה-Lab:
בדף הפרטים של הקטגוריה, לוחצים על הלחצן Upload files
כדי להעלות תמונה.
לדוגמה, ב-Codebase שלכם יש תמונה של GeekHour.jpeg
במסגרת /services/image-analysis/java
. בוחרים תמונה ומקישים על Open button
:
עכשיו אפשר לבדוק את הפעלת השירות, החל מ-image-analysis-jit
ולאחר מכן image-analysis-native
.
מתוך "המבורגר" (\t) בתפריט, צריך לעבור לשירות Cloud Run > image-analysis-jit
.
לוחצים על Logs ומעיינים בפלט:
ואכן, ברשימת היומנים, אני רואה שהופעל שירות ה-JIT image-analysis-jit
.
היומנים מציינים את ההתחלה והסיום של הפעלת השירות. בין היתר, נוכל לראות את היומנים שהכנסנו לפונקציה שלנו עם הצהרות היומן ברמת INFO. אנחנו רואים:
- פרטי האירוע שהפעיל את הפונקציה שלנו,
- התוצאות הגולמיות מהקריאה ל-Vision API
- התוויות שנמצאו בתמונה שהעלינו,
- את המידע על הצבעים הדומיננטיים
- אם בטוח להציג את התמונה,
- בסופו של דבר, המטא-נתונים לגבי התמונה אוחסנו ב-Firestore.
חוזרים על התהליך בשביל השירות image-analysis-native
.
מתוך "המבורגר" (\t) בתפריט, צריך לעבור לשירות Cloud Run > image-analysis-native
.
לוחצים על Logs ומעיינים בפלט:
חשוב לבדוק עכשיו אם המטא-נתונים של התמונה אוחסנו ב-Fiorestore.
שוב מהמבורגר (\t) בתפריט, עוברים לקטע 'Firestore
'. בקטע המשנה Data
(מוצג כברירת מחדל), אמור להופיע האוסף pictures
עם מסמך חדש שנוסף לתמונה שהעלית:
15. הסרת המשאבים (אופציונלי)
אם אתם לא מתכוונים להמשיך עם שיעורי ה-Lab האחרים בסדרה, תוכלו לפנות משאבים כדי לחסוך בעלויות ולהיות אזרחי הענן באופן כללי. אפשר למחוק משאבים בנפרד באופן הבא.
מוחקים את הקטגוריה:
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 מקוריות