1. סקירה כללית
Cloud Run היא פלטפורמה מנוהלת באופן מלא שמאפשרת להריץ את הקוד ישירות על גבי התשתית הניתנת להתאמה של Google. ב-Codelab הזה נראה איך לחבר אפליקציית Angular ב-Cloud Run למסד נתונים של Cloud SQL ל-PostgreSQL באמצעות Cloud SQL Node.js Connector.
בשיעור ה-Lab הזה תלמדו איך:
- יצירת מכונות של Cloud SQL ל-PostgreSQL
- פריסת אפליקציה ב-Cloud Run שמתחברת למסד הנתונים של Cloud SQL
2. דרישות מוקדמות
- אם אין לכם חשבון Google, אתם צריכים ליצור חשבון Google.
- משתמשים בחשבון לשימוש אישי במקום בחשבון לצורכי עבודה או בחשבון בית ספרי. יכול להיות שבחשבונות לצורכי עבודה או בחשבונות בית ספריים יש הגבלות שימנעו מכם להפעיל את ממשקי ה-API שנדרשים למעבדה הזו.
3. הגדרת הפרויקט
- נכנסים ל-מסוף Google Cloud.
- מפעילים את החיוב במסוף Cloud.
- העלות של השלמת ה-Lab הזה במשאבי Cloud צריכה להיות פחות מ-1$.
- כדי למחוק משאבים ולמנוע חיובים נוספים, אפשר לבצע את השלבים בסוף ה-Lab הזה.
- משתמשים חדשים זכאים לתקופת ניסיון בחינם בשווי 300$.
- יוצרים פרויקט חדש או בוחרים להשתמש מחדש בפרויקט קיים.
4. פתיחת Cloud Shell Editor
- עוברים אל Cloud Shell Editor.
- אם הטרמינל לא מופיע בתחתית המסך, פותחים אותו:
- לוחצים על סמל האפשרויות הנוספות (3 קווים)
. - לוחצים על Terminal (מסוף).
- לוחצים על New Terminal (טרמינל חדש)
.
- לוחצים על סמל האפשרויות הנוספות (3 קווים)
- בטרמינל, מגדירים את הפרויקט באמצעות הפקודה הבאה:
- פורמט:
gcloud config set project [PROJECT_ID] - דוגמה:
gcloud config set project lab-project-id-example - אם אתם לא זוכרים את מזהה הפרויקט:
- כדי לראות את כל מזהי הפרויקטים, מריצים את הפקודה:
gcloud projects list | awk '/PROJECT_ID/{print $2}'

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

