1. סקירה כללית
בשיעור ה-Lab הזה תמשיכו את העבודה משיעור ה-Lab הקודם ותוסיפו שירות של תמונות ממוזערות. שירות התמונות הממוזערות הוא קונטיינר אינטרנטי שלוקח תמונות גדולות ויוצר מהן תמונות ממוזערות.
כשהתמונה מועלית ל-Cloud Storage, נשלחת התראה דרך Cloud Pub/Sub למאגר אינטרנט של Cloud Run, שמשנה את גודל התמונות ושומר אותן בחזרה בקטגוריה אחרת ב-Cloud Storage.

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



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

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

המכונה הווירטואלית הזו כוללת את כל הכלים שדרושים למפתחים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר מאוד את הביצועים והאימות ברשת. אפשר לבצע את כל העבודה ב-Lab הזה רק באמצעות דפדפן.
3. הפעלת ממשקי ה-API
בשיעור ה-Lab הזה תצטרכו להשתמש ב-Cloud Build כדי ליצור קובצי אימג' של קונטיינרים וב-Cloud Run כדי לפרוס את הקונטיינר.
מפעילים את שני ממשקי ה-API מ-Cloud Shell:
gcloud services enable cloudbuild.googleapis.com \ run.googleapis.com
הפעולה אמורה להסתיים בהצלחה:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. יצירת קטגוריה נוספת
תאחסנו תמונות ממוזערות של התמונות שהועלו בדלי אחר. נשתמש ב-gsutil כדי ליצור את הקטגוריה השנייה.
ב-Cloud Shell, מגדירים משתנה לשם הייחודי של הקטגוריה. ב-Cloud Shell, הערך GOOGLE_CLOUD_PROJECT כבר מוגדר למזהה הפרויקט הייחודי שלכם. אפשר להוסיף את זה לשם הקטגוריה. לאחר מכן, יוצרים קטגוריה ציבורית במספר אזורים באירופה עם גישה אחידה ברמה:
BUCKET_THUMBNAILS=thumbnails-$GOOGLE_CLOUD_PROJECT gsutil mb -l EU gs://$BUCKET_THUMBNAILS gsutil uniformbucketlevelaccess set on gs://$BUCKET_THUMBNAILS gsutil iam ch allUsers:objectViewer gs://$BUCKET_THUMBNAILS
בסופו של דבר, אמור להיות לכם מאגר חדש שגלוי לכולם:

5. שיבוט הקוד
משכפלים את הקוד ועוברים לספרייה שמכילה את השירות:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop cd serverless-photosharing-workshop/services/thumbnails/nodejs
הפריסה של הקובץ בשירות תהיה כזו:
services
|
├── thumbnails
|
├── nodejs
|
├── Dockerfile
├── index.js
├── package.json
בתיקייה thumbnails/nodejs יש 3 קבצים:
-
index.jsמכיל את קוד Node.js -
package.jsonמגדיר את יחסי התלות של הפרויקט בספריות -
Dockerfileמגדיר את קובץ האימג' של הקונטיינר
6. הסבר על הקוד
כדי לעיין בקוד, אפשר להשתמש בכלי המובנה לעריכת טקסט. לשם כך, לוחצים על הלחצן Open Editor בחלק העליון של חלון Cloud Shell:

