פריסת אפליקציית Next.js מלאה ב-Cloud Run עם Firestore באמצעות Node.js Admin SDK

פריסת אפליקציית Next.js מלאה ב-Cloud Run עם Firestore באמצעות Node.js Admin SDK

מידע על Codelab זה

subjectהעדכון האחרון: אפר׳ 11, 2025
account_circleנכתב על ידי Luke Schlangen

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

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

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

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

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. אם מסוף ה-CLI לא מופיע בחלק התחתון של המסך, פותחים אותו:
    • לוחצים על תפריט שלושת הקווים סמל התפריט של שלושת הקווים.
    • לוחצים על 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 \
  firestore.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.‏ יצירת מסד נתונים ב-Firestore

  1. מריצים את הפקודה gcloud firestore databases create כדי ליצור מסד נתונים של Firestore
    gcloud firestore databases create --location=nam5

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

מכינים אפליקציית 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. מתקינים את firebase-admin כדי ליצור אינטראקציה עם מסד הנתונים של Firestore.
    npm install firebase-admin
  1. פותחים את הקובץ actions.ts ב-Cloud Shell Editor:
    cloudshell edit app/actions.ts
    עכשיו אמור להופיע קובץ ריק בחלק העליון של המסך. כאן אפשר לערוך את הקובץ actions.ts. הצגת הקוד הזה בחלק העליון של המסך
  2. מעתיקים את הקוד הבא ומדביקים אותו בקובץ actions.ts הפתוח:
    'use server'
    import { initializeApp, applicationDefault, getApps } from 'firebase-admin/app';
    import { getFirestore } from 'firebase-admin/firestore';
    const credential = applicationDefault();

    // Only initialize app if it does not already exist
    if (getApps().length === 0) {
      initializeApp({ credential });
    }

    const db = getFirestore();
    const tasksRef = db.collection('tasks');

    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };

    // CREATE
    export async function addNewTaskToDatabase(newTask: string) {
      await tasksRef.doc().create({
        title: newTask,
        status: 'IN_PROGRESS',
        createdAt: Date.now(),
      });
      return;
    }

    // READ
    export async function getTasksFromDatabase() {
      const snapshot = await tasksRef.orderBy('createdAt', 'desc').limit(100).get();
      const tasks = await snapshot.docs.map(doc => ({
        id: doc.id,
        title: doc.data().title,
        status: doc.data().status,
        createdAt: doc.data().createdAt,
      }));
      return tasks;
    }

    // UPDATE
    export async function updateTaskInDatabase(task: Task) {
      await tasksRef.doc(task.id).set(task);
      return;
    }

    // DELETE
    export async function deleteTaskFromDatabase(taskId: string) {
      await tasksRef.doc(taskId).delete();
      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>
      );
    }

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

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

  1. מריצים את הפקודה הבאה כדי לפרוס את האפליקציה ב-Cloud Run:
    gcloud run deploy helloworld \
      --region=us-central1 \
      --source=.
  2. אם מופיעה בקשה, מקישים על Y ו-Enter כדי לאשר את המשך הפעולה:
    Do you want to continue (Y/n)? Y
    

אחרי כמה דקות, האפליקציה אמורה לספק כתובת URL שאפשר להיכנס אליה.

עוברים לכתובת ה-URL כדי לראות את האפליקציה בפעולה. בכל פעם שתכנסו לכתובת ה-URL או תחדשו את הדף, תראו את אפליקציית המשימות.

9.‏ מזל טוב

בשיעור ה-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

המשך הלמידה