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



- שם הפרויקט הוא השם המוצג של הפרויקט הזה למשתתפים. זו מחרוזת של תווים שלא נמצאת בשימוש ב-Google APIs, ואפשר לעדכן אותה בכל שלב.
- מזהה הפרויקט חייב להיות ייחודי לכל הפרויקטים ב-Google Cloud, והוא קבוע (אי אפשר לשנות אותו אחרי שמגדירים אותו). מסוף Cloud יוצר באופן אוטומטי מחרוזת ייחודית. בדרך כלל לא צריך להתייחס אליה. ברוב סדנאות ה-Codelab, צריך להפנות למזהה הפרויקט (ובדרך כלל הוא מזוהה כ-
PROJECT_ID), אז אם לא מוצא חן בעיניכם, אפשר ליצור מזהה אקראי אחר, או לנסות מזהה משלכם ולבדוק אם הוא זמין. אחרי שהפרויקט נוצר, הוא 'קפוא'. - יש ערך שלישי, מספר פרויקט, שחלק מממשקי ה-API משתמשים בו. במאמרי העזרה מפורט מידע נוסף על שלושת הערכים האלה.
- בשלב הבא, תצטרכו להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבי Cloud או בממשקי API. העלות של התרגול הזה לא אמורה להיות גבוהה, ואולי אפילו לא תצטרכו לשלם בכלל. כדי לכבות את המשאבים ולא לחייב אתכם מעבר למה שמוסבר במדריך הזה, צריך לפעול לפי ההוראות לניקוי שמופיעות בסוף ה-Codelab. משתמשים חדשים ב-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 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 ותגשו לנתונים שמאוחסנים במסד נתונים של Cloud SQL. סקריפט ההגדרה שמופיע בהמשך מכין את התשתית הזו בשבילכם. תהליך ההקצאה יימשך יותר מ-10 דקות. אפשר להמשיך לשלבים הבאים בזמן שההגדרה מתבצעת.
./setup.sh
3. יצירת אפליקציית Java חדשה למתחילים
בקטע הזה ניצור אפליקציית Java Spring Boot חדשה מאפס, באמצעות אפליקציה לדוגמה שסופקה על ידי spring.io
שכפול האפליקציה לדוגמה
- יצירת אפליקציה למתחילים
curl https://start.spring.io/starter.zip -d dependencies=web -d type=maven-project -d javaVersion=11 -d packageName=com.example.springboot -o sample-app.zip
- ביטול הדחיסה של האפליקציה
unzip sample-app.zip -d sample-app
- עוברים לספרייה sample-app ופותחים את התיקייה בסביבת העבודה של Cloud Shell IDE.
cd sample-app && cloudshell workspace .
הוספה של 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>
בוחרים באפשרות Always אם מוצגת בקשה לגבי שינוי בקובץ build.

יצירת קובצי מניפסט
Skaffold מספקת כלים משולבים שמפשטים את פיתוח הקונטיינרים. בשלב הזה מאתחלים את skaffold, שיצור באופן אוטומטי קובצי YAML בסיסיים של Kubernetes. התהליך מנסה לזהות ספריות עם הגדרות של קובצי אימג' של קונטיינרים, כמו Dockerfile, ואז יוצר מניפסט של פריסה ושירות לכל אחת מהן.
מריצים את הפקודה שלמטה כדי להתחיל בתהליך.
- מריצים את הפקודה הבאה במסוף
skaffold init --generate-manifests
- כשמופיעה בקשה:
- משתמשים בחצים כדי להזיז את הסמן אל
Jib Maven Plugin - מקישים על מקש הרווח כדי לבחור את האפשרות.
- לוחצים על Enter כדי להמשיך
- מזינים 8080 בשדה של היציאה.
- מזינים y כדי לשמור את ההגדרה
שני קבצים נוספים לסביבת העבודה, skaffold.yaml ו-deployment.yaml
עדכון שם האפליקציה
ערכי ברירת המחדל שכלולים בתצורה לא תואמים כרגע לשם האפליקציה. מעדכנים את הקבצים כך שיפנו לשם האפליקציה ולא לערכי ברירת המחדל.
- שינוי רשומות בהגדרות של 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/java: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 מסורתיות לפיתוח מבוסס-קונטיינרים.
פריסה ב-Kubernetes
- בחלונית שבתחתית Cloud Shell Editor, בוחרים באפשרות Cloud Code 

- בחלונית שמופיעה בחלק העליון, בוחרים באפשרות 'ניפוי באגים ב-Kubernetes'. אם מוצגת בקשה, בוחרים באפשרות Yes כדי להשתמש בהקשר הנוכחי של Kubernetes.

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

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

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

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

- כדי לחזור לתצוגה הפשוטה, בוחרים באפשרות Kubernetes: Run/Debug (הפעלה/ניפוי באגים) מהתפריט הנפתח.
- בסיום הבנייה והבדיקות, בכרטיסייה 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), ואז בתיאור הכלים שמופיע בוחרים באפשרות 'פתיחת תצוגה מקדימה באינטרנט'.
התשובה תהיה:
Hello from your local environment!
שימוש בנקודות עצירה (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 (ענן).
- לוחצים על הלחצן 'המשך' בחלונית הבקרה של ניפוי הבאגים.
- בודקים את התגובה בדפדפן, שבו מוצג עכשיו הערך המעודכן שהזנתם.
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>
קידוד שאר השירות
Quote.java
יוצרים קובץ בשם Quote.java בנתיב /src/main/java/com/example/springboot/ ומעתיקים אליו את הקוד שבהמשך. ההגדרה הזו מגדירה את מודל הישות לאובייקט הצעת המחיר שמשמש באפליקציה.
package com.example.springboot;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.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) {
try {
quoteRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (RuntimeException e) {
System.out.println(e.getMessage());
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
יוצרים תיקייה בנתיב src/main/resources/db/migration/
יוצרים קובץ 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
פריסה ואימות של אפליקציה
- בחלונית שבתחתית 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" (כתובת URL שהועברה מהשירות demo-app: http://localhost:8080) - הצגת ציטוטים אקראיים
מריצים את הפקודה הבאה כמה פעמים במסוף של Cloud Shell כדי להפעיל אותה מול נקודת הקצה random-quote. התבוננות בשיחה חוזרת שבה מוצגות הצעות מחיר שונות
curl -v 127.0.0.1:8080/random-quote
- הוספת הצעת מחיר
יוצרים הצעת מחיר חדשה עם id=6 באמצעות הפקודה שמופיעה בהמשך, ומתבוננים בבקשה שמוחזרת.
curl -v -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 127.0.0.1:8080/quotes
- מחיקת הצעת מחיר
עכשיו מוחקים את הצעת המחיר שהוספתם באמצעות שיטת המחיקה, ומתבוננים בקוד התגובה HTTP/1.1 204.
curl -v -X DELETE 127.0.0.1:8080/quotes/6
- שגיאת שרת
לנסות להריץ שוב את הבקשה האחרונה אחרי שהערך כבר נמחק כדי לראות את מצב השגיאה
curl -v -X DELETE 127.0.0.1:8080/quotes/6
שימו לב שהתשובה מחזירה HTTP:500 Internal Server Error.
ניפוי באגים באפליקציה
בקטע הקודם, נתקלתם במצב שגיאה באפליקציה כשניסיתם למחוק רשומה שלא הייתה במסד הנתונים. בקטע הזה נגדיר נקודת עצירה כדי לאתר את הבעיה. השגיאה התרחשה בפעולת המחיקה, ולכן תעבדו עם המחלקה QuoteController.
- פותחים את הקובץ src.main.java.com.example.springboot.QuoteController.java
- חיפוש אמצעי התשלום
deleteQuote() - מאתרים את השורה שבה מוחקים פריט מהמסד נתונים:
quoteRepository.deleteById(id); - מגדירים נקודת עצירה בשורה הזו על ידי לחיצה על השטח הריק שמימין למספר השורה.
- יופיע סימן אדום שמציין שהנקודה לעצירה הוגדרה
- מריצים שוב את הפקודה
delete.
curl -v -X DELETE 127.0.0.1:8080/quotes/6
- כדי לחזור לתצוגת ניפוי הבאגים, לוחצים על הסמל בעמודה הימנית.
- שימו לב ששורת הניפוי באגים נעצרה במחלקה QuoteController.
- במאגר הבאגים, לוחצים על סמל
step over
ורואים שמוצג חריג. - שימו לב שהקוד הזה מחזיר שגיאת שרת פנימית HTTP 500 ללקוח, וזה לא אידיאלי.
RuntimeException was caught.
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
עדכון הקוד
הקוד שגוי, וצריך לשנות את מבנה בלוק החריגה כדי ללכוד את החריגה EmptyResultDataAccessException ולהחזיר קוד סטטוס HTTP 404 (לא נמצא).
מתקנים את השגיאה.
- בזמן שסשן ניפוי הבאגים עדיין פועל, לוחצים על הלחצן 'המשך' בלוח הבקרה של ניפוי הבאגים כדי להשלים את הבקשה.
- לאחר מכן מוסיפים את הבלוק הבא לקוד:
} catch (EmptyResultDataAccessException e){
return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
}
השיטה צריכה להיראות כך
public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
try {
quoteRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch(EmptyResultDataAccessException e){
return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
} catch (RuntimeException e) {
System.out.println(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
- להריץ מחדש את פקודת המחיקה
curl -v -X DELETE 127.0.0.1:8080/quotes/6
- מריצים את מאתר הבאגים ומסתכלים על
EmptyResultDataAccessExceptionשנתפס ועל קוד הסטטוס 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 מרוחק, לפי אותו תהליך פיתוח שקיים במערכות אפליקציות מסורתיות.
כדי לנקות אחרי השלמת ה-Lab:
- מחיקת הקבצים שבהם נעשה שימוש במעבדה
cd ~ && rm -rf container-developer-workshop
- מחיקת הפרויקט כדי להסיר את כל התשתית והמשאבים שקשורים אליו