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

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

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

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

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

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

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

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

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

4. פתיחת Cloud Shell Editor

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

אם מתבקשים לאשר, לוחצים על אישור כדי להמשיך. לוחצים כדי לתת הרשאה ל-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 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 Run לא מחויבים כשלא משתמשים בשירות, אבל יכול להיות שתחויבו על אחסון קובץ האימג' של הקונטיינר ב-Artifact Registry. כשמוחקים פרויקט בענן, החיוב על כל המשאבים שנעשה בהם שימוש באותו פרויקט נפסק.

אם רוצים, אפשר למחוק את הפרויקט:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

אפשר גם למחוק משאבים מיותרים מהדיסק של Cloud Shell. אתם יכולים:

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

לומדים בכיף