لمحة عن هذا الدرس التطبيقي حول الترميز
1. نظرة عامة
Cloud Run هي منصّة مُدارة بالكامل تتيح لك تشغيل الرمز البرمجي مباشرةً على بنية Google الأساسية القابلة للتوسّع. سيوضّح لك هذا الدرس التطبيقي كيفية ربط تطبيق Angular على Cloud Run بقاعدة بيانات Cloud SQL لقاعدة بيانات PostgreSQL باستخدام موصل Cloud SQL Node.js.
في هذه الميزة الاختبارية، ستتعرّف على كيفية تنفيذ ما يلي:
- إنشاء مثيل Cloud SQL for PostgreSQL
- نشر تطبيق على Cloud Run يتصل بقاعدة بيانات Cloud SQL
2. المتطلبات الأساسية
- إذا لم يكن لديك حساب على Google، عليك إنشاء حساب على Google.
- استخدام حساب شخصي بدلاً من حساب عمل أو حساب تديره مؤسسة تعليمية قد تكون هناك قيود على حسابات العمل والحسابات التي تديرها المؤسسات التعليمية تمنعك من تفعيل واجهات برمجة التطبيقات اللازمة لهذا الدرس التطبيقي.
3. إعداد المشروع
- سجِّل الدخول إلى Google Cloud Console.
- فعِّل الفوترة في Cloud Console.
- من المفترض أن تبلغ تكلفة إكمال هذا البرنامج التدريبي أقل من دولار أمريكي واحد في موارد Cloud.
- يمكنك اتّباع الخطوات الواردة في نهاية هذا البرنامج التدريبي لحذف الموارد لتجنُّب تحصيل المزيد من الرسوم.
- يكون المستخدمون الجدد مؤهّلين للاستفادة من فترة تجريبية مجانية بقيمة 300 دولار أمريكي.
- أنشئ مشروعًا جديدًا أو اختَر إعادة استخدام مشروع حالي.
4. فتح محرِّر Cloud Shell
- انتقِل إلى محرر Cloud Shell.
- إذا لم تظهر المحطة الطرفية في أسفل الشاشة، افتح المحطة باتّباع الخطوات التالية:
- انقر على قائمة الخطوط الثلاثة
.
- انقر على Terminal (الوحدة الطرفية).
- انقر على وحدة تحكّم جديدة
.
- انقر على قائمة الخطوط الثلاثة
- في الوحدة الطرفية، اضبط مشروعك باستخدام الأمر التالي:
- طبيعة الحضور:
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. تفعيل واجهات برمجة التطبيقات
في الوحدة الطرفية، فعِّل واجهات برمجة التطبيقات:
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 إلى حساب خدمة 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 على النحو التالي لإضافة دور كاتب السجلّ إلى حساب الخدمة على 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
- ثبِّت
pg
ومكتبة موصل Cloud SQL Node.js للتفاعل مع قاعدة بيانات PostgreSQL.npm install pg @google-cloud/cloud-sql-connector google-auth-library
- ثبِّت
@types/pg
كتبعية للمطوّرين لاستخدام تطبيق TypeScript Next.js.npm install --save-dev @types/pg
- افتح ملف
server.ts
في محرِّر Cloud Shell: من المفترض أن يظهر ملف الآن في أعلى الشاشة. يمكنك هنا تعديل ملف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: من المفترض أن يظهر الآن ملف حالي في الجزء العلوي من الشاشة. يمكنك هنا تعديل ملف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. تهانينا
في هذه الميزة الاختبارية، تعلمت كيفية تنفيذ ما يلي:
- إنشاء مثيل Cloud SQL for 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 for PostgreSQL باستخدام Cloud SQL Node.js Connector
- نشر تطبيق Angular متكامل على Cloud Run باستخدام Firestore باستخدام حزمة SDK للمشرف في Node.js
- نشر تطبيق Next.js متكامل على Cloud Run باستخدام Firestore باستخدام حزمة Admin SDK لـ Node.js