אפשר גם לפתוח את העורך בחלון דפדפן ייעודי, כדי להשתמש בשטח מסך גדול יותר.
תלות
בקובץ package.json מוגדרים יחסי התלות בספריות הנדרשות:
{
"name": "thumbnail_service",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"bluebird": "^3.7.2",
"express": "^4.17.1",
"imagemagick": "^0.1.3",
"@google-cloud/firestore": "^4.9.9",
"@google-cloud/storage": "^5.8.3"
}
}
ספריית Cloud Storage משמשת לקריאה ולשמירה של קובצי תמונה ב-Cloud Storage. Firestore כדי לעדכן את המטא-נתונים של התמונה. Express היא מסגרת אינטרנט של JavaScript / Node. המודול body-parser משמש לניתוח קל של בקשות נכנסות. Bluebird משמש לטיפול בהבטחות, ו-Imagemagick היא ספרייה לעריכת תמונות.
קובץ Docker
Dockerfile מגדיר את קובץ האימג' של קונטיינר עבור האפליקציה:
FROM node:14-slim
# installing Imagemagick
RUN set -ex; \
apt-get -y update; \
apt-get -y install imagemagick; \
rm -rf /var/lib/apt/lists/*; \
mkdir /tmp/original; \
mkdir /tmp/thumbnail;
WORKDIR /picadaily/services/thumbnails
COPY package*.json ./
RUN npm install --production
COPY . .
CMD [ "npm", "start" ]
תמונת הבסיס היא Node 14 וספריית imagemagick משמשת לעריכת תמונות. חלק מהספריות הזמניות נוצרות כדי להכיל קבצים של תמונות מקוריות ותמונות ממוזערות. לאחר מכן, מודולי NPM שנדרשים לקוד שלנו מותקנים לפני הפעלת הקוד באמצעות npm start.
index.js
נבחן את הקוד בחלקים כדי להבין טוב יותר מה התוכנית הזו עושה.
const express = require('express');
const imageMagick = require('imagemagick');
const Promise = require("bluebird");
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const Firestore = require('@google-cloud/firestore');
const app = express();
app.use(express.json());
קודם כל אנחנו דורשים את התלות הדרושה, ויוצרים את אפליקציית האינטרנט Express שלנו, וגם מציינים שאנחנו רוצים להשתמש במנתח גוף ה-JSON, כי בקשות נכנסות הן למעשה רק מטען JSON שמועבר באמצעות בקשת POST לאפליקציה שלנו.
app.post('/', async (req, res) => {
try {
// ...
} catch (err) {
console.log(`Error: creating the thumbnail: ${err}`);
console.error(err);
res.status(500).send(err);
}
});
המערכת שלנו מקבלת את המטען הייעודי (payload) הנכנס בכתובת ה-URL הבסיסית / ואנחנו משתמשים בלוגיקה מסוימת לטיפול בשגיאות בקוד שלנו, כדי לקבל מידע טוב יותר על הסיבות האפשריות לכשלים בקוד. לשם כך אנחנו בודקים את היומנים שמוצגים בממשק של Stackdriver Logging במסוף האינטרנט של Google Cloud.
const pubSubMessage = req.body;
console.log(`PubSub message: ${JSON.stringify(pubSubMessage)}`);
const fileEvent = JSON.parse(Buffer.from(pubSubMessage.message.data, 'base64').toString().trim());
console.log(`Received thumbnail request for file ${fileEvent.name} from bucket ${fileEvent.bucket}`);
בפלטפורמת Cloud Run, הודעות Pub/Sub נשלחות באמצעות בקשות HTTP POST, כמטענים ייעודיים (payloads) של JSON מהצורה:
{
"message": {
"attributes": {
"bucketId": "uploaded-pictures",
"eventTime": "2020-02-27T09:22:43.255225Z",
"eventType": "OBJECT_FINALIZE",
"notificationConfig": "projects/_/buckets/uploaded-pictures/notificationConfigs/28",
"objectGeneration": "1582795363255481",
"objectId": "IMG_20200213_181159.jpg",
"payloadFormat": "JSON_API_V1"
},
"data": "ewogICJraW5kIjogInN0b3JhZ2Ujb2JqZWN...FQUU9Igp9Cg==",
"messageId": "1014308302773399",
"message_id": "1014308302773399",
"publishTime": "2020-02-27T09:22:43.973Z",
"publish_time": "2020-02-27T09:22:43.973Z"
},
"subscription": "projects/serverless-picadaily/subscriptions/gcs-events-subscription"
}
אבל מה שבאמת מעניין במסמך ה-JSON הזה הוא מה שמופיע במאפיין message.data, שהוא רק מחרוזת אבל מקודד את המטען הייעודי (payload) בפועל ב-Base 64. לכן הקוד שלמעלה מפענח את תוכן Base 64 של המאפיין הזה. המאפיין data, אחרי הפענוח, מכיל מסמך JSON נוסף שמייצג את פרטי האירוע ב-Cloud Storage, שכוללים, בין היתר, את שם הקובץ ואת שם ה-bucket.
{
"kind": "storage#object",
"id": "uploaded-pictures/IMG_20200213_181159.jpg/1582795363255481",
"selfLink": "https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg",
"name": "IMG_20200213_181159.jpg",
"bucket": "uploaded-pictures",
"generation": "1582795363255481",
"metageneration": "1",
"contentType": "image/jpeg",
"timeCreated": "2020-02-27T09:22:43.255Z",
"updated": "2020-02-27T09:22:43.255Z",
"storageClass": "STANDARD",
"timeStorageClassUpdated": "2020-02-27T09:22:43.255Z",
"size": "4944335",
"md5Hash": "QzBIoPJBV2EvqB1EVk1riw==",
"mediaLink": "https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg?generation=1582795363255481&alt=media",
"crc32c": "hQ3uHg==",
"etag": "CLmJhJu08ecCEAE="
}
אנחנו מתעניינים בשמות התמונה וה-bucket, כי הקוד שלנו יביא את התמונה הזו מה-bucket כדי ליצור ממנה תמונה ממוזערת:
const bucket = storage.bucket(fileEvent.bucket);
const thumbBucket = storage.bucket(process.env.BUCKET_THUMBNAILS);
const originalFile = path.resolve('/tmp/original', fileEvent.name);
const thumbFile = path.resolve('/tmp/thumbnail', fileEvent.name);
await bucket.file(fileEvent.name).download({
destination: originalFile
});
console.log(`Downloaded picture into ${originalFile}`);
אנחנו מאחזרים את השם של קטגוריית האחסון של הפלט ממשתנה סביבה.
יש לנו את קטגוריית המקור שבה יצירת הקובץ הפעילה את שירות Cloud Run, ואת קטגוריית היעד שבה נאחסן את התמונה שנוצרה. אנחנו משתמשים ב-API המובנה path כדי לטפל בקבצים מקומיים, כי ספריית imagemagick תיצור את התמונה הממוזערת באופן מקומי בספריית הזמני /tmp. אנחנו await כדי להוריד את קובץ התמונה שהועלה.
const resizeCrop = Promise.promisify(im.crop);
await resizeCrop({
srcPath: originalFile,
dstPath: thumbFile,
width: 400,
height: 400
});
console.log(`Created local thumbnail in ${thumbFile}`);
מודול imagemagick לא מאוד ידידותי ל-async / await, לכן אנחנו עוטפים אותו בהבטחה של JavaScript (שמסופקת על ידי מודול Bluebird). לאחר מכן, אנחנו קוראים לפונקציה האסינכרונית של שינוי הגודל או החיתוך שיצרנו עם הפרמטרים של קובצי המקור והיעד, וגם עם המידות של התמונה הממוזערת שאנחנו רוצים ליצור.
await thumbBucket.upload(thumbFile);
console.log(`Uploaded thumbnail to Cloud Storage bucket ${process.env.BUCKET_THUMBNAILS}`);
אחרי שהקובץ של התמונה הממוזערת מועלה ל-Cloud Storage, אנחנו גם מעדכנים את המטא-נתונים ב-Cloud Firestore כדי להוסיף דגל בוליאני שמציין שהתמונה הממוזערת של התמונה הזו אכן נוצרה:
const pictureStore = new Firestore().collection('pictures');
const doc = pictureStore.doc(fileEvent.name);
await doc.set({
thumbnail: true
}, {merge: true});
console.log(`Updated Firestore about thumbnail creation for ${fileEvent.name}`);
res.status(204).send(`${fileEvent.name} processed`);
אחרי שהבקשה שלנו מסתיימת, אנחנו משיבים לבקשת ה-HTTP POST שהקובץ עובד בצורה תקינה.
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Started thumbnail generator on port ${PORT}`);
});
בסוף קובץ המקור, יש לנו את ההוראות להפעלה של אפליקציית האינטרנט ב-Express ביציאת ברירת המחדל 8080.
7. בדיקה מקומית
בודקים את הקוד באופן מקומי כדי לוודא שהוא פועל לפני הפריסה לענן.
בתיקייה thumbnails/nodejs, מתקינים את יחסי התלות של npm ומפעילים את השרת:
npm install; npm start
אם הכול עבר בצורה חלקה, השרת אמור להתחיל לפעול ביציאה 8080:
Started thumbnail generator on port 8080
כדי לצאת, לוחצים על CTRL-C.
8. איך יוצרים ומפרסמים את קובץ אימג' של קונטיינר
Cloud Run מריץ קונטיינרים, אבל קודם צריך ליצור את קובץ האימג' של הקונטיינר (מוגדר ב-Dockerfile). אפשר להשתמש ב-Google Cloud Build כדי ליצור קובצי אימג' של קונטיינרים ולאחר מכן לארח אותם ב-Google Container Registry.
בתיקייה thumbnails/nodejs שבה נמצא Dockerfile, מריצים את הפקודה הבאה כדי ליצור את קובץ אימג' של קונטיינר:
gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service
אחרי דקה או שתיים, הבנייה אמורה להצליח:

בקטע 'היסטוריה' של Cloud Build אמורה להופיע גם הגרסה שנבנתה בהצלחה:

אם לוחצים על מזהה ה-build כדי לראות את תצוגת הפרטים, בכרטיסייה build artifacts (פריטי build) אמור להופיע קובץ האימג' של הקונטיינר שהועלה ל-Cloud Registry (GCR):

אם רוצים, אפשר לבדוק שוב שקובץ האימג' של הקונטיינר פועל באופן מקומי ב-Cloud Shell:
docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service
הפקודה אמורה להפעיל את השרת ביציאה 8080 במאגר:
Started thumbnail generator on port 8080
כדי לצאת, לוחצים על CTRL-C.
9. פריסה ב-Cloud Run
לפני שמבצעים פריסה ב-Cloud Run, צריך להגדיר את האזור ב-Cloud Run לאחד מהאזורים הנתמכים ואת הפלטפורמה ל-managed:
gcloud config set run/region europe-west1 gcloud config set run/platform managed
כדי לבדוק שההגדרה מוגדרת:
gcloud config list ... [run] platform = managed region = europe-west1
מריצים את הפקודה הבאה כדי לפרוס את קובץ האימג' של הקונטיינר ב-Cloud Run:
SERVICE_NAME=thumbnail-service
gcloud run deploy $SERVICE_NAME \
--image gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service \
--no-allow-unauthenticated \
--update-env-vars BUCKET_THUMBNAILS=$BUCKET_THUMBNAILS
שימו לב לדגל --no-allow-unauthenticated. כך שירות Cloud Run הופך לשירות פנימי שיופעל רק על ידי חשבונות שירות ספציפיים.
אם הפריסה תצליח, הפלט הבא יוצג:

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

10. אירועים ב-Cloud Storage ל-Cloud Run דרך Pub/Sub
השירות מוכן, אבל עדיין צריך ליצור אירועים ב-Cloud Storage בשביל שירות Cloud Run החדש. Cloud Storage יכול לשלוח אירועים של יצירת קבצים באמצעות Cloud Pub/Sub, אבל יש כמה שלבים שצריך לבצע כדי שזה יקרה.
יוצרים נושא Pub/Sub כפייפליין תקשורת:
TOPIC_NAME=cloudstorage-cloudrun-topic gcloud pubsub topics create $TOPIC_NAME
יצירת התראות Pub/Sub כשקבצים מאוחסנים בקטגוריה:
BUCKET_PICTURES=uploaded-pictures-$GOOGLE_CLOUD_PROJECT gsutil notification create -t $TOPIC_NAME -f json gs://$BUCKET_PICTURES
יוצרים חשבון שירות למינוי Pub/Sub שייווצר בהמשך:
SERVICE_ACCOUNT=$TOPIC_NAME-sa
gcloud iam service-accounts create $SERVICE_ACCOUNT \
--display-name "Cloud Run Pub/Sub Invoker"
נותנים לחשבון השירות הרשאה להפעיל שירות Cloud Run:
SERVICE_NAME=thumbnail-service gcloud run services add-iam-policy-binding $SERVICE_NAME \ --member=serviceAccount:$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com \ --role=roles/run.invoker
אם הפעלתם את חשבון השירות של Pub/Sub ב-8 באפריל 2021 או לפני כן, צריך להקצות את התפקיד iam.serviceAccountTokenCreator לחשבון השירות של Pub/Sub:
PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
--member=serviceAccount:service-$PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
--role=roles/iam.serviceAccountTokenCreator
יכול להיות שיחלפו כמה דקות עד שהשינויים ב-IAM יתעדכנו במערכת.
לבסוף, יוצרים מינוי ל-Pub/Sub עם חשבון השירות:
SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --format 'value(status.url)') gcloud pubsub subscriptions create $TOPIC_NAME-subscription --topic $TOPIC_NAME \ --push-endpoint=$SERVICE_URL \ --push-auth-service-account=$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com
אפשר לבדוק אם נוצר מינוי. נכנסים ל-Pub/Sub במסוף, בוחרים את הנושא gcs-events ובחלק התחתון אמור להופיע המינוי:

11. בדיקת השירות
כדי לבדוק שההגדרה פועלת, מעלים תמונה חדשה לקטגוריית uploaded-pictures ובודקים בקטגוריית thumbnails שתמונות חדשות בגודל שונה מופיעות כמו שציפיתם.
אפשר גם לבדוק את היומנים כדי לראות את הודעות הרישום ביומן שמופיעות בזמן שהשלבים השונים של שירות Cloud Run מתבצעים:

12. ניקוי (אופציונלי)
אם אתם לא מתכוונים להמשיך עם שאר המעבדות בסדרה, מומלץ לנקות את המשאבים כדי לחסוך בעלויות ולשמור על סביבת ענן נקייה. כדי לנקות משאבים בנפרד, פועלים לפי השלבים הבאים.
מוחקים את הקטגוריה:
gsutil rb gs://$BUCKET_THUMBNAILS
מוחקים את השירות:
gcloud run services delete $SERVICE_NAME -q
מחיקת נושא Pub/Sub:
gcloud pubsub topics delete $TOPIC_NAME
אפשר גם למחוק את כל הפרויקט:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
13. מעולה!
הכול מוכן:
- יצירת התראה ב-Cloud Storage ששולחת הודעות Pub/Sub בנושא מסוים, כשמעלים תמונה חדשה.
- הגדרתם את הקישורים והחשבונות הנדרשים ב-IAM (בניגוד ל-Cloud Functions, שבהם הכול אוטומטי, כאן ההגדרה מתבצעת באופן ידני).
- יצרנו מינוי כדי ששירות Cloud Run שלנו יקבל את ההודעות של Pub/Sub.
- בכל פעם שמעלים תמונה חדשה לקטגוריה, גודל התמונה משתנה בזכות שירות Cloud Run החדש.
מה נכלל
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub