מידע על Codelab זה
1. סקירה כללית
Cloud Run היא פלטפורמה מנוהלת שמאפשרת להריץ את הקוד ישירות מעל התשתית הניתנת להתאמה של Google. ב-Codelab הזה נסביר איך לחבר אפליקציית Angular ב-Cloud Run למסד נתונים של Cloud SQL ל-PostgreSQL באמצעות המחבר של Cloud SQL ל-Node.js.
בשיעור ה-Lab הזה תלמדו איך:
- יצירת מכונות של Cloud SQL ל-PostgreSQL
- פריסת אפליקציה ב-Cloud Run שמתחבר למסד הנתונים של Cloud SQL
2. דרישות מוקדמות
- אם עדיין אין לכם חשבון Google, עליכם ליצור חשבון Google.
- משתמשים בחשבון אישי במקום בחשבון לצורכי עבודה או בחשבון בית ספרי. יכול להיות שבחשבונות לצורכי עבודה וחשבונות בית ספריים יש הגבלות שמונעות מכם להפעיל את ממשקי ה-API הנדרשים לסדנה הזו.
3. הגדרת הפרויקט
- נכנסים למסוף Google Cloud.
- מפעילים את החיוב במסוף Cloud.
- השלמת ה-Lab הזה אמורה לעלות פחות מ-1 $בארה"ב במשאבי Cloud.
- כדי למנוע חיובים נוספים, תוכלו לפעול לפי השלבים שמפורטים בסוף שיעור ה-Lab הזה כדי למחוק את המשאבים.
- משתמשים חדשים זכאים לתקופת ניסיון בחינם בשווי 300$.
- יוצרים פרויקט חדש או בוחרים לעשות שימוש חוזר בפרויקט קיים.
4. פתיחת Cloud Shell Editor
- עוברים אל Cloud Shell Editor.
- אם מסוף ה-CLI לא מופיע בחלק התחתון של המסך, פותחים אותו:
- לוחצים על תפריט שלושת הקווים
.
- לוחצים על 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 \
sqladmin.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.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" - מריצים את הפקודה 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.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" - כדי להוסיף את התפקיד כתיבה ביומן לחשבון השירות ב-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
- מריצים את הפקודה
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
כחבילת תלות לפיתוח.npm install --save-dev @types/pg
- פותחים את הקובץ
server.ts
ב-Cloud Shell Editor: עכשיו אמור להופיע קובץ בחלק העליון של המסך. כאן אפשר לערוך את הקובץcloudshell edit src/server.ts
server.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.ts
app.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.
אמנם אין חיוב ב-Cloud Run כשלא משתמשים בשירות, אבל יכול להיות שתחויבו על אחסון קובץ האימג' בקונטיינר ב-Artifact Registry. מחיקת הפרויקט ב-Cloud תפסיק את החיוב על כל המשאבים שבהם נעשה שימוש באותו פרויקט.
אם רוצים, מוחקים את הפרויקט:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
מומלץ גם למחוק משאבים לא נחוצים מהדיסק של cloudshell. אפשר:
- מוחקים את הספרייה של פרויקט ה-codelab:
rm -rf ~/task-app
- אזהרה! אי אפשר לבטל את הפעולה הבאה! אם רוצים למחוק את כל התוכן ב-Cloud Shell כדי לפנות מקום, אפשר למחוק את כל ספריית הבית. חשוב לוודא שכל מה שרוצים לשמור נשמר במקום אחר.
sudo rm -rf $HOME
המשך הלמידה
- פריסת אפליקציית Next.js מלאה ב-Cloud Run עם Cloud SQL ל-PostgreSQL באמצעות המחבר של Cloud SQL ל-Node.js
- פריסת אפליקציית Angular בסטראק מלא ב-Cloud Run באמצעות Cloud SQL ל-PostgreSQL באמצעות המחבר של Cloud SQL ל-Node.js
- פריסת אפליקציית Angular מלאה ב-Cloud Run עם Firestore באמצעות Node.js Admin SDK
- פריסה של אפליקציית Next.js בסביבת סטאק מלאה ב-Cloud Run עם Firestore באמצעות Node.js Admin SDK