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

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'.

אחרי שתחנת העבודה תופעל, תראו את סביבת הפיתוח המשולבת (IDE) של Code OSS. לוחצים על 'סימון כהשלמה' בדף 'תחילת העבודה' ב-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. בסיום התהליך, הקטע build אמור להיראות כך.
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 (Kubernetes: הפעלה/ניפוי באגים – מפורט) בתפריט הנפתח של הערוץ בצד שמאל.

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

- בודקים את האפליקציה שנפרסה ב-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" - במסוף Cloud Code, מעבירים את העכבר מעל כתובת ה-URL בפלט (http://localhost:8080), ואז בתיאור הכלים שמופיע בוחרים באפשרות 'מעבר לקישור'.

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

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

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

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

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

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

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

- כדי להסיר את נקודת העצירה, לוחצים על האינדיקטור האדום שמשמאל למספר השורה. כך הקוד לא יפסיק את ההרצה בשורה הזו כשמתקדמים בשיעור ה-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
- הצגת ציטוטים אקראיים
בטרמינל, מריצים את הפקודה שלמטה כמה פעמים מול נקודת הקצה random-quote. התבוננות בשיחה חוזרת שחוזרת על עצמה עם הצעות מחיר שונות
curl $URL/random-quote | jq
- הוספת הצעת מחיר
יוצרים הצעת מחיר חדשה עם מזהה 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
עדכון הקוד
הקוד שגוי, וצריך לשנות את המבנה של הבלוק 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
- חיבור האפליקציה למסד נתונים של CloudSQL
