1. סקירה כללית
בשיעור ה-Lab הזה נדגים תכונות ויכולות שנועדו לייעל את תהליך העבודה של מהנדסי תוכנה שמפתחים אפליקציות Java בסביבה מבוססת-קונטיינרים. בדרך כלל, פיתוח קונטיינרים דורש מהמשתמש להבין את הפרטים של הקונטיינרים ואת תהליך build של הקונטיינרים. בנוסף, מפתחים בדרך כלל צריכים להפסיק את תהליך העבודה שלהם, לצאת מסביבת הפיתוח המשולבת כדי לבדוק את האפליקציות שלהם ולנפות מהן באגים בסביבות מרוחקות. בעזרת הכלים והטכנולוגיות שמוזכרים במדריך הזה, מפתחים יכולים לעבוד ביעילות עם אפליקציות מבוססות-קונטיינרים בלי לצאת מסביבת הפיתוח המשולבת (IDE).
מה תלמדו
בשיעור ה-Lab הזה תלמדו שיטות לפיתוח באמצעות קונטיינרים ב-GCP, כולל:
- פיתוח InnerLoop עם Cloud Workstations
- יצירת אפליקציית Java חדשה למתחילים
- הסבר על תהליך הפיתוח
- פיתוח שירות REST פשוט של CRUD
- ניפוי באגים באפליקציה באשכול GKE
- חיבור אפליקציה למסד נתונים של Cloud SQL

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



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

- ייפתח חלונית חדשה בחלק התחתון של החלון.
- לוחצים על הלחצן 'פתיחת הכלי לעריכה'.

