1. نظرة عامة
Cloud Run هي منصة مُدارة بالكامل تتيح لك تشغيل الرموز البرمجية مباشرةً على بنية Google الأساسية القابلة للتوسّع. سيوضّح لك هذا الدرس التطبيقي كيفية ربط تطبيق Angular على Cloud Run بقاعدة بيانات Cloud SQL لـ PostgreSQL باستخدام Cloud SQL Node.js Connector.
في هذه الميزة الاختبارية، ستتعرّف على كيفية:
- إنشاء مثيل Cloud SQL for PostgreSQL
- نشر تطبيق على Cloud Run يتصل بقاعدة بيانات Cloud SQL
2. المتطلبات الأساسية
- إذا لم يكن لديك حساب على Google، عليك إنشاء حساب على Google.
- استخدام حساب شخصي بدلاً من حساب تديره المؤسسة التعليمية أو حساب تابع للعمل. قد تتضمّن حسابات العمل والحسابات المُدارة من المؤسسات التعليمية قيودًا تمنعك من تفعيل واجهات برمجة التطبيقات اللازمة لهذا الدرس التطبيقي.
3- إعداد المشروع
- سجِّل الدخول إلى Google Cloud Console.
- فعِّل الفوترة في Cloud Console.
- يجب أن تكلّف إكمال هذا المختبر أقل من دولار أمريكي واحد من موارد السحابة الإلكترونية.
- يمكنك اتّباع الخطوات في نهاية هذا المختبر لحذف الموارد وتجنُّب المزيد من الرسوم.
- يمكن للمستخدمين الجدد الاستفادة من فترة تجريبية مجانية بقيمة 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.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": من المفترض أن يظهر ملف حالي في الجزء العلوي من الشاشة. هذا هو المكان الذي يمكنك فيه تعديل ملف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. تهانينا
في هذه الميزة الاختبارية، تعرّفت على كيفية تنفيذ ما يلي:
- إنشاء مثيل Cloud SQL for PostgreSQL
- نشر تطبيق على Cloud Run يتصل بقاعدة بيانات Cloud SQL
تَنظيم
لا تتوفّر طبقة مجانية في Cloud SQL، وسيتم تحصيل رسوم منك إذا واصلت استخدامها. يمكنك حذف مشروعك على السحابة الإلكترونية لتجنُّب تحمّل رسوم إضافية.
على الرغم من أنّ Cloud Run لا تفرض رسومًا عندما لا تكون الخدمة قيد الاستخدام، قد يتم تحصيل رسوم منك مقابل تخزين صورة الحاوية في Artifact Registry. يؤدي حذف مشروعك على السحابة الإلكترونية إلى إيقاف الفوترة لجميع الموارد المستخدَمة في هذا المشروع.
إذا أردت، يمكنك حذف المشروع باتّباع الخطوات التالية:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
يمكنك أيضًا حذف الموارد غير الضرورية من قرص cloudshell. يمكنك إجراء ما يلي:
- احذف دليل مشروع الدرس البرمجي:
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 for PostgreSQL من خلال Cloud SQL Node.js Connector
- نشر تطبيق Angular كامل الميزات على Cloud Run مع Firestore باستخدام حزمة Node.js Admin SDK
- نشر تطبيق Next.js متكامل على Cloud Run باستخدام Firestore من خلال حزمة Node.js Admin SDK