פריסת אפליקציית Next.js מלאה ב-Cloud Run עם Cloud SQL ל-PostgreSQL באמצעות המחבר של Cloud SQL ל-Node.js

פריסת אפליקציית Next.js מלאה ב-Cloud Run עם Cloud SQL ל-PostgreSQL באמצעות המחבר של Cloud SQL ל-Node.js

מידע על Codelab זה

subjectהעדכון האחרון: מרץ 28, 2025
account_circleנכתב על ידי Luke Schlangen

1.‏ סקירה כללית

Cloud Run היא פלטפורמה מנוהלת שמאפשרת להריץ את הקוד ישירות מעל התשתית הניתנת להתאמה של Google. ב-Codelab הזה נסביר איך לחבר אפליקציית Next.js ב-Cloud Run למסד נתונים של Cloud SQL ל-PostgreSQL באמצעות המחבר של Cloud SQL ל-Node.js.

בשיעור ה-Lab הזה תלמדו איך:

  • יצירת מכונות של Cloud SQL ל-PostgreSQL
  • פריסת אפליקציה ב-Cloud Run שמתחבר למסד הנתונים של Cloud SQL

2.‏ דרישות מוקדמות

  1. אם עדיין אין לכם חשבון Google, עליכם ליצור חשבון Google.
    • משתמשים בחשבון אישי במקום בחשבון לצורכי עבודה או בחשבון בית ספרי. יכול להיות שבחשבונות לצורכי עבודה ובחשבונות בית ספריים יש הגבלות שמונעות מכם להפעיל את ממשקי ה-API הנדרשים לסדנה הזו.

3.‏ הגדרת הפרויקט

  1. נכנסים למסוף Google Cloud.
  2. מפעילים את החיוב במסוף Cloud.
    • השלמת ה-Lab הזה אמורה לעלות פחות מ-1 $בארה"ב במשאבי Cloud.
    • כדי למנוע חיובים נוספים, תוכלו לפעול לפי השלבים שמפורטים בסוף שיעור ה-Lab הזה כדי למחוק את המשאבים.
    • משתמשים חדשים זכאים לתקופת ניסיון בחינם בשווי 300$‎.
  3. יוצרים פרויקט חדש או בוחרים לעשות שימוש חוזר בפרויקט קיים.

4.‏ פתיחת Cloud Shell Editor

  1. עוברים אל Cloud Shell Editor.
  2. אם מסוף ה-SSH לא מופיע בחלק התחתון של המסך, פותחים אותו:
    • לוחצים על תפריט שלושת הקווים סמל התפריט של שלושת הקווים.
    • לוחצים על Terminal (מסוף).
    • לוחצים על מסוף חדשפתיחת טרמינל חדש ב-Cloud Shell Editor
  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 Editor
  4. אם מתבקשים לאשר, לוחצים על Authorize (מתן הרשאה) כדי להמשיך. לוחצים כדי לאשר את Cloud Shell
  5. אמורה להופיע ההודעה הבאה:
    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 (מתן הרשאה) כדי להמשיך. לוחצים כדי לאשר את 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 באופן הבא כדי להוסיף את התפקיד לקוח 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"
  3. מריצים את הפקודה 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"
  4. כדי להוסיף את התפקיד כתיבה ביומן לחשבון השירות ב-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

  1. מריצים את הפקודה gcloud sql instances create כדי ליצור מכונה של Cloud SQL
    gcloud sql instances create quickstart-instance \
        --database-version=POSTGRES_14 \
        --cpu=4 \
        --memory=16GB \
        --region=us-central1 \
        --database-flags=cloudsql.iam_authentication=on

השלמת הפקודה הזו עשויה להימשך כמה דקות.

  1. מריצים את הפקודה gcloud sql databases create כדי ליצור מסד נתונים של Cloud SQL בתוך quickstart-instance.
    gcloud sql databases create quickstart_db \
       
    --instance=quickstart-instance
  2. יוצרים משתמש של מסד נתונים ב-PostgreSQL לחשבון השירות שיצרתם מקודם כדי לגשת למסד הנתונים.
    gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \
        --instance=quickstart-instance \
        --type=cloud_iam_service_account

8.‏ הכנת הבקשה

מכינים אפליקציית Next.js שמגיבה לבקשות HTTP.

  1. כדי ליצור פרויקט Next.js חדש בשם task-app, משתמשים בפקודה:
    npx --yes create-next-app@15.2.4 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
  2. עוברים לספרייה task-app:
    cd task-app
  1. כדי ליצור אינטראקציה עם מסד הנתונים של PostgreSQL, מתקינים את pg ואת ספריית המחבר של Cloud SQL ל-Node.js.
    npm install pg @google-cloud/cloud-sql-connector google-auth-library
  2. כדי להשתמש באפליקציית TypeScript Next.js, צריך להתקין את @types/pg כחבילת תלות לפיתוח.
    npm install --save-dev @types/pg
  1. פותחים את הקובץ actions.ts ב-Cloud Shell Editor:
    cloudshell edit app/actions.ts
    עכשיו אמור להופיע קובץ ריק בחלק העליון של המסך. כאן אפשר לערוך את הקובץ actions.ts. הצגת הקוד הזה בחלק העליון של המסך
  2. מעתיקים את הקוד הבא ומדביקים אותו בקובץ actions.ts הפתוח:
    'use server'
    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';
    };

    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)
       
    );`);
    }

    // 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`);
     
    return rows;
    }

    // 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;
    }
  1. פותחים את הקובץ page.tsx ב-Cloud Shell Editor:
    cloudshell edit app/page.tsx
    עכשיו אמור להופיע קובץ קיים בחלק העליון של המסך. כאן אפשר לערוך את הקובץ page.tsx. הצגת הקוד הזה בחלק העליון של המסך
  2. מוחקים את התוכן הקיים בקובץ page.tsx.
  3. מעתיקים את הקוד הבא ומדביקים אותו בקובץ 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';
     
    createdAt: number;
    };

    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 handleSubmit(e: React.FormEvent<HTMLFormElement>) {
       
    e.preventDefault();
       
    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}
                       
    onChange={() => 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 float-right"
                      >
                       
    Delete
                      <
    /button>
                   
    </td>
                  <
    /tr>
               
    );
             
    })}
           
    </tbody>
          <
    /table>
       
    </main>
     
    );
    }

האפליקציה מוכנה עכשיו לפריסה.

9.‏ פריסת האפליקציה ב-Cloud Run

  1. מריצים את הפקודה הבאה כדי לפרוס את האפליקציה ב-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
  2. אם מופיעה בקשה, מקישים על 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. אפשר:

  1. מוחקים את הספרייה של פרויקט ה-codelab:
    rm -rf ~/task-app
  2. אזהרה! אי אפשר לבטל את הפעולה הבאה! אם רוצים למחוק את כל התוכן ב-Cloud Shell כדי לפנות מקום, אפשר למחוק את כל ספריית הבית. חשוב לוודא שכל מה שרוצים לשמור נשמר במקום אחר.
    sudo rm -rf $HOME