نشر تطبيق JavaScript متكامل على Cloud Run باستخدام Cloud SQL for PostgreSQL

1. نظرة عامة

Cloud Run هي منصّة مُدارة بالكامل تتيح لك تشغيل الرمز البرمجي مباشرةً على بنية Google الأساسية القابلة للتوسّع. سيوضِّح هذا الدرس التطبيقي كيفية ربط تطبيق Next.js على Cloud Run بخدمة Cloud SQL لقاعدة بيانات PostgreSQL.

في هذه الميزة الاختبارية، ستتعرّف على كيفية تنفيذ ما يلي:

  • إنشاء مثيل Cloud SQL for PostgreSQL (تم ضبطه لاستخدام Private Service Connect)
  • نشر تطبيق على Cloud Run يتصل بقاعدة بيانات Cloud SQL
  • استخدام ميزة Gemini Code Assist لإضافة وظائف إلى تطبيقك

2. المتطلبات الأساسية

  1. إذا لم يكن لديك حساب على Google، عليك إنشاء حساب على Google.
    • استخدام حساب شخصي بدلاً من حساب عمل أو حساب تديره مؤسسة تعليمية قد تفرض حسابات العمل والحسابات التي تديرها المؤسسات التعليمية قيودًا تمنعك من تفعيل واجهات برمجة التطبيقات اللازمة لهذا الدرس التطبيقي.

3- إعداد المشروع

  1. سجِّل الدخول إلى Google Cloud Console.
  2. فعِّل الفوترة في Cloud Console.
    • من المفترض أن تبلغ تكلفة إكمال هذا البرنامج التدريبي أقل من دولار أمريكي واحد في موارد Cloud.
    • يمكنك اتّباع الخطوات الواردة في نهاية هذا البرنامج التدريبي لحذف الموارد لتجنُّب تحصيل المزيد من الرسوم.
    • يكون المستخدمون الجدد مؤهّلين للاستفادة من الفترة التجريبية المجانية التي تبلغ قيمتها 300 دولار أمريكي.
  3. أنشئ مشروعًا جديدًا أو اختَر إعادة استخدام مشروع حالي.

4. فتح محرِّر Cloud Shell

  1. انتقِل إلى محرِّر Cloud Shell.
  2. إذا لم تظهر المحطة الطرفية في أسفل الشاشة، افتح المحطة الطرفية باتّباع الخطوات التالية:
    • انقر على قائمة الخطوط الثلاثة رمز قائمة الهمبرغر.
    • انقر على Terminal (الوحدة الطرفية).
    • انقر على وحدة تحكّم جديدةفتح وحدة طرفية جديدة في محرِّر Cloud Shell.
  3. في الوحدة الطرفية، اضبط مشروعك باستخدام الأمر التالي:
    • طبيعة الحضور:
      gcloud config set project [PROJECT_ID]
      
    • مثال:
      gcloud config set project lab-project-id-example
      
    • إذا لم تتذكر رقم تعريف المشروع:
      • يمكنك إدراج جميع أرقام تعريف مشاريعك باستخدام:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      ضبط رقم تعريف المشروع في محطة محرِّر Cloud Shell
  4. إذا طُلب منك تفويض، انقر على تفويض للمتابعة. انقر على تفويض Cloud Shell.
  5. من المفترض أن تظهر لك هذه الرسالة:
    Updated property [core/project].
    
    إذا ظهر لك رمز WARNING وتلقّيت رسالة Do you want to continue (Y/N)?، هذا يعني على الأرجح أنّك أدخلت رقم تعريف المشروع بشكل غير صحيح. اضغط على N، ثم اضغط على Enter، وحاول تنفيذ الأمر gcloud config set project مرة أخرى.

5- تفعيل واجهات برمجة التطبيقات

في الوحدة الطرفية، فعِّل واجهات برمجة التطبيقات:

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

إذا طُلب منك تفويض، انقر على تفويض للمتابعة. انقر على تفويض Cloud Shell.

قد يستغرق تنفيذ هذا الأمر بضع دقائق، ولكن من المفترض أن تظهر في النهاية رسالة تفيد بنجاح العملية، مثل هذه الرسالة:

Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.

6- إعداد حساب خدمة

أنشئ حساب خدمة على Google Cloud واضبطه لكي يستخدمه Cloud Run كي يحصل على الأذونات الصحيحة للاتصال بخدمة Cloud SQL.

  1. شغِّل الأمر gcloud iam service-accounts create على النحو التالي لإنشاء حساب خدمة جديد:
    gcloud iam service-accounts create quickstart-service-account \
      --display-name="Quickstart Service Account"
    
  2. شغِّل الأمر 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

  1. أنشئ سياسة اتصال خدمة للسماح بإمكانية الاتصال بالشبكة من 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
    
  2. إنشاء كلمة مرور فريدة لقاعدة بياناتك
    export DB_PASSWORD=$(openssl rand -base64 20)
    
  3. تنفيذ الأمر gcloud sql instances create لإنشاء مثيل Cloud SQL
    gcloud 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
    