- תוצג ההודעה הבאה:
אם מופיעUpdated property [core/project].
WARNINGומוצגת השאלהDo you want to continue (Y/N)?, כנראה שהזנתם את מזהה הפרויקט בצורה שגויה. לוחצים עלN, לוחצים עלEnterומנסים להריץ שוב את הפקודהgcloud config set project.
5. הפעלת ממשקי ה-API
בטרמינל, מפעילים את ממשקי ה-API:
gcloud services enable \
sqladmin.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com
אם מתבקשים לאשר, לוחצים על אישור כדי להמשיך. 
השלמת הפקודה עשויה להימשך כמה דקות, אבל בסופו של דבר אמורה להתקבל הודעה על הצלחה שדומה להודעה הבאה:
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" - מריצים את הפקודה gcloud projects add-iam-policy-binding באופן הבא כדי להוסיף את התפקיד Cloud SQL Client לחשבון השירות של Google Cloud שיצרתם.
gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \ --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \ --role="roles/cloudsql.client" - מריצים את הפקודה gcloud projects add-iam-policy-binding באופן הבא כדי להוסיף את התפקיד משתמש במכונת Cloud SQL לחשבון השירות של Google Cloud שיצרתם.
gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \ --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \ --role="roles/cloudsql.instanceUser" - מריצים את הפקודה gcloud projects add-iam-policy-binding באופן הבא כדי להוסיף את התפקיד Log Writer לחשבון השירות ב-Google Cloud שיצרתם.
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
- מריצים את הפקודה
gcloud sql instances createכדי ליצור מכונה של Cloud SQLgcloud sql instances create quickstart-instance \ --database-version=POSTGRES_14 \ --cpu=4 \ --memory=16GB \ --region=us-central1 \ --database-flags=cloudsql.iam_authentication=on
יכול להיות שיעברו כמה דקות עד שהפקודה הזו תסתיים.
- מריצים את הפקודה
gcloud sql databases createכדי ליצור מסד נתונים ב-Cloud SQL בתוךquickstart-instance.gcloud sql databases create quickstart_db \ --instance=quickstart-instance - יוצרים משתמש במסד נתונים של PostgreSQL עבור חשבון השירות שיצרתם קודם כדי לגשת למסד הנתונים.
gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \ --instance=quickstart-instance \ --type=cloud_iam_service_account
8. הכנת הבקשה
מכינים אפליקציית Next.js שמגיבה לבקשות HTTP.
- כדי ליצור פרויקט חדש של Next.js בשם
task-app, משתמשים בפקודה:npx --yes @angular/cli@19.2.5 new task-app \ --minimal \ --inline-template \ --inline-style \ --ssr \ --server-routing \ --defaults - שינוי הספרייה ל-
task-app:cd task-app
- כדי ליצור אינטראקציה עם מסד הנתונים של PostgreSQL, צריך להתקין את
pgואת ספריית המחברים של Cloud SQL Node.js.npm install pg @google-cloud/cloud-sql-connector google-auth-library - כדי להשתמש באפליקציית TypeScript Next.js, צריך להתקין את
@types/pgכ-dev dependency.npm install --save-dev @types/pg
- פותחים את הקובץ
server.tsב-Cloud Shell Editor: קובץ אמור להופיע בחלק העליון של המסך. כאן אפשר לערוך את הקובץcloudshell edit src/server.tsserver.ts.
- מוחקים את התוכן הקיים בקובץ
server.ts. - מעתיקים את הקוד הבא ומדביקים אותו בקובץ
server.tsשפתחתם:import { AngularNodeAppEngine, createNodeRequestHandler, isMainModule, writeResponseToNodeResponse, } from '@angular/ssr/node'; import express from 'express'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import pg from 'pg'; import { AuthTypes, Connector } from '@google-cloud/cloud-sql-connector'; import { GoogleAuth } from 'google-auth-library'; const auth = new GoogleAuth(); const { Pool } = pg; type Task = { id: string; title: string; status: 'IN_PROGRESS' | 'COMPLETE'; createdAt: number; }; const projectId = await auth.getProjectId(); const connector = new Connector(); const clientOpts = await connector.getOptions({ instanceConnectionName: `${projectId}:us-central1:quickstart-instance`, authType: AuthTypes.IAM, }); const pool = new Pool({ ...clientOpts, user: `quickstart-service-account@${projectId}.iam`, database: 'quickstart_db', }); const tableCreationIfDoesNotExist = async () => { 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) );`); } const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const browserDistFolder = resolve(serverDistFolder, '../browser'); const app = express(); const angularApp = new AngularNodeAppEngine(); app.use(express.json()); app.get('/api/tasks', async (req, res) => { await tableCreationIfDoesNotExist(); const { rows } = await pool.query(`SELECT id, created_at, status, title FROM tasks ORDER BY created_at DESC LIMIT 100`); res.send(rows); }); app.post('/api/tasks', async (req, res) => { const newTaskTitle = req.body.title; if (!newTaskTitle) { res.status(400).send("Title is required"); return; } await tableCreationIfDoesNotExist(); await pool.query(`INSERT INTO tasks(created_at, status, title) VALUES(NOW(), 'IN_PROGRESS', $1)`, [newTaskTitle]); res.sendStatus(200); }); app.put('/api/tasks', async (req, res) => { const task: Task = req.body; if (!task || !task.id || !task.title || !task.status) { res.status(400).send("Invalid task data"); return; } await tableCreationIfDoesNotExist(); await pool.query( `UPDATE tasks SET status = $1, title = $2 WHERE id = $3`, [task.status, task.title, task.id] ); res.sendStatus(200); }); app.delete('/api/tasks', async (req, res) => { const task: Task = req.body; if (!task || !task.id) { res.status(400).send("Task ID is required"); return; } await tableCreationIfDoesNotExist(); await pool.query(`DELETE FROM tasks WHERE id = $1`, [task.id]); res.sendStatus(200); }); /** * Serve static files from /browser */ app.use( express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false, }), ); /** * Handle all other requests by rendering the Angular application. */ app.use('/**', (req, res, next) => { angularApp .handle(req) .then((response) => response ? writeResponseToNodeResponse(response, res) : next(), ) .catch(next); }); /** * Start the server if this module is the main entry point. * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. */ if (isMainModule(import.meta.url)) { const port = process.env['PORT'] || 4000; app.listen(port, () => { console.log(`Node Express server listening on http://localhost:${port}`); }); } /** * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions. */ export const reqHandler = createNodeRequestHandler(app);
- פותחים את הקובץ
app.component.tsב-Cloud Shell Editor: קובץ קיים אמור להופיע בחלק העליון של המסך. כאן אפשר לערוך את הקובץcloudshell edit src/app/app.component.tsapp.component.ts.
- מוחקים את התוכן הקיים בקובץ
app.component.ts. - מעתיקים את הקוד הבא ומדביקים אותו בקובץ
app.component.tsשפתחתם:import { afterNextRender, Component, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; type Task = { id: string; title: string; status: 'IN_PROGRESS' | 'COMPLETE'; createdAt: number; }; @Component({ selector: 'app-root', standalone: true, imports: [FormsModule], template: ` <section> <input type="text" placeholder="New Task Title" [(ngModel)]="newTaskTitle" class="text-black border-2 p-2 m-2 rounded" /> <button (click)="addTask()">Add new task</button> <table> <tbody> @for (task of tasks(); track task) { @let isComplete = task.status === 'COMPLETE'; <tr> <td> <input (click)="updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })" type="checkbox" [checked]="isComplete" /> </td> <td>{{ task.title }}</td> <td>{{ task.status }}</td> <td> <button (click)="deleteTask(task)">Delete</button> </td> </tr> } </tbody> </table> </section> `, styles: '', }) export class AppComponent { newTaskTitle = ''; tasks = signal<Task[]>([]); constructor() { afterNextRender({ earlyRead: () => this.getTasks() }); } async getTasks() { const response = await fetch(`/api/tasks`); const tasks = await response.json(); this.tasks.set(tasks); } async addTask() { await fetch(`/api/tasks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: this.newTaskTitle, status: 'IN_PROGRESS', createdAt: Date.now(), }), }); this.newTaskTitle = ''; await this.getTasks(); } async updateTask(task: Task, newTaskValues: Partial<Task>) { await fetch(`/api/tasks`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...task, ...newTaskValues }), }); await this.getTasks(); } async deleteTask(task: any) { await fetch('/api/tasks', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task), }); await this.getTasks(); } }
האפליקציה מוכנה עכשיו לפריסה.
9. פריסת האפליקציה ב-Cloud Run
- מריצים את הפקודה הבאה כדי לפרוס את האפליקציה ב-Cloud Run:
gcloud run deploy to-do-tracker \ --region=us-central1 \ --source=. \ --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \ --allow-unauthenticated - אם מוצגת הנחיה, מקישים על
YועלEnterכדי לאשר שרוצים להמשיך:Do you want to continue (Y/n)? Y
אחרי כמה דקות, האפליקציה אמורה לספק כתובת URL שאפשר להיכנס אליה.
עוברים לכתובת ה-URL כדי לראות את האפליקציה בפעולה. בכל פעם שתבקרו בכתובת ה-URL או תרעננו את הדף, תראו את אפליקציית המשימות.
10. מזל טוב
בשיעור ה-Lab הזה למדתם איך:
- יצירת מכונות של Cloud SQL ל-PostgreSQL
- פריסת אפליקציה ב-Cloud Run שמתחברת למסד הנתונים של Cloud SQL
הסרת המשאבים
ל-Cloud SQL אין רמת שירות בחינם, ותחויבו אם תמשיכו להשתמש בו. כדי להימנע מחיובים נוספים, אפשר למחוק את פרויקט בענן.
ב-Cloud Run לא מחויבים כשלא משתמשים בשירות, אבל יכול להיות שתחויבו על אחסון קובץ האימג' של הקונטיינר ב-Artifact Registry. כשמוחקים פרויקט בענן, החיוב על כל המשאבים שנעשה בהם שימוש באותו פרויקט נפסק.
אם רוצים, אפשר למחוק את הפרויקט:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
אפשר גם למחוק משאבים מיותרים מהדיסק של Cloud Shell. אתם יכולים:
- מוחקים את ספריית הפרויקט של ה-codelab:
rm -rf ~/task-app - אזהרה! אי אפשר לבטל את הפעולה הבאה! אם רוצים למחוק את כל מה שיש ב-Cloud Shell כדי לפנות מקום, אפשר למחוק את כל ספריית הבית. חשוב לוודא שכל מה שרוצים לשמור נשמר במקום אחר.
sudo rm -rf $HOME
לומדים בכיף
- פריסת אפליקציית Next.js מלאה ב-Cloud Run עם Cloud SQL ל-PostgreSQL באמצעות Cloud SQL Node.js Connector
- פריסת אפליקציית Angular מלאה ב-Cloud Run עם Cloud SQL ל-PostgreSQL באמצעות Cloud SQL Node.js Connector
- פריסת אפליקציית Angular מלאה ב-Cloud Run עם Firestore באמצעות Node.js Admin SDK
- פריסת אפליקציית Next.js מלאה ב-Cloud Run עם Firestore באמצעות Node.js Admin SDK