Triển khai ứng dụng JavaScript toàn bộ ngăn xếp lên Cloud Run bằng Cloud SQL cho PostgreSQL

1. Tổng quan

Cloud Run là một nền tảng được quản lý hoàn toàn, cho phép bạn chạy mã trực tiếp trên cơ sở hạ tầng có thể mở rộng của Google. Lớp học lập trình này sẽ minh hoạ cách kết nối một ứng dụng Next.js trên Cloud Run với cơ sở dữ liệu Cloud SQL cho PostgreSQL.

Trong phòng thí nghiệm này, bạn sẽ tìm hiểu cách:

  • Tạo một phiên bản Cloud SQL cho PostgreSQL
  • Triển khai một ứng dụng lên Cloud Run để kết nối với cơ sở dữ liệu Cloud SQL

2. Điều kiện tiên quyết

  1. Nếu chưa có Tài khoản Google, bạn phải tạo một Tài khoản Google.
    • Sử dụng tài khoản cá nhân thay vì tài khoản do nơi làm việc hoặc trường học cấp. Tài khoản công việc và tài khoản trường học có thể có những hạn chế khiến bạn không bật được các API cần thiết cho lớp học này.

3. Thiết lập dự án

  1. Đăng nhập vào Google Cloud Console.
  2. Bật tính năng thanh toán trong Cloud Console.
    • Việc hoàn thành bài thực hành này sẽ tốn ít hơn 1 USD cho các tài nguyên trên đám mây.
    • Bạn có thể làm theo các bước ở cuối bài thực hành này để xoá tài nguyên nhằm tránh bị tính thêm phí.
    • Người dùng mới sẽ đủ điều kiện dùng thử miễn phí 300 USD.
  3. Tạo một dự án mới hoặc chọn sử dụng lại một dự án hiện có.

4. Mở Trình chỉnh sửa Cloud Shell

  1. Chuyển đến Trình chỉnh sửa Cloud Shell
  2. Nếu thiết bị đầu cuối không xuất hiện ở cuối màn hình, hãy mở thiết bị đầu cuối:
    • Nhấp vào trình đơn có biểu tượng ba dấu gạch ngang Biểu tượng trình đơn ba đường kẻ
    • Nhấp vào Terminal (Thiết bị đầu cuối)
    • Nhấp vào New Terminal (Thiết bị đầu cuối mới) Mở cửa sổ dòng lệnh mới trong Trình chỉnh sửa Cloud Shell
  3. Trong thiết bị đầu cuối, hãy thiết lập dự án bằng lệnh sau:
    • Định dạng:
      gcloud config set project [PROJECT_ID]
      
    • Ví dụ:
      gcloud config set project lab-project-id-example
      
    • Nếu bạn không nhớ mã dự án của mình, hãy làm như sau:
      • Bạn có thể liệt kê tất cả mã dự án bằng cách dùng:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Đặt mã dự án trong thiết bị đầu cuối Cloud Shell Editor
  4. Nếu được nhắc uỷ quyền, hãy nhấp vào Uỷ quyền để tiếp tục. Nhấp để uỷ quyền cho Cloud Shell
  5. Bạn sẽ thấy thông báo sau:
    Updated property [core/project].
    
    Nếu thấy biểu tượng WARNING và được yêu cầu Do you want to continue (Y/N)?, thì có thể bạn đã nhập sai mã dự án. Nhấn N, nhấn Enter rồi thử chạy lại lệnh gcloud config set project.

5. Bật API

Trong dòng lệnh, hãy bật các API:

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

Nếu được nhắc uỷ quyền, hãy nhấp vào Uỷ quyền để tiếp tục. Nhấp để uỷ quyền cho Cloud Shell

Lệnh này có thể mất vài phút để hoàn tất, nhưng cuối cùng sẽ tạo ra một thông báo thành công tương tự như thông báo này:

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

6. Thiết lập Tài khoản dịch vụ

Tạo và định cấu hình một tài khoản dịch vụ Google Cloud mà Cloud Run sẽ sử dụng để tài khoản đó có các quyền phù hợp để kết nối với Cloud SQL.

  1. Chạy lệnh gcloud iam service-accounts create như sau để tạo tài khoản dịch vụ mới:
    gcloud iam service-accounts create quickstart-service-account \
      --display-name="Quickstart Service Account"
    
  2. Chạy lệnh gcloud projects add-iam-policy-binding như sau để thêm vai trò Log Writer (Người ghi nhật ký) vào tài khoản dịch vụ Google Cloud mà bạn vừa tạo.
    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. Tạo cơ sở dữ liệu Cloud SQL

  1. Tạo Chính sách kết nối dịch vụ để cho phép kết nối mạng từ Cloud Run đến Cloud SQL bằng 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. Tạo một mật khẩu duy nhất cho cơ sở dữ liệu của bạn
    export DB_PASSWORD=$(openssl rand -base64 20)
    
  3. Chạy lệnh gcloud sql instances create để tạo một phiên bản 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
    

Lệnh này có thể mất vài phút để hoàn tất.

  1. Chạy lệnh gcloud sql databases create để tạo cơ sở dữ liệu Cloud SQL trong quickstart-instance.
    gcloud sql databases create quickstart_db \
      --instance=quickstart-instance
    

8. Chuẩn bị đơn đăng ký

Chuẩn bị một ứng dụng Next.js phản hồi các yêu cầu HTTP.

  1. Để tạo một dự án Next.js mới có tên là task-app, hãy dùng lệnh:
    npx --yes create-next-app@15 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. Thay đổi thư mục thành task-app:
    cd task-app
    
  3. Cài đặt pg để tương tác với cơ sở dữ liệu PostgreSQL.
    npm install pg
    
  4. Cài đặt @types/pg dưới dạng một phần phụ thuộc của nhà phát triển để sử dụng ứng dụng TypeScript Next.js.
    npm install --save-dev @types/pg
    
  5. Mở tệp actions.ts trong Cloud Shell Editor:
    cloudshell edit app/actions.ts
    
    Giờ đây, một tệp trống sẽ xuất hiện ở phần trên cùng của màn hình. Đây là nơi bạn có thể chỉnh sửa tệp actions.ts này. Cho thấy mã nằm ở phần trên cùng của màn hình
  6. Sao chép mã sau đây rồi dán vào tệp actions.ts đã mở:
    '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 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;
    }
    
  7. Mở tệp page.tsx trong Cloud Shell Editor:
    cloudshell edit app/page.tsx
    
    Giờ đây, một tệp hiện có sẽ xuất hiện ở phần trên cùng của màn hình. Đây là nơi bạn có thể chỉnh sửa tệp page.tsx này. Cho thấy mã nằm ở phần trên cùng của màn hình
  8. Xoá nội dung hiện có của tệp page.tsx.
  9. Sao chép mã sau đây rồi dán vào tệp page.tsx đã mở:
    '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 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}
                        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 float-right"
                      >
                        Delete
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </main>
      );
    }
    

Giờ đây, ứng dụng đã sẵn sàng được triển khai.

9. Triển khai ứng dụng lên Cloud Run

  1. Chạy lệnh gcloud projects add-iam-policy-binding như sau để thêm vai trò Network User (Người dùng mạng) vào tài khoản dịch vụ Cloud Run cho dịch vụ Cloud Run mà bạn sắp tạo.
    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. Chạy lệnh gcloud projects add-iam-policy-binding như sau để thêm vai trò Artifact Registry Writer (Người ghi Artifact Registry) cho người dùng hiện tại đối với dịch vụ Cloud Run mà bạn sắp tạo.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
        --member=user:$(gcloud auth list --filter=status:ACTIVE --format="value(account)") \
        --role="roles/artifactregistry.writer"
    
  1. Chạy lệnh bên dưới để triển khai ứng dụng của bạn lên 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. Nếu được nhắc, hãy nhấn YEnter để xác nhận rằng bạn muốn tiếp tục:
    Do you want to continue (Y/n)? Y
    

Sau vài phút, ứng dụng sẽ cung cấp một URL để bạn truy cập.

Chuyển đến URL để xem ứng dụng của bạn hoạt động. Mỗi khi truy cập vào URL hoặc làm mới trang, bạn sẽ thấy ứng dụng Tasks.

10. Thêm một tính năng bằng Gemini Code Assist

