ติดตั้งใช้งานแอปพลิเคชัน Next.js แบบ Full Stack ไปยัง Cloud Run ด้วย Firestore โดยใช้ Node.js Admin SDK

ติดตั้งใช้งานแอปพลิเคชัน Next.js แบบ Full Stack ไปยัง 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 โดยใช้ Node.js Admin SDK

ในบทแนะนำนี้ คุณจะได้เรียนรู้วิธีต่อไปนี้

  • สร้างฐานข้อมูล Firestore
  • ติดตั้งใช้งานแอปพลิเคชันใน Cloud Run ที่เชื่อมต่อกับฐานข้อมูล Firestore

2 ข้อกำหนดเบื้องต้น

  1. หากยังไม่มีบัญชี Google คุณต้องสร้างบัญชี Google
    • ใช้บัญชีส่วนตัวแทนบัญชีงานหรือบัญชีโรงเรียน บัญชีงานและบัญชีโรงเรียนอาจมีข้อจํากัดที่ทำให้คุณเปิดใช้ API ที่จําเป็นสําหรับห้องทดลองนี้ไม่ได้

3 การตั้งค่าโปรเจ็กต์

  1. ลงชื่อเข้าใช้คอนโซล Google Cloud
  2. เปิดใช้การเรียกเก็บเงินในคอนโซล Cloud
    • การทำแล็บนี้ให้เสร็จสมบูรณ์ควรใช้ทรัพยากรในระบบคลาวด์ไม่ถึง $1 USD
    • คุณสามารถทำตามขั้นตอนที่ส่วนท้ายของห้องทดลองนี้เพื่อลบทรัพยากรเพื่อหลีกเลี่ยงการเรียกเก็บเงินเพิ่มเติม
    • ผู้ใช้ใหม่มีสิทธิ์รับช่วงทดลองใช้ฟรีมูลค่า$300 USD
  3. สร้างโปรเจ็กต์ใหม่หรือเลือกนําโปรเจ็กต์ที่มีอยู่มาใช้ซ้ำ

4 เปิดเครื่องมือแก้ไข Cloud Shell

  1. ไปที่ Cloud Shell Editor
  2. หากเทอร์มินัลไม่ปรากฏที่ด้านล่างของหน้าจอ ให้เปิดเทอร์มินัลโดยทำดังนี้
    • คลิกเมนู 3 ขีด ไอคอนเมนู 3 ขีด
    • คลิก Terminal
    • คลิก New 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 เปิดใช้ 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.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
    

หลังจากผ่านไป 2-3 นาที แอปพลิเคชันควรแสดง URL ให้คุณเข้าชม

ไปที่ URL เพื่อดูการทำงานของแอปพลิเคชัน คุณจะเห็นแอปงานทุกครั้งที่เข้าชม URL หรือรีเฟรชหน้าเว็บ

9 ขอแสดงความยินดี

ในห้องทดลองนี้ คุณได้เรียนรู้วิธีดำเนินการต่อไปนี้

  • สร้างอินสแตนซ์ Cloud SQL สำหรับ PostgreSQL
  • ติดตั้งใช้งานแอปพลิเคชันใน Cloud Run ที่เชื่อมต่อกับฐานข้อมูล Cloud SQL

ล้างข้อมูล

Cloud SQL ไม่มีแพ็กเกจที่ไม่มีค่าใช้จ่ายและจะเรียกเก็บเงินจากคุณหากยังใช้งานต่อไป คุณลบโปรเจ็กต์ที่อยู่ในระบบคลาวด์เพื่อหลีกเลี่ยงการเรียกเก็บเงินเพิ่มเติมได้

แม้ว่า Cloud Run จะไม่เรียกเก็บเงินเมื่อไม่ได้ใช้งานบริการ แต่คุณอาจยังคงถูกเรียกเก็บเงินสำหรับการจัดเก็บอิมเมจคอนเทนเนอร์ใน Artifact Registry การลบโปรเจ็กต์ในระบบคลาวด์จะหยุดการเรียกเก็บเงินสำหรับทรัพยากรทั้งหมดที่ใช้ภายในโปรเจ็กต์นั้น

หากต้องการลบโปรเจ็กต์ ให้ทำดังนี้

gcloud projects delete $GOOGLE_CLOUD_PROJECT

คุณอาจต้องลบทรัพยากรที่ไม่จำเป็นออกจากดิสก์ CloudShell ด้วย ดังนี้

  1. ลบไดเรกทอรีโปรเจ็กต์ Codelab โดยทำดังนี้
    rm -rf ~/task-app
  2. คำเตือน! การดำเนินการถัดไปนี้จะยกเลิกไม่ได้ หากต้องการลบทุกอย่างใน Cloud Shell เพื่อเพิ่มพื้นที่ว่าง คุณสามารถลบไดเรกทอรีหน้าแรกทั้งหมด โปรดตรวจสอบว่าคุณได้บันทึกข้อมูลทั้งหมดที่ต้องการเก็บไว้ไว้ที่อื่นแล้ว
    sudo rm -rf $HOME

เรียนรู้ต่อไป