1. סקירה כללית
Cloud Run היא פלטפורמה מנוהלת שמאפשרת להריץ את הקוד ישירות מעל התשתית הניתנת להתאמה של Google. ב-Codelab הזה נסביר איך לחבר אפליקציית Next.js ב-Cloud Run למסד נתונים של Cloud SQL ל-PostgreSQL.
בשיעור ה-Lab הזה תלמדו איך:
- יצירת מכונה של Cloud SQL ל-PostgreSQL (שמוגדרת לשימוש ב-Private Service Connect)
- פריסת אפליקציה ב-Cloud Run שמתחבר למסד הנתונים של Cloud SQL
- איך משתמשים ב-Gemini Code Assist כדי להוסיף פונקציונליות לאפליקציה
2. דרישות מוקדמות
- אם עדיין אין לכם חשבון Google, עליכם ליצור חשבון Google.
- משתמשים בחשבון אישי במקום בחשבון לצורכי עבודה או בחשבון בית ספרי. יכול להיות שבחשבונות לצורכי עבודה ובחשבונות בית ספריים יש הגבלות שמונעות מכם להפעיל את ממשקי ה-API הנדרשים לסדנה הזו.
3. הגדרת הפרויקט
- נכנסים למסוף Google Cloud.
- מפעילים את החיוב במסוף Cloud.
- השלמת ה-Lab הזה אמורה לעלות פחות מ-1 $בארה"ב במשאבי Cloud.
- כדי למנוע חיובים נוספים, תוכלו לפעול לפי השלבים שמפורטים בסוף שיעור ה-Lab הזה כדי למחוק את המשאבים.
- משתמשים חדשים זכאים לתקופת ניסיון בחינם בשווי 300$.
- יוצרים פרויקט חדש או בוחרים לעשות שימוש חוזר בפרויקט קיים.
4. פתיחת Cloud Shell Editor
- עוברים אל Cloud Shell Editor.
- אם מסוף ה-SSH לא מופיע בחלק התחתון של המסך, פותחים אותו:
- לוחצים על תפריט שלושת הקווים
.
- לוחצים על Terminal (מסוף).
- לוחצים על מסוף חדש
- לוחצים על תפריט שלושת הקווים
- בטרמינל, מגדירים את הפרויקט באמצעות הפקודה הבאה:
- פורמט:
gcloud config set project [PROJECT_ID]
- דוגמה:
gcloud config set project lab-project-id-example
- אם לא זוכרים את מזהה הפרויקט:
- אפשר להציג את כל מזהי הפרויקטים באמצעות:
gcloud projects list | awk '/PROJECT_ID/{print $2}'
- אפשר להציג את כל מזהי הפרויקטים באמצעות:
- פורמט:
- אם מתבקשים לאשר, לוחצים על Authorize (מתן הרשאה) כדי להמשיך.
- אמורה להופיע ההודעה הבאה:
אם מופיעUpdated property [core/project].
WARNING
ונשאלת השאלהDo you want to continue (Y/N)?
, סביר להניח שהזנתם את מזהה הפרויקט בצורה שגויה. מקישים עלN
, מקישים עלEnter
ומנסים להריץ שוב את הפקודהgcloud config set project
.
5. הפעלת ממשקי API
מפעילים את ממשקי ה-API בטרמינל:
gcloud services enable \
compute.googleapis.com \
sqladmin.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com \
networkconnectivity.googleapis.com \
servicenetworking.googleapis.com \
cloudaicompanion.googleapis.com
אם מתבקשים לאשר, לוחצים על Authorize (מתן הרשאה) כדי להמשיך.
השלמת הפקודה עשויה להימשך כמה דקות, אבל בסופו של דבר אמורה להופיע הודעה על השלמה, בדומה להודעה הבאה:
Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.
6. הגדרת חשבון שירות
יוצרים ומגדירים חשבון שירות ב-Google Cloud לשימוש ב-Cloud Run, כך שיהיה לו את ההרשאות המתאימות להתחברות ל-Cloud SQL.
- כדי ליצור חשבון שירות חדש, מריצים את הפקודה
gcloud iam service-accounts create
באופן הבא:gcloud iam service-accounts create quickstart-service-account \ --display-name="Quickstart Service Account"
- כדי להוסיף את התפקיד כתיבה ביומן לחשבון השירות ב-Google Cloud שיצרתם, מריצים את הפקודה gcloud projects add-iam-policy-binding באופן הבא:
gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \ --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \ --role="roles/logging.logWriter"
7. יצירת מכונה של Cloud SQL
- יצירת מדיניות חיבור לשירות כדי לאפשר קישוריות רשת מ-Cloud Run ל-Cloud SQL באמצעות Private Service Connect
gcloud network-connectivity service-connection-policies create quickstart-policy \ --network=default \ --project=${GOOGLE_CLOUD_PROJECT} \ --region=us-central1 \ --service-class=google-cloud-sql \ --subnets=https://www.googleapis.com/compute/v1/projects/${GOOGLE_CLOUD_PROJECT}/regions/us-central1/subnetworks/default
- יצירת סיסמה ייחודית למסד הנתונים
export DB_PASSWORD=$(openssl rand -base64 20)
- מריצים את הפקודה
gcloud sql instances create
כדי ליצור מכונה של Cloud SQLgcloud sql instances create quickstart-instance \ --project=${GOOGLE_CLOUD_PROJECT} \ --root-password=${DB_PASSWORD} \ --database-version=POSTGRES_17 \ --tier=db-perf-optimized-N-2 \ --region=us-central1 \ --ssl-mode=ENCRYPTED_ONLY \ --no-assign-ip \ --enable-private-service-connect \ --psc-auto-connections=network=projects/${GOOGLE_CLOUD_PROJECT}/global/networks/default
השלמת הפקודה הזו עשויה להימשך כמה דקות.
- מריצים את הפקודה
gcloud sql databases create
כדי ליצור מסד נתונים של Cloud SQL בתוךquickstart-instance
.gcloud sql databases create quickstart_db \ --instance=quickstart-instance
8. הכנת הבקשה
מכינים אפליקציית Next.js שמגיבה לבקשות HTTP.
- כדי ליצור פרויקט Next.js חדש בשם
task-app
, משתמשים בפקודה:npx create-next-app@15.0.3 task-app \ --ts \ --eslint \ --tailwind \ --no-src-dir \ --turbopack \ --app \ --no-import-alias
- אם מופיעה בקשה להתקין את
create-next-app
, מקישים עלEnter
כדי להמשיך:Need to install the following packages: create-next-app@15.0.3 Ok to proceed? (y)
- עוברים לספרייה
task-app
:cd task-app
- מתקינים את
pg
כדי ליצור אינטראקציה עם מסד הנתונים של PostgreSQL.npm install pg
- כדי להשתמש באפליקציית TypeScript Next.js, צריך להתקין את
@types/pg
כחבילת תלות לפיתוח.npm install --save-dev @types/pg
- יוצרים את הקובץ
actions.ts
.touch app/actions.ts
- פותחים את הקובץ
actions.ts
ב-Cloud Shell Editor: עכשיו אמור להופיע קובץ ריק בחלק העליון של המסך. כאן אפשר לערוך את הקובץcloudshell edit app/actions.ts
actions.ts
. - מעתיקים את הקוד הבא ומדביקים אותו בקובץ
actions.ts
הפתוח:'use server' import pg from 'pg'; type Task = { id: string; title: string; status: 'IN_PROGRESS' | 'COMPLETE'; }; const { Pool } = pg; const pool = new Pool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, ssl: { // @ts-expect-error require true is not recognized by @types/pg, but does exist on pg require: true, rejectUnauthorized: false, // required for self-signed certs // https://node-postgres.com/features/ssl#self-signed-cert } }); const tableCreationIfDoesNotExist = async () => { await pool.query(`CREATE TABLE IF NOT EXISTS visits ( id SERIAL NOT NULL, created_at timestamp NOT NULL, PRIMARY KEY (id) );`); await pool.query(`CREATE TABLE IF NOT EXISTS tasks ( id SERIAL NOT NULL, created_at timestamp NOT NULL, status VARCHAR(255) NOT NULL default 'IN_PROGRESS', title VARCHAR(1024) NOT NULL, PRIMARY KEY (id) );`); } // CREATE export async function addNewTaskToDatabase(newTask: string) { await tableCreationIfDoesNotExist(); await pool.query(`INSERT INTO tasks(created_at, status, title) VALUES(NOW(), 'IN_PROGRESS', $1)`, [newTask]); return; } // READ export async function getTasksFromDatabase() { await tableCreationIfDoesNotExist(); const { rows } = await pool.query(`SELECT id, created_at, status, title FROM tasks ORDER BY created_at DESC LIMIT 100`); console.table(rows); // logs the last 5 visits on the server return rows; // sends the last 5 visits to the client } // UPDATE export async function updateTaskInDatabase(task: Task) { await tableCreationIfDoesNotExist(); await pool.query( `UPDATE tasks SET status = $1, title = $2 WHERE id = $3`, [task.status, task.title, task.id] ); return; } // DELETE export async function deleteTaskFromDatabase(taskId: string) { await tableCreationIfDoesNotExist(); await pool.query(`DELETE FROM tasks WHERE id = $1`, [taskId]); return; }
- פותחים את הקובץ
page.tsx
ב-Cloud Shell Editor: עכשיו אמור להופיע קובץ קיים בחלק העליון של המסך. כאן אפשר לערוך את הקובץcloudshell edit app/page.tsx
page.tsx
. - מוחקים את התוכן הקיים בקובץ
page.tsx
. - מעתיקים את הקוד הבא ומדביקים אותו בקובץ
page.tsx
הפתוח:'use client' import React, { useEffect, useState } from "react"; import { addNewTaskToDatabase, getTasksFromDatabase, deleteTaskFromDatabase, updateTaskInDatabase } from "./actions"; type Task = { id: string; title: string; status: 'IN_PROGRESS' | 'COMPLETE'; }; export default function Home() { const [newTaskTitle, setNewTaskTitle] = useState(''); const [tasks, setTasks] = useState<Task[]>([]); async function getTasks() { const updatedListOfTasks = await getTasksFromDatabase(); setTasks(updatedListOfTasks); } useEffect(() => { getTasks(); }, []); async function addTask() { await addNewTaskToDatabase(newTaskTitle); await getTasks(); setNewTaskTitle(''); } async function updateTask(task: Task, newTaskValues: Partial<Task>) { await updateTaskInDatabase({ ...task, ...newTaskValues }); await getTasks(); } async function deleteTask(taskId: string) { await deleteTaskFromDatabase(taskId); await getTasks(); } return ( <main className="p-4"> <h2 className="text-2xl font-bold mb-4">To Do List</h2> <div className="flex mb-4"> <form onSubmit={handleSubmit} className="flex mb-8"> <input type="text" placeholder="New Task Title" value={newTaskTitle} onChange={(e) => setNewTaskTitle(e.target.value)} className="flex-grow border border-gray-400 rounded px-3 py-2 mr-2 bg-inherit" /> <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-nowrap" > Add New Task </button> </form> </div> <table className="w-full"> <tbody> {tasks.map(function (task) { const isComplete = task.status === 'COMPLETE'; return ( <tr key={task.id} className="border-b border-gray-200"> <td className="py-2 px-4"> <input type="checkbox" checked={isComplete} onClick={() => updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })} className="transition-transform duration-300 ease-in-out transform scale-100 checked:scale-125 checked:bg-green-500" /> </td> <td className="py-2 px-4"> <span className={`transition-all duration-300 ease-in-out ${isComplete ? 'line-through text-gray-400 opacity-50' : 'opacity-100'}`}> {task.title} </span> </td> <td className="py-2 px-4"> <button onClick={() => deleteTask(task.id)} className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded" > Delete </button> </td> </tr> ); })} </tbody> </table> </main> ); }
האפליקציה מוכנה עכשיו לפריסה.
9. פריסת האפליקציה ב-Cloud Run
- כדי להוסיף את התפקיד Network User לחשבון השירות של Cloud Run לשירות Cloud Run שאתם עומדים ליצור, מריצים את הפקודה gcloud projects add-iam-policy-binding באופן הבא:
gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \ --member "serviceAccount:service-$(gcloud projects describe ${GOOGLE_CLOUD_PROJECT} --format="value(projectNumber)")@serverless-robot-prod.iam.gserviceaccount.com" \ --role "roles/compute.networkUser"
- מריצים את הפקודה הבאה כדי לפרוס את האפליקציה ב-Cloud Run:
gcloud run deploy helloworld \ --region=us-central1 \ --source=. \ --set-env-vars DB_NAME="quickstart_db" \ --set-env-vars DB_USER="postgres" \ --set-env-vars DB_PASSWORD=${DB_PASSWORD} \ --set-env-vars DB_HOST="$(gcloud sql instances describe quickstart-instance --project=${GOOGLE_CLOUD_PROJECT} --format='value(settings.ipConfiguration.pscConfig.pscAutoConnections.ipAddress)')" \ --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \ --network=default \ --subnet=default \ --allow-unauthenticated
- אם מופיעה בקשה, מקישים על
Y
ו-Enter
כדי לאשר את המשך הפעולה:Do you want to continue (Y/n)? Y
אחרי כמה דקות, האפליקציה אמורה לספק כתובת URL שאפשר להיכנס אליה.
עוברים לכתובת ה-URL כדי לראות את האפליקציה בפעולה. בכל פעם שתכנסו לכתובת ה-URL או תחדשו את הדף, תראו את אפליקציית המשימות.
10. הוספת תכונה באמצעות Gemini Code Assist
עכשיו פרסתם אפליקציית אינטרנט עם מסד נתונים. בשלב הבא נוסיף תכונה חדשה לאפליקציית next.js שלנו בעזרת AI.
- חזרה ל-Cloud Shell Editor
- פותחים שוב את
page.tsx
cd ~/task-app cloudshell edit app/page.tsx
- עוברים אל Gemini Code Assist ב-Cloud Shell Editor:
- לוחצים על סמל Gemini
בסרגל הכלים שבצד ימין של המסך.
- אם תתבקשו לעשות זאת, היכנסו לחשבון Google.
- אם מתבקשים לבחור פרויקט, בוחרים את הפרויקט שיצרתם ל-Codelab הזה
- לוחצים על סמל Gemini
- מזינים את ההנחיה:
Add the ability to update the title of the task. The code in your output should be complete and working code.
. התשובה אמורה לכלול קטעי קוד דומים לקטעים הבאים כדי להוסיף פונקציותhandleEditStart
ו-handleEditCancel
:const [editingTaskId, setEditingTaskId] = useState<string>(''); const [editedTaskTitle, setEditedTaskTitle] = useState(''); const handleEditStart = (task: Task) => { setEditingTaskId(task.id); setEditedTaskTitle(task.title); }; const handleEditCancel = () => { setEditingTaskId(''); setEditedTaskTitle(''); };
<td className="py-2 px-4"> {editingTaskId === task.id ? ( <form onSubmit={(e) => { e.preventDefault(); updateTask(task, { title: editedTaskTitle }); }}> <input type="text" value={editedTaskTitle} onChange={(e) => setEditedTaskTitle(e.target.value)} onBlur={() => handleEditCancel} // Handle clicking outside input className="border border-gray-400 rounded px-3 py-1 mr-2" /> <button type="submit" className="text-green-600 hover:text-green-900 mr-1">Save</button> </form> ) : ( <span onClick={() => handleEditStart(task)} className="cursor-pointer"> {task.title} </span> )} </td>
- מחליפים את
page.tsx
בפלט של Gemini Code Assist. דוגמה עובדת:'use client' import React, { useEffect, useState } from "react"; import { addNewTaskToDatabase, getTasksFromDatabase, deleteTaskFromDatabase, updateTaskInDatabase } from "./actions"; type Task = { id: string; title: string; status: 'IN_PROGRESS' | 'COMPLETE'; }; export default function Home() { const [newTaskTitle, setNewTaskTitle] = useState(''); const [tasks, setTasks] = useState<Task[]>([]); const [editingTaskId, setEditingTaskId] = useState<string>(''); const [editedTaskTitle, setEditedTaskTitle] = useState(''); async function getTasks() { const updatedListOfTasks = await getTasksFromDatabase(); setTasks(updatedListOfTasks); } useEffect(() => { getTasks(); }, []); async function addTask() { await addNewTaskToDatabase(newTaskTitle); await getTasks(); setNewTaskTitle(''); } async function updateTask(task: Task, newTaskValues: Partial<Task>) { await updateTaskInDatabase({ ...task, ...newTaskValues }); await getTasks(); setEditingTaskId(''); // Clear editing state after update setEditedTaskTitle(''); } async function deleteTask(taskId: string) { await deleteTaskFromDatabase(taskId); await getTasks(); } const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); addTask(); }; const handleEditStart = (task: Task) => { setEditingTaskId(task.id); setEditedTaskTitle(task.title); }; const handleEditCancel = () => { setEditingTaskId(''); setEditedTaskTitle(''); }; return ( <main className="p-4"> <h2 className="text-2xl font-bold mb-4">To Do List</h2> <form onSubmit={handleSubmit} className="flex mb-8"> <input type="text" placeholder="New Task Title" value={newTaskTitle} onChange={(e) => setNewTaskTitle(e.target.value)} className="flex-grow border border-gray-400 rounded px-3 py-2 mr-2 bg-inherit" /> <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-nowrap" > Add New Task </button> </form> <table className="w-full"> <tbody> {tasks.map(task => { const isComplete = task.status === 'COMPLETE'; return ( <tr key={task.id} className="border-b border-gray-200"> <td className="py-2 px-4"> <input type="checkbox" checked={isComplete} onClick={() => updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })} className="transition-transform duration-300 ease-in-out transform scale-100 checked:scale-125 checked:bg-green-500" /> </td> <td className="py-2 px-4"> {editingTaskId === task.id ? ( <form onSubmit={(e) => { e.preventDefault(); updateTask(task, { title: editedTaskTitle }); }} className="flex" > <input type="text" value={editedTaskTitle} onChange={(e) => setEditedTaskTitle(e.target.value)} onBlur={() => handleEditCancel()} // Handle clicking outside input className="flex-grow border border-gray-400 rounded px-3 py-1 mr-2 bg-inherit" /> <button type="submit" className="bg-green-600 hover:bg-green-900 m-1 text-white font-bold py-2 px-4 rounded" > Save </button> </form> ) : ( <span onClick={() => handleEditStart(task)} className={`transition-all duration-300 ease-in-out cursor-pointer ${isComplete ? 'line-through text-gray-400 opacity-50' : 'opacity-100'}`} > {task.title} </span> )} </td> <td className="py-2 px-4"> <button onClick={() => deleteTask(task.id)} className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded float-right" > Delete </button> </td> </tr> ); })} </tbody> </table> </main> ); }
11. פריסה מחדש של האפליקציה ב-Cloud Run
- מריצים את הפקודה הבאה כדי לפרוס את האפליקציה ב-Cloud Run:
gcloud run deploy helloworld \ --region=us-central1 \ --source=. \ --set-env-vars DB_NAME="quickstart_db" \ --set-env-vars DB_USER="postgres" \ --set-env-vars DB_PASSWORD=${DB_PASSWORD} \ --set-env-vars DB_HOST="$(gcloud sql instances describe quickstart-instance --project=${GOOGLE_CLOUD_PROJECT} --format='value(settings.ipConfiguration.pscConfig.pscAutoConnections.ipAddress)')" \ --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \ --network=default \ --subnet=default \ --allow-unauthenticated
- אם מופיעה בקשה, מקישים על
Y
ו-Enter
כדי לאשר את המשך הפעולה:Do you want to continue (Y/n)? Y
12. מזל טוב
בשיעור ה-Lab הזה למדתם:
- יצירת מכונה של Cloud SQL ל-PostgreSQL (שמוגדרת לשימוש ב-Private Service Connect)
- פריסת אפליקציה ב-Cloud Run שמתחבר למסד הנתונים של Cloud SQL
- איך משתמשים ב-Gemini Code Assist כדי להוסיף פונקציונליות לאפליקציה
הסרת המשאבים
ל-Cloud SQL אין שכבה חינמית, ותתבקשו לשלם אם תמשיכו להשתמש בו. כדי להימנע מחיובים נוספים, אפשר למחוק את הפרויקט ב-Cloud.
אמנם אין חיוב ב-Cloud Run כשלא משתמשים בשירות, אבל יכול להיות שתחויבו על אחסון קובץ האימג' בקונטיינר ב-Artifact Registry. מחיקת הפרויקט ב-Cloud תפסיק את החיוב על כל המשאבים שבהם נעשה שימוש באותו פרויקט.
אם רוצים, מוחקים את הפרויקט:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
מומלץ גם למחוק משאבים לא נחוצים מהדיסק של cloudshell. אפשר:
- מוחקים את הספרייה של פרויקט ה-codelab:
rm -rf ~/task-app
- אזהרה! אי אפשר לבטל את הפעולה הבאה! אם רוצים למחוק את כל התוכן ב-Cloud Shell כדי לפנות מקום, אפשר למחוק את כל ספריית הבית. חשוב לוודא שכל מה שרוצים לשמור נשמר במקום אחר.
sudo rm -rf $HOME