- העורך ייפתח עם סייר בצד שמאל ועורך באזור המרכזי
- גם חלונית טרמינל צריכה להיות זמינה בתחתית המסך
- אם הטרמינל לא פתוח, משתמשים בשילוב המקשים Ctrl + ` כדי לפתוח חלון טרמינל חדש.
הגדרת gcloud
ב-Cloud Shell, מגדירים את מזהה הפרויקט ואת האזור שבו רוצים לפרוס את האפליקציה. שומרים אותם כמשתנים PROJECT_ID ו-REGION.
export REGION=us-central1
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
לשכפל את קוד המקור
קוד המקור של ה-Lab הזה נמצא ב-container-developer-workshop ב-GoogleCloudPlatform ב-GitHub. משכפלים אותו באמצעות הפקודה שלמטה ואז עוברים לספרייה.
git clone https://github.com/GoogleCloudPlatform/container-developer-workshop.git
cd container-developer-workshop/labs/spring-boot
הקצאת התשתית שמשמשת בשיעור ה-Lab הזה
בשיעור ה-Lab הזה תפרסו קוד ב-GKE ותגשו לנתונים שמאוחסנים במסד נתונים של CloudSQL. סקריפט ההגדרה שמופיע בהמשך מכין את התשתית הזו בשבילכם. תהליך ההקצאה יימשך יותר מ-25 דקות. צריך להמתין עד שהסקריפט יסיים את הפעולה לפני שעוברים לקטע הבא.
./setup_with_cw.sh &
Cloud Workstations Cluster
פותחים את Cloud Workstations ב-Cloud Console. מחכים שהסטטוס של האשכול יהיה READY.
יצירת הגדרה של תחנות עבודה
אם הסשן של Cloud Shell התנתק, לוחצים על 'התחברות מחדש' ואז מריצים את הפקודה gcloud cli כדי להגדיר את מזהה הפרויקט. לפני שמריצים את הפקודה, מחליפים את מזהה הפרויקט לדוגמה שבהמשך במזהה הפרויקט שלכם ב-Qwiklabs.
gcloud config set project qwiklabs-gcp-project-id
מריצים את הסקריפט הבא במסוף כדי ליצור הגדרה של Cloud Workstations.
cd ~/container-developer-workshop/labs/spring-boot
./workstation_config_setup.sh
בודקים את התוצאות בקטע 'הגדרות'. יידרשו 2 דקות למעבר לסטטוס 'מוכן'.

פותחים את Cloud Workstations במסוף ויוצרים מכונה חדשה.

משנים את השם ל-my-workstation ובוחרים הגדרה קיימת: codeoss-java.

בודקים את התוצאות בקטע 'תחנות עבודה'.

הפעלת תחנת עבודה
מפעילים את תחנת העבודה.

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

לוחצים על 'האתר לא פועל?'.

לוחצים על 'אפשר להשתמש בקובצי Cookie'.

אחרי שהתחנה תופעל, יופיע Code OSS IDE. לוחצים על 'סימון כהשלמה' בדף 'תחילת העבודה' ב-IDE של תחנת העבודה.

3. יצירת אפליקציית Java חדשה למתחילים
בקטע הזה ניצור אפליקציית Java Spring Boot חדשה מאפס, באמצעות אפליקציית דוגמה שמופיעה באתר spring.io. פותחים חלון Terminal חדש.

שכפול האפליקציה לדוגמה
- יצירת אפליקציה למתחילים
curl https://start.spring.io/starter.zip -d dependencies=web -d type=maven-project -d javaVersion=17 -d packageName=com.example.springboot -o sample-app.zip
אם ההודעה הזו מופיעה, לוחצים על הלחצן 'אישור' כדי שתוכלו להעתיק ולהדביק את הקובץ בתחנת העבודה.

- ביטול הדחיסה של האפליקציה
unzip sample-app.zip -d sample-app
- פותחים את התיקייה sample-app.
cd sample-app && code-oss-cloud-workstations -r --folder-uri="$PWD"
הוספה של spring-boot-devtools ו-Jib
כדי להפעיל את Spring Boot DevTools, מוצאים את הקובץ pom.xml בסייר שבכלי העריכה ופותחים אותו. אחר כך מדביקים את הקוד הבא אחרי שורת התיאור <description>Demo project for Spring Boot</description>
- הוספת spring-boot-devtools ב-pom.xml
פותחים את הקובץ pom.xml בתיקיית הבסיס של הפרויקט. מוסיפים את ההגדרה הבאה אחרי הרשומה Description.
pom.xml
<!-- Spring profiles-->
<profiles>
<profile>
<id>sync</id>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
- הפעלת jib-maven-plugin בקובץ pom.xml
Jib הוא כלי בקוד פתוח של Google ליצירת קונטיינרים של Java, שמאפשר למפתחי Java ליצור קונטיינרים באמצעות כלי Java שהם מכירים. Jib הוא כלי מהיר ופשוט ליצירת קובצי אימג' של קונטיינרים, שמטפל בכל השלבים של אריזת האפליקציה בקובץ אימג' של קונטיינר. לא צריך לכתוב Dockerfile או להתקין את Docker, והוא משולב ישירות ב-Maven וב-Gradle.
גוללים למטה בקובץ pom.xml ומעדכנים את הקטע Build כדי לכלול את הפלאגין Jib. בסיום התהליך, הקטע 'יצירה' אמור להיראות כך.
pom.xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Jib Plugin-->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<!-- Maven Resources Plugin-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
</plugin>
</plugins>
</build>
יצירת קובצי מניפסט
Skaffold מספק כלים משולבים כדי לפשט את פיתוח הקונטיינרים. בשלב הזה תאתחלו את Skaffold, שיצור באופן אוטומטי קובצי YAML בסיסיים של Kubernetes. התהליך מנסה לזהות ספריות עם הגדרות של קובצי אימג' של קונטיינרים, כמו Dockerfile, ואז יוצר מניפסט של פריסה ושירות לכל אחת מהן.
כדי להתחיל בתהליך, מריצים את הפקודה הבאה במסוף.

- מריצים את הפקודה הבאה במסוף
skaffold init --generate-manifests
- כשמופיעה בקשה:
- משתמשים בחצים כדי להזיז את הסמן אל
Jib Maven Plugin - מקישים על מקש הרווח כדי לבחור את האפשרות.
- לוחצים על Enter כדי להמשיך
- מזינים 8080 בשדה של היציאה.
- מזינים y כדי לשמור את ההגדרה
שני קבצים נוספו לסביבת העבודה skaffold.yaml ו-deployment.yaml
פלט של Skaffold:

עדכון שם האפליקציה
ערכי ברירת המחדל שכלולים בתצורה לא תואמים כרגע לשם האפליקציה. מעדכנים את הקבצים כך שיפנו לשם האפליקציה ולא לערכי ברירת המחדל.
- שינוי רשומות בהגדרות של Skaffold
- פתיחה של
skaffold.yaml - בוחרים את שם התמונה שמוגדר כרגע כ-
pom-xml-image - לוחצים לחיצה ימנית ובוחרים באפשרות 'שינוי כל המופעים'.
- מקלידים את השם החדש ב
demo-app
- שינוי רשומות בהגדרות של Kubernetes
- פתיחת קובץ
deployment.yaml - בוחרים את שם התמונה שמוגדר כרגע כ-
pom-xml-image - לוחצים לחיצה ימנית ובוחרים באפשרות 'שינוי כל המופעים'.
- מקלידים את השם החדש ב
demo-app
הפעלת מצב סנכרון אוטומטי
כדי לאפשר חוויית Hot Reload אופטימלית, תשתמשו בתכונת הסנכרון שמספק Jib. בשלב הזה מגדירים את Skaffold כך שישתמש בתכונה הזו בתהליך build.
שימו לב שהפרופיל sync שאתם מגדירים בהגדרת Skaffold משתמש בפרופיל sync של Spring שהגדרתם בשלב הקודם, שבו הפעלתם תמיכה ב-spring-dev-tools.
- עדכון ההגדרה של Skaffold
בקובץ skaffold.yaml, מחליפים את כל קטע ה-build בקובץ במפרט הבא. אין לשנות חלקים אחרים בקובץ.
skaffold.yaml
build:
artifacts:
- image: demo-app
jib:
project: com.example:demo
type: maven
args:
- --no-transfer-progress
- -Psync
fromImage: gcr.io/distroless/java17-debian11:debug
sync:
auto: true
הוספת נתיב ברירת מחדל
יוצרים קובץ בשם HelloController.java בתיקייה /src/main/java/com/example/springboot/.

כדי ליצור נתיב HTTP שמוגדר כברירת מחדל, מדביקים את התוכן הבא בקובץ.
HelloController.java
package com.example.springboot;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;
@RestController
public class HelloController {
@Value("${target:local}")
String target;
@GetMapping("/")
public String hello()
{
return String.format("Hello from your %s environment!", target);
}
}
4. הסבר על תהליך הפיתוח
בקטע הזה נסביר איך להשתמש בתוסף Cloud Code כדי ללמוד על התהליכים הבסיסיים ולאמת את ההגדרה וההתקנה של אפליקציית המתחילים.
Cloud Code משולב עם Skaffold כדי לייעל את תהליך הפיתוח. כשמבצעים פריסה ל-GKE בשלבים הבאים, Cloud Code ו-Skaffold יוצרים אוטומטית את קובץ האימג' של הקונטיינר, מעבירים אותו בדחיפה ל-Container Registry ואז פורסים את האפליקציה ל-GKE. התהליך הזה מתבצע מאחורי הקלעים, והפרטים לא מוצגים בתהליך הפיתוח. בנוסף, Cloud Code משפר את תהליך הפיתוח באמצעות מתן יכולות ניפוי באגים ו-hotsync מסורתיות לפיתוח מבוסס-קונטיינרים.
כניסה ל-Google Cloud
לוחצים על סמל Cloud Code ובוחרים באפשרות Sign in to Google Cloud (כניסה ל-Google Cloud):

לוחצים על 'המשך לכניסה'.

בודקים את הפלט ב-Terminal ופותחים את הקישור:

מתחברים באמצעות פרטי הכניסה שלכם ב-Qwiklabs.

בוחרים באפשרות 'אישור':

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

מדביקים את קוד האימות ומקישים על Enter.

הוספת אשכול Kubernetes
- הוספת אשכול

- בוחרים באפשרות Google Kubernetes Engine:

- בוחרים פרויקט.

- בוחרים באפשרות quote-cluster (ציטוט-אשכול) שנוצרה בהגדרה הראשונית.


הגדרת מזהה הפרויקט הנוכחי באמצעות gcloud CLI
מעתיקים את מזהה הפרויקט לשיעור ה-Lab הזה מדף qwiklabs.

מריצים את הפקודה gcloud cli כדי להגדיר את מזהה הפרויקט. לפני שמריצים את הפקודה, מחליפים את מזהה הפרויקט לדוגמה.
gcloud config set project qwiklabs-gcp-project-id
פלט לדוגמה:

ניפוי באגים ב-Kubernetes
- בחלונית הימנית, בתחתית, בוחרים באפשרות Cloud Code.

- בחלונית שמופיעה מתחת ל-DEVELOPMENT SESSIONS (סביבות פיתוח), בוחרים באפשרות Debug on Kubernetes (ניפוי באגים ב-Kubernetes).
אם האפשרות לא מוצגת, גוללים למטה.

- בוחרים באפשרות 'כן' כדי להשתמש בהקשר הנוכחי.

- בוחרים באפשרות quote-cluster (ציטוט-אשכול) שנוצרה במהלך ההגדרה הראשונית.

- בוחרים באפשרות 'מאגר תגים'.

- בוחרים בכרטיסייה 'פלט' בחלונית התחתונה כדי לראות את ההתקדמות וההתראות.
- בוחרים באפשרות Kubernetes: Run/Debug - Detailed (קובernetes: הפעלה/ניפוי באגים – מפורט) בתפריט הנפתח של הערוץ בצד שמאל כדי לראות פרטים נוספים ויומנים שמוזרמים בשידור חי מהקונטיינרים.

ממתינים לפריסת האפליקציה.

- בודקים את האפליקציה שנפרסה ב-GKE ב-Cloud Console.

- כדי לחזור לתצוגה הפשוטה, בוחרים באפשרות Kubernetes: Run/Debug (הפעלה/ניפוי באגים ב-Kubernetes) מהתפריט הנפתח בכרטיסייה OUTPUT (פלט).
- בסיום הבנייה והבדיקות, בכרטיסייה Output (פלט) מופיע הכיתוב
Resource deployment/demo-app status completed successfully, וכתובת URL מופיעה ברשימה: "Forwarded URL from service demo-app: http://localhost:8080" (כתובת URL שהועברה מהשירות demo-app: http://localhost:8080) - במסוף של Cloud Code, מעבירים את העכבר מעל כתובת ה-URL בפלט (http://localhost:8080), ואז בתיאור הכלים שמופיע בוחרים באפשרות 'מעבר לכתובת ה-URL'.

תיפתח כרטיסייה חדשה ויוצג הפלט הבא:

שימוש בנקודות עצירה (breakpoint)
- פותחים את האפליקציה
HelloController.javaשנמצאת במיקום/src/main/java/com/example/springboot/HelloController.java - מאתרים את פקודת החזרה של נתיב הבסיס (root) שבו כתוב
return String.format("Hello from your %s environment!", target); - מוסיפים נקודת עצירה לשורה הזו בלחיצה על השטח הריק שמימין למספר השורה. יוצג סימן אדום כדי לציין שהנקודה לעצירה מוגדרת

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

- לוחצים על החץ למטה בקטע Variables (משתנים) עד שמוצאים את המשתנה Target (יעד).
- הערך הנוכחי הוא local

- לוחצים לחיצה כפולה על שם המשתנה target ובחלון הקופץ,
משנים את הערך ל-Cloud Workstations

- לוחצים על הלחצן 'המשך' בחלונית הבקרה של ניפוי הבאגים.

- בודקים את התגובה בדפדפן, שבו מוצג עכשיו הערך המעודכן שהזנתם.

- כדי להסיר את נקודת העצירה, לוחצים על האינדיקטור האדום שמשמאל למספר השורה. כך הקוד לא יפסיק את ההרצה בשורה הזו כשמתקדמים בשיעור ה-Lab.
Hot Reload
- משנים את ההצהרה כך שתחזיר ערך אחר, כמו Hello from %s Code
- הקובץ נשמר ומסונכרן אוטומטית במאגרי הנתונים המרוחקים ב-GKE
- כדי לראות את התוצאות המעודכנות, צריך לרענן את הדפדפן.
- כדי להפסיק את סשן ניפוי הבאגים, לוחצים על הריבוע האדום בסרגל הכלים לניפוי באגים.

בוחרים באפשרות 'כן, לנקות אחרי כל הפעלה'.

5. פיתוח שירות REST פשוט של CRUD
בשלב הזה, האפליקציה שלכם מוגדרת באופן מלא לפיתוח מבוסס-קונטיינרים, ועברתם על תהליך העבודה הבסיסי לפיתוח באמצעות Cloud Code. בקטעים הבאים תתרגלו את מה שלמדתם על ידי הוספת נקודות קצה של שירות REST שמתחברות למסד נתונים מנוהל ב-Google Cloud.
הגדרת יחסי תלות
קוד האפליקציה משתמש במסד נתונים כדי לשמור את נתוני שירות ה-REST. כדי לוודא שיחסי התלות זמינים, מוסיפים את השורות הבאות לקובץ pom.xml
- פותחים את קובץ
pom.xmlומוסיפים את הפרטים הבאים לקטע dependencies (תלויות) בהגדרות:
pom.xml
<!-- Database dependencies-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>
קוד שירות REST
Quote.java
יוצרים קובץ בשם Quote.java בתיקייה /src/main/java/com/example/springboot/ ומעתיקים אליו את הקוד שבהמשך. ההגדרה הזו מגדירה את מודל הישות לאובייקט הצעת המחיר שמשמש באפליקציה.
package com.example.springboot;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.Objects;
@Entity
@Table(name = "quotes")
public class Quote
{
@Id
@Column(name = "id")
private Integer id;
@Column(name="quote")
private String quote;
@Column(name="author")
private String author;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getQuote() {
return quote;
}
public void setQuote(String quote) {
this.quote = quote;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Quote quote1 = (Quote) o;
return Objects.equals(id, quote1.id) &&
Objects.equals(quote, quote1.quote) &&
Objects.equals(author, quote1.author);
}
@Override
public int hashCode() {
return Objects.hash(id, quote, author);
}
}
QuoteRepository.java
יוצרים קובץ בשם QuoteRepository.java במיקום src/main/java/com/example/springboot ומעתיקים אליו את הקוד הבא
package com.example.springboot;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface QuoteRepository extends JpaRepository<Quote,Integer> {
@Query( nativeQuery = true, value =
"SELECT id,quote,author FROM quotes ORDER BY RANDOM() LIMIT 1")
Quote findRandomQuote();
}
הקוד הזה משתמש ב-JPA כדי לשמור את הנתונים. הכיתה מרחיבה את הממשק של Spring JPARepository ומאפשרת יצירה של קוד בהתאמה אישית. בקוד שהוספתם, יש findRandomQuote שיטה מותאמת אישית.
QuoteController.java
כדי לחשוף את נקודת הקצה של השירות, מחלקה מסוג QuoteController תספק את הפונקציונליות הזו.
יוצרים קובץ בשם QuoteController.java במיקום src/main/java/com/example/springboot ומעתיקים אליו את התוכן הבא:
package com.example.springboot;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class QuoteController {
private final QuoteRepository quoteRepository;
public QuoteController(QuoteRepository quoteRepository) {
this.quoteRepository = quoteRepository;
}
@GetMapping("/random-quote")
public Quote randomQuote()
{
return quoteRepository.findRandomQuote();
}
@GetMapping("/quotes")
public ResponseEntity<List<Quote>> allQuotes()
{
try {
List<Quote> quotes = new ArrayList<Quote>();
quoteRepository.findAll().forEach(quotes::add);
if (quotes.size()==0 || quotes.isEmpty())
return new ResponseEntity<List<Quote>>(HttpStatus.NO_CONTENT);
return new ResponseEntity<List<Quote>>(quotes, HttpStatus.OK);
} catch (Exception e) {
System.out.println(e.getMessage());
return new ResponseEntity<List<Quote>>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("/quotes")
public ResponseEntity<Quote> createQuote(@RequestBody Quote quote) {
try {
Quote saved = quoteRepository.save(quote);
return new ResponseEntity<Quote>(saved, HttpStatus.CREATED);
} catch (Exception e) {
System.out.println(e.getMessage());
return new ResponseEntity<Quote>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PutMapping("/quotes/{id}")
public ResponseEntity<Quote> updateQuote(@PathVariable("id") Integer id, @RequestBody Quote quote) {
try {
Optional<Quote> existingQuote = quoteRepository.findById(id);
if(existingQuote.isPresent()){
Quote updatedQuote = existingQuote.get();
updatedQuote.setAuthor(quote.getAuthor());
updatedQuote.setQuote(quote.getQuote());
return new ResponseEntity<Quote>(updatedQuote, HttpStatus.OK);
} else {
return new ResponseEntity<Quote>(HttpStatus.NOT_FOUND);
}
} catch (Exception e) {
System.out.println(e.getMessage());
return new ResponseEntity<Quote>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@DeleteMapping("/quotes/{id}")
public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
Optional<Quote> quote = quoteRepository.findById(id);
if (quote.isPresent()) {
quoteRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} else {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
הוספת הגדרות מסד נתונים
application.yaml
מוסיפים הגדרה למסד הנתונים של הקצה העורפי שהשירות ניגש אליו. עורכים (או יוצרים אם הוא לא קיים) את הקובץ שנקרא application.yaml בתיקייה src/main/resources ומוסיפים הגדרת Spring עם פרמטרים עבור ה-Backend.
target: local
spring:
config:
activate:
on-profile: cloud-dev
datasource:
url: 'jdbc:postgresql://${DB_HOST:127.0.0.1}/${DB_NAME:quote_db}'
username: '${DB_USER:user}'
password: '${DB_PASS:password}'
jpa:
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
dialect: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
הוספה של Database Migration
יצירת תיקיות db/migration מתחת ל-src/main/resources
יוצרים קובץ SQL: V1__create_quotes_table.sql
מדביקים את התוכן הבא בקובץ
V1__create_quotes_table.sql
CREATE TABLE quotes(
id INTEGER PRIMARY KEY,
quote VARCHAR(1024),
author VARCHAR(256)
);
INSERT INTO quotes (id,quote,author) VALUES (1,'Never, never, never give up','Winston Churchill');
INSERT INTO quotes (id,quote,author) VALUES (2,'While there''s life, there''s hope','Marcus Tullius Cicero');
INSERT INTO quotes (id,quote,author) VALUES (3,'Failure is success in progress','Anonymous');
INSERT INTO quotes (id,quote,author) VALUES (4,'Success demands singleness of purpose','Vincent Lombardi');
INSERT INTO quotes (id,quote,author) VALUES (5,'The shortest answer is doing','Lord Herbert');
Kubernetes Config
התוספות הבאות לקובץ deployment.yaml מאפשרות לאפליקציה להתחבר למופעי CloudSQL.
- TARGET – מגדיר את המשתנה כך שיציין את הסביבה שבה האפליקציה מופעלת
- SPRING_PROFILES_ACTIVE – מציג את פרופיל Spring הפעיל, שיוגדר ל-
cloud-dev - DB_HOST – כתובת ה-IP הפרטית של מסד הנתונים, שצוינה כשנוצרה מכונת מסד הנתונים או בלחיצה על
SQLבתפריט הניווט של מסוף Google Cloud – צריך לשנות את הערך. - DB_USER ו-DB_PASS – כמו שהוגדר בהגדרות של מופע CloudSQL, מאוחסן כסוד ב-GCP
מעדכנים את הקובץ deployment.yaml עם התוכן שבהמשך.
deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: demo-app
labels:
app: demo-app
spec:
ports:
- port: 8080
protocol: TCP
clusterIP: None
selector:
app: demo-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
labels:
app: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: demo-app
image: demo-app
env:
- name: PORT
value: "8080"
- name: TARGET
value: "Local Dev - CloudSQL Database - K8s Cluster"
- name: SPRING_PROFILES_ACTIVE
value: cloud-dev
- name: DB_HOST
value: ${DB_INSTANCE_IP}
- name: DB_PORT
value: "5432"
- name: DB_USER
valueFrom:
secretKeyRef:
name: gke-cloud-sql-secrets
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: gke-cloud-sql-secrets
key: password
- name: DB_NAME
valueFrom:
secretKeyRef:
name: gke-cloud-sql-secrets
key: database
כדי להחליף את הערך DB_HOST בכתובת של מסד הנתונים, מריצים את הפקודות הבאות בטרמינל:
export DB_INSTANCE_IP=$(gcloud sql instances describe quote-db-instance \
--format=json | jq \
--raw-output ".ipAddresses[].ipAddress")
envsubst < deployment.yaml > deployment.new && mv deployment.new deployment.yaml
פותחים את הקובץ deployment.yaml ומוודאים שהערך DB_HOST עודכן עם כתובת ה-IP של המופע.

פריסה ואימות של אפליקציה
- בחלונית שבתחתית Cloud Shell Editor, בוחרים באפשרות Cloud Code ואז בוחרים באפשרות Debug on Kubernetes (ניפוי באגים ב-Kubernetes) בחלק העליון של המסך.

- בסיום הבנייה והבדיקות, בכרטיסייה Output (פלט) מופיע הכיתוב
Resource deployment/demo-app status completed successfully, וכתובת URL: "Forwarded URL from service demo-app: http://localhost:8080". שימו לב: לפעמים היציאה עשויה להיות שונה, למשל 8081. אם כן, מגדירים את הערך המתאים. הגדרת הערך של כתובת ה-URL במסוף
export URL=localhost:8080
- הצגת ציטוטים אקראיים
מריצים את הפקודה הבאה ב-Terminal כמה פעמים מול נקודת הקצה random-quote. התבוננות בשיחה חוזרת שבה מוצגות הצעות מחיר שונות
curl $URL/random-quote | jq
- הוספת הצעת מחיר
יוצרים הצעת מחיר חדשה עם id=6 באמצעות הפקודה שמופיעה בהמשך, ומתבוננים בבקשה שמוחזרת.
curl -H 'Content-Type: application/json' -d '{"id":"6","author":"Henry David Thoreau","quote":"Go confidently in the direction of your dreams! Live the life you have imagined"}' -X POST $URL/quotes
- מחיקת הצעת מחיר
עכשיו מוחקים את הצעת המחיר שהוספתם באמצעות שיטת המחיקה, ומתבוננים בקוד התגובה HTTP/1.1 204.
curl -v -X DELETE $URL/quotes/6
- שגיאת שרת
לנסות להריץ שוב את הבקשה האחרונה אחרי שהערך כבר נמחק כדי לראות את מצב השגיאה
curl -v -X DELETE $URL/quotes/6
שימו לב שהתשובה מחזירה HTTP:500 Internal Server Error.
ניפוי באגים באפליקציה
בקטע הקודם, נתקלתם במצב שגיאה באפליקציה כשניסיתם למחוק רשומה שלא הייתה במסד הנתונים. בקטע הזה נגדיר נקודת עצירה כדי לאתר את הבעיה. השגיאה התרחשה בפעולת המחיקה, ולכן תעבדו עם המחלקה QuoteController.
- פתיחה של
src/main/java/com/example/springboot/QuoteController.java - חיפוש אמצעי התשלום
deleteQuote() - מחפשים את השורה:
Optional<Quote> quote = quoteRepository.findById(id); - מגדירים נקודת עצירה בשורה הזו על ידי לחיצה על השטח הריק שמימין למספר השורה.
- יופיע סימן אדום שמציין שהנקודה לעצירה הוגדרה
- מריצים שוב את הפקודה
delete.
curl -v -X DELETE $URL/quotes/6
- כדי לחזור לתצוגת ניפוי הבאגים, לוחצים על הסמל בעמודה הימנית.
- שימו לב ששורת הניפוי באגים נעצרה במחלקה QuoteController.
- במאגר הבאגים, לוחצים על הסמל
step over
- שימו לב שהקוד מחזיר שגיאת שרת פנימית HTTP 500 ללקוח, וזה לא אידיאלי.
Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > DELETE /quotes/6 HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.74.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 500 < Content-Length: 0 < Date: < * Connection #0 to host 127.0.0.1 left intact
עדכון הקוד
הקוד שגוי, וצריך לבצע refactoring בבלוק else כדי להחזיר קוד סטטוס HTTP 404 (לא נמצא).
מתקנים את השגיאה.
- בזמן שסשן ניפוי הבאגים עדיין פועל, לוחצים על הלחצן 'המשך' בלוח הבקרה של ניפוי הבאגים כדי להשלים את הבקשה.
- לאחר מכן משנים את הבלוק
elseלקוד:
else {
return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
}
השיטה צריכה להיראות כך
@DeleteMapping("/quotes/{id}")
public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
Optional<Quote> quote = quoteRepository.findById(id);
if (quote.isPresent()) {
quoteRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} else {
return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
}
}
- להריץ מחדש את פקודת המחיקה
curl -v -X DELETE $URL/quotes/6
- מריצים את מאתר הבאגים ומסתכלים על התגובה HTTP 404 Not Found שמוחזרת למתקשר.
Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > DELETE /quotes/6 HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.74.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 404 < Content-Length: 0 < Date: < * Connection #0 to host 127.0.0.1 left intact
- כדי להפסיק את סשן ניפוי הבאגים, לוחצים על הריבוע האדום בסרגל הכלים לניפוי באגים.


6. מזל טוב
מעולה! בשיעור ה-Lab הזה יצרתם אפליקציית Java חדשה מאפס והגדרתם אותה כך שתפעל ביעילות עם קונטיינרים. לאחר מכן פרסתם את האפליקציה וניפיתם בה באגים באשכול GKE מרוחק, לפי אותו תהליך פיתוח שקיים במערכות אפליקציות מסורתיות.
מה למדתם
- פיתוח InnerLoop באמצעות Cloud Workstations
- יצירת אפליקציית Java חדשה למתחילים
- הסבר על תהליך הפיתוח
- פיתוח שירות REST פשוט של CRUD
- ניפוי באגים באפליקציה באשכול GKE
- חיבור אפליקציה למסד נתונים של Cloud SQL