قد يستغرق تنفيذ هذا الأمر بضع دقائق.

  1. نفِّذ الأمر gcloud sql databases create لإنشاء قاعدة بيانات Cloud SQL ضمن quickstart-instance.
    gcloud sql databases create quickstart_db \
      --instance=quickstart-instance
    

8. تحضير الطلب

حضِّر تطبيق Next.js يستجيب لطلبات HTTP.

  1. لإنشاء مشروع Next.js جديد باسم task-app، استخدِم الأمر التالي:
    npx create-next-app@15.0.3 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. إذا طُلب منك تثبيت create-next-app، اضغط على Enter للمتابعة:
    Need to install the following packages:
    create-next-app@15.0.3
    Ok to proceed? (y)
    
  3. تغيير الدليل إلى task-app:
    cd task-app
    
  4. ثبِّت pg للتفاعل مع قاعدة بيانات PostgreSQL.
    npm install pg
    
  5. ثبِّت @types/pg كتبعية للمطوّرين لاستخدام تطبيق TypeScript Next.js.
    npm install --save-dev @types/pg
    
  6. أنشئ ملف actions.ts.
    touch app/actions.ts
    
  7. افتح ملف actions.ts في محرِّر Cloud Shell:
    cloudshell edit app/actions.ts
    
    من المفترض أن يظهر الآن ملف فارغ في الجزء العلوي من الشاشة. يمكنك هنا تعديل ملف actions.ts هذا. يُرجى إظهار أنّ الرمز يظهر في أعلى الشاشة.
  8. انسخ الرمز التالي والصقه في ملف 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;
    }
    
  9. افتح ملف page.tsx في محرِّر Cloud Shell:
    cloudshell edit app/page.tsx
    
    من المفترض أن يظهر الآن ملف حالي في الجزء العلوي من الشاشة. يمكنك هنا تعديل ملف page.tsx هذا. يُرجى إظهار أنّ الرمز يظهر في أعلى الشاشة.
  10. احذف المحتوى الحالي من ملف page.tsx.
  11. انسخ الرمز التالي والصقه في ملف 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

  1. نفِّذ الأمر gcloud projects add-iam-policy-binding على النحو التالي لإضافة دور مستخدم الشبكة إلى حساب خدمة Cloud Run لخدمة Cloud Run التي أنت على وشك إنشاؤها.
    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"
    
  1. نفِّذ الأمر أدناه لنشر تطبيقك على 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
    
  2. اضغط على Y وEnter لتأكيد رغبتك في المتابعة إذا طُلب منك ذلك:
    Do you want to continue (Y/n)? Y
    

بعد بضع دقائق، من المفترض أن يقدّم التطبيق عنوان URL يمكنك الانتقال إليه.

انتقِل إلى عنوان URL للاطّلاع على تطبيقك أثناء تنفيذه. سيظهر لك تطبيق المهام في كل مرة تزور فيها عنوان URL أو تُعيد فيها تحميل الصفحة.

10. إضافة ميزة باستخدام ميزة Gemini Code Assist

لقد نجحت الآن في نشر تطبيق ويب يتضمّن قاعدة بيانات. بعد ذلك، سنضيف ميزة جديدة إلى تطبيق next.js باستخدام تكنولوجيات الذكاء الاصطناعي.

  1. الرجوع إلى محرِّر Cloud Shell
  2. فتح page.tsx مرة أخرى
    cd ~/task-app
    cloudshell edit app/page.tsx
    
  3. انتقِل إلى ميزة Gemini Code Assist في محرِّر Cloud Shell:
    • انقر على رمز Gemini رمز Gemini Code Assist في شريط الأدوات على يمين الشاشة.
    • سجِّل الدخول باستخدام بيانات اعتماد حسابك على Google إذا طُلب منك ذلك.
    • إذا طُلب منك اختيار مشروع، اختَر المشروع الذي أنشأته لهذا الدليل التعليمي. اختيار مشروع Gemini
  4. أدخِل الطلب: 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>
    
  5. استبدِل 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

  1. نفِّذ الأمر أدناه لنشر تطبيقك على 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
    
  2. اضغط على Y وEnter لتأكيد رغبتك في المتابعة إذا طُلب منك ذلك:
    Do you want to continue (Y/n)? Y
    

12. تهانينا

في هذه الميزة الاختبارية، تعلمت كيفية تنفيذ ما يلي:

  • إنشاء مثيل Cloud SQL for 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. يمكنك إجراء ما يلي:

  1. احذف دليل مشروع Codelab:
    rm -rf ~/task-app
    
  2. تحذير! لا يمكن التراجع عن هذا الإجراء التالي. إذا أردت حذف كل المحتوى على Cloud Shell لإخلاء بعض المساحة، يمكنك حذف الدليل الرئيسي كاملاً. احرص على حفظ كل ما تريد الاحتفاظ به في مكان آخر.
    sudo rm -rf $HOME