תמונות יומיות: אחסון וניתוח תמונות באמצעות ספריות לקוח של Java מקורי של Google

1. סקירה כללית

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

427de3100de3a61e.png

מה תלמדו

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

2. הגדרה ודרישות

הגדרת סביבה בקצב עצמאי

  1. נכנסים למסוף Google Cloud ויוצרים פרויקט חדש או עושים שימוש חוזר בפרויקט קיים. אם אין לכם עדיין חשבון Gmail או חשבון Google Workspace, עליכם ליצור חשבון.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Project name הוא השם המוצג של המשתתפים בפרויקט. זו מחרוזת תווים שלא משמשת את Google APIs. אפשר לעדכן אותו בכל שלב.
  • Project ID חייב להיות ייחודי בכל הפרויקטים ב-Google Cloud ואי אפשר לשנות אותו (אי אפשר לשנות אותו אחרי שמגדירים אותו). מסוף Cloud יוצר מחרוזת ייחודית באופן אוטומטי; בדרך כלל לא מעניין אותך מה זה. ברוב ה-Codelabs תצטרכו להפנות אל מזהה הפרויקט (בדרך כלל הוא מזוהה כ-PROJECT_ID). אם המזהה שנוצר לא מוצא חן בעיניך, יש לך אפשרות ליצור מזהה אקראי אחר. לחלופין, אפשר לנסות תבנית משלך ולבדוק אם היא זמינה. לא ניתן לשנות אותו אחרי השלב הזה, והוא יישאר למשך הפרויקט.
  • לידיעתך, יש ערך שלישי – Project Number (מספר פרויקט), שחלק מממשקי ה-API משתמשים בו. מידע נוסף על כל שלושת הערכים האלה זמין במסמכי התיעוד.
  1. בשלב הבא צריך להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבים או בממשקי API של Cloud. מעבר ב-Codelab הזה לא אמור לעלות הרבה, אם בכלל. כדי להשבית את המשאבים ולא לצבור חיובים מעבר למדריך הזה, אתם יכולים למחוק את המשאבים שיצרתם או למחוק את הפרויקט כולו. משתמשים חדשים ב-Google Cloud זכאים להצטרף לתוכנית תקופת ניסיון בחינם בשווי 1,200 ש"ח.

הפעלת Cloud Shell

אומנם אפשר להפעיל את Google Cloud מרחוק מהמחשב הנייד, אבל ב-Codelab הזה משתמשים ב-Google Cloud Shell, סביבת שורת הפקודה שפועלת ב-Cloud.

במסוף Google Cloud, לוחצים על הסמל של Cloud Shell בסרגל הכלים שבפינה השמאלית העליונה:

55efc1aaa7a4d3ad.png

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

7ffe5cbb04455448.png

למכונה הווירטואלית הזו נטען כל כלי הפיתוח הדרושים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר משמעותית את ביצועי הרשת והאימות. כל העבודה ב-Codelab הזה יכולה להתבצע בתוך דפדפן. אתה לא צריך להתקין שום דבר.

3. הפעלת ממשקי API

בשיעור ה-Lab הזה תשתמשו ב-Cloud Functions וב-Vision API, אבל קודם צריך להפעיל אותם ב-Cloud Console או באמצעות gcloud.

כדי להפעיל את Vision API במסוף Cloud, מחפשים את Cloud Vision API בסרגל החיפוש:

cf48b1747ba6a6fb.png

בשלב הזה מגיעים לדף Cloud Vision API:

ba4af419e6086fbb.png

לוחצים על הלחצן 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.

1930e055d138150a.png

מתן שם לקטגוריה

לוחצים על הלחצן CREATE BUCKET.

34147939358517f8.png

לוחצים על CONTINUE.

בחירת מיקום

197817f20be07678.png

יוצרים קטגוריה מרובת אזורים באזור הרצוי (כאן Europe).

לוחצים על CONTINUE.

בחירת סוג האחסון (storage class) שמוגדר כברירת מחדל

53cd91441c8caf0e.png

צריך לבחור את סוג האחסון (storage class) Standard לנתונים.

לוחצים על CONTINUE.

הגדרה של בקרת גישה

8c2b3b459d934a51.png

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

צריך לבחור באפשרות Uniform של בקרת הגישה.

לוחצים על CONTINUE.

הגדרת הגנה/הצפנה

d931c24c3e705a68.png

אני רוצה להשאיר את ברירת המחדל (Google-managed key), כי לא ייעשה שימוש במפתחות ההצפנה שלכם).

לוחצים על CREATE כדי לסיים את יצירת הקטגוריה.

הוספת כל המשתמשים כמציג של נפח אחסון

מעבר לכרטיסייה Permissions:

d0ecfdcff730ea51.png

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

e9f25ec1ea0b6cc6.png

לוחצים על 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:

a98ed4ba17873e40.png

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

6. בדיקת הגישה הציבורית לקטגוריה

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

89e7a4d2c80a0319.png

הקטגוריה שלכם מוכנה עכשיו לקבל תמונות.

בלחיצה על שם הקטגוריה יוצגו פרטי הקטגוריה.

131387f12d3eb2d3.png

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

e87584471a6e9c6d.png

לצד תווית הגישה Public, יופיע גם סמל קישור קטן. כשתלחצו עליו, הדפדפן שלכם ינווט לכתובת ה-URL הציבורית של אותה תמונה, שתופיע בתבנית:

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

כאשר BUCKET_NAME הוא השם הייחודי הגלובלי שבחרתם לקטגוריה, ואז שם הקובץ של התמונה שלכם.

על ידי לחיצה על תיבת הסימון לצד שם התמונה, הלחצן DELETE יופעל, ותהיה לך אפשרות למחוק את התמונה הראשונה.

7. הכנת מסד הנתונים

תוכלו לאחסן מידע על התמונה שסופקה על ידי Vision API במסד הנתונים של Cloud Firestore – מסד נתונים מהיר, מנוהל וללא שרת (serverless), שתומך בענן. כדי להכין את מסד הנתונים, עוברים לקטע Firestore במסוף Cloud:

9e4708d2257de058.png

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

לוחצים על SELECT NATIVE MODE.

9449ace8cc84de43.png

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

לוחצים על הלחצן CREATE DATABASE.

לאחר שמסד הנתונים נוצר, אתם אמורים לראות:

56265949a124819e.png

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

שם האוסף pictures.

75806ee24c4e13a7.png

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

לוחצים על Save.

Firestore יוצר מסמך ברירת מחדל ראשון באוסף החדש שנוצר. ניתן למחוק את המסמך בבטחה כי הוא לא מכיל מידע שימושי:

5c2f1e17ea47f48f.png

המסמכים שייווצרו באופן פרוגרמטי באוסף שלנו יכילו 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, בעמודת הניווט שמימין, ולאחר מכן יצירת אינדקס מורכב כמו שמוצג כאן:

ecb8b95e3c791272.png

לוחצים על Create. יצירת האינדקס עשויה להימשך מספר דקות.

8. שכפול הקוד

משכפלים את הקוד, אם עדיין לא עשיתם זאת ב-Code Lab הקודם:

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

עכשיו אפשר לעבור לספרייה שמכילה את השירות כדי להתחיל לבנות את שיעור ה-Lab:

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

תהיה לכם פריסת קבצים הבאה עבור השירות:

f79613aff479d8ad.png

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 (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 ושהכלי ליצירת תמונות מקורי מותקן ומוגדר. יש כמה אפשרויות.

To start, מורידים את GraalVM 22.2.x Community Edition ופועלים לפי ההוראות בדף התקנה של GraalVM.

אפשר לפשט את התהליך הזה בעזרת SDKMAN!

כדי להתקין את הפצת ה-JDK המתאימה עם SDKman, יש להשתמש תחילה בפקודת ההתקנה:

sdk install java 22.2.r17-grl

צריך להנחות את SDKman להשתמש בגרסה הזו, גם לגרסאות build של JIT וגם של AOT:

sdk use java 22.2.0.r17-grl

מתקינים את native-image utility של GraalVM:

gu install native-image

ב-Cloudshell, לנוחותך, אפשר להתקין את GraalVM ואת כלי העזר לתמונה מותאמת באמצעות הפקודות הפשוטות הבאות:

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

יוצרים את קובץ האימג' (Native) ב-AOT:

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

יוצרים את קובץ האימג' (Native) ב-AOT:

./mvnw package -Pnative-image

בודקים את יומן ה-build בטרמינל, כולל יומני build מקורי של תמונות ודחיסת תמונות באמצעות 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 מציעה פתרון סטנדרטי לניהול זרימת השינויים במצב (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 לתמונות של שירות 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:

ff8a6567afc76235.png

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

לדוגמה, ב-Codebase שלכם יש תמונה של GeekHour.jpeg במסגרת /services/image-analysis/java. בוחרים תמונה ומקישים על Open button:

347b76e8b775f2f5.png

עכשיו אפשר לבדוק את הפעלת השירות, החל מ-image-analysis-jvm ולאחר מכן image-analysis-native.

מתוך "המבורגר" (\t) בתפריט, צריך לעבור לשירות Cloud Run > image-analysis-jvm.

לוחצים על Logs ומעיינים בפלט:

810a8684414ceafa.png

ואכן, ברשימת היומנים, אני רואה שהופעל שירות ה-JIT(JVM) image-analysis-jvm.

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

  • פרטי האירוע שהפעיל את הפונקציה שלנו,
  • התוצאות הגולמיות מהקריאה ל-Vision API
  • התוויות שנמצאו בתמונה שהעלינו,
  • את המידע על הצבעים הדומיננטיים
  • אם בטוח להציג את התמונה,
  • בסופו של דבר, המטא-נתונים לגבי התמונה אוחסנו ב-Firestore.

חוזרים על התהליך בשביל השירות image-analysis-native.

מתוך "המבורגר" (\t) בתפריט, צריך לעבור לשירות Cloud Run > image-analysis-native.

לוחצים על Logs ומעיינים בפלט:

b80308c7d0f55a3.png

חשוב לבדוק עכשיו אם המטא-נתונים של התמונה אוחסנו ב-Fiorestore.

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

933a20a9709cb006.png

15. הסרת המשאבים (אופציונלי)

אם אתם לא מתכוונים להמשיך עם שיעורי ה-Lab האחרים בסדרה, תוכלו לפנות משאבים כדי לחסוך בעלויות ולהיות אזרחי הענן באופן כללי. אפשר למחוק משאבים בנפרד באופן הבא.

מוחקים את הקטגוריה:

gsutil rb gs://${BUCKET_PICTURES}

מוחקים את הפונקציה:

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

מוחקים את האוסף של Firestore על ידי בחירה באפשרות 'מחיקת אוסף' מהאוסף:

410b551c3264f70a.png

לחלופין, אפשר למחוק את הפרויקט כולו:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. מעולה!

מעולה! הטמעת בהצלחה את השירות למפתחות הצפנה הראשון של הפרויקט!

אילו נושאים דיברנו?

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore
  • תמונות Java מקוריות

השלבים הבאים