Giờ đây, bạn đã triển khai một ứng dụng web có cơ sở dữ liệu. Tiếp theo, chúng ta sẽ thêm một tính năng mới vào ứng dụng next.js bằng cách sử dụng sức mạnh của trợ lý AI.

  1. Quay lại Cloud Shell Editor
  2. Mở lại page.tsx
    cd ~/task-app
    cloudshell edit app/page.tsx
    
  3. Chuyển đến Gemini Code Assist trong Cloud Shell Editor:
    • Nhấp vào biểu tượng Gemini Biểu tượng Gemini Code Assist trong thanh công cụ ở bên trái màn hình
    • Nếu được nhắc, hãy đăng nhập bằng thông tin đăng nhập Tài khoản Google của bạn
    • Nếu bạn nhận được lời nhắc chọn một dự án, hãy chọn dự án mà bạn đã tạo cho Lớp học lập trình này Chọn dự án cho Gemini Code Assist
  4. Nhập câu lệnh: Add the ability to update the title of the task. The code in your output should be complete and working code.. Phản hồi phải bao gồm những đoạn mã như thế này để thêm các hàm handleEditStarthandleEditCancel:
    const [editingTaskId, setEditingTaskId] = useState('');
    const [editedTaskTitle, setEditedTaskTitle] = useState('');
    
    function handleEditStart(task: Task) {
      setEditingTaskId(task.id);
      setEditedTaskTitle(task.title);
    };
    
  5. Thay thế page.tsx bằng đầu ra của Gemini Code Assist. Sau đây là một ví dụ minh hoạ:
    '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('');
      const [editedTaskTitle, setEditedTaskTitle] = useState('');
    
      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();
        setEditingTaskId('');
        setEditedTaskTitle('');
      }
    
      async function deleteTask(taskId: string) {
        await deleteTaskFromDatabase(taskId);
        await getTasks();
      }
    
      function handleEditStart(task: Task) {
        setEditingTaskId(task.id);
        setEditedTaskTitle(task.title);
      };
    
      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">
                      {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={() => updateTask(task, { title: editedTaskTitle })} // Handle clicking outside input
                            className="flex-grow border border-gray-400 rounded px-3 py-1 mr-2 bg-inherit"
                          />
                        </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. Triển khai lại ứng dụng lên Cloud Run

  1. Chạy lệnh bên dưới để triển khai ứng dụng của bạn lên 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. Nếu được nhắc, hãy nhấn YEnter để xác nhận rằng bạn muốn tiếp tục:
    Do you want to continue (Y/n)? Y
    

12. Xin chúc mừng

Trong phòng thí nghiệm này, bạn đã tìm hiểu cách thực hiện những việc sau:

  • Tạo một phiên bản Cloud SQL cho PostgreSQL
  • Triển khai một ứng dụng lên Cloud Run để kết nối với cơ sở dữ liệu Cloud SQL

Dọn dẹp

Cloud SQL không có bậc miễn phí và sẽ tính phí nếu bạn tiếp tục sử dụng dịch vụ này. Bạn có thể xoá dự án trên Google Cloud để tránh phát sinh thêm chi phí.

Mặc dù Cloud Run không tính phí khi dịch vụ không được sử dụng, nhưng bạn vẫn có thể bị tính phí khi lưu trữ hình ảnh vùng chứa trong Artifact Registry. Khi bạn xoá dự án trên Cloud, hệ thống sẽ ngừng tính phí cho tất cả tài nguyên được dùng trong dự án đó.

Nếu bạn muốn, hãy xoá dự án:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Bạn cũng có thể xoá các tài nguyên không cần thiết khỏi đĩa cloudshell. Bạn có thể:

  1. Xoá thư mục dự án lớp học lập trình:
    rm -rf ~/task-app
    
  2. Cảnh báo! Bạn không thể huỷ thao tác tiếp theo này! Nếu muốn xoá mọi thứ trên Cloud Shell để giải phóng dung lượng, bạn có thể xoá toàn bộ thư mục gốc. Hãy cẩn thận để đảm bảo mọi thứ bạn muốn giữ lại đều được lưu ở nơi khác.
    sudo rm -rf $HOME
    

Tiếp tục học tập