1. Tổng quan
Cloud Run là một nền tảng được quản lý toàn diệ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 ứng dụng Next.js trên Cloud Run với Cloud SQL cho cơ sở dữ liệu PostgreSQL.
Trong lớp học lập trình này, bạn sẽ tìm hiểu cách:
- Tạo phiên bản Cloud SQL cho PostgreSQL (được định cấu hình để sử dụng Private Service Connect)
- Triển khai một ứng dụng trên Cloud Run để kết nối với cơ sở dữ liệu Cloud SQL
- Sử dụng tính năng Hỗ trợ mã Gemini để thêm chức năng vào ứng dụng
2. Điều kiện tiên quyết
- Nếu chưa có Tài khoản Google, bạn phải tạo Tài khoản Google.
- Sử dụng tài khoản cá nhân thay vì tài khoản công việc hoặc trường học. Tài khoản công việc và tài khoản trường học có thể có các quy định 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
- Đăng nhập vào Google Cloud Console.
- Bật tính năng thanh toán trong Cloud Console.
- Chi phí để hoàn thành lớp học này sẽ dưới 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 lớp học này để xoá tài nguyên nhằm tránh bị tính thêm phí.
- Người dùng mới đủ điều kiện dùng thử miễn phí 300 USD.
- Tạo 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
- Chuyển đến Trình chỉnh sửa Cloud Shell
- Nếu cửa sổ dòng lệnh không xuất hiện ở cuối màn hình, hãy mở cửa sổ đó:
- Nhấp vào trình đơn có biểu tượng ba dấu gạch ngang
- 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)
- Nhấp vào trình đơn có biểu tượng ba dấu gạch ngang
- Trong dòng lệnh, 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:
- Bạn có thể liệt kê tất cả mã dự án bằng:
gcloud projects list | awk '/PROJECT_ID/{print $2}'
- Bạn có thể liệt kê tất cả mã dự án bằng:
- Định dạng:
- Nếu bạn được nhắc uỷ quyền, hãy nhấp vào Uỷ quyền để tiếp tục.
- Bạn sẽ thấy thông báo này:
Nếu bạn thấyUpdated property [core/project].
WARNING
và được hỏiDo you want to continue (Y/N)?
, thì có thể bạn đã nhập mã dự án không chính xác. NhấnN
, nhấnEnter
rồi thử chạy lại lệnhgcloud 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 bạn được nhắc uỷ quyền, hãy nhấp vào Uỷ quyền để tiếp tục.
Quá trình thực thi lệnh này có thể mất vài phú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ư sau:
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 tài khoản dịch vụ Google Cloud để Cloud Run sử dụng, nhờ đó tài khoản này có các quyền thích hợp để kết nối với Cloud SQL.
- Chạy lệnh
gcloud iam service-accounts create
như sau để tạo một tài khoản dịch vụ mới:gcloud iam service-accounts create quickstart-service-account \ --display-name="Quickstart Service Account"
- Chạy lệnh gcloud projects add-iam-policy-binding như sau để thêm vai trò Trình 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 phiên bản Cloud SQL
- 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
- Tạo mật khẩu duy nhất cho cơ sở dữ liệu
export DB_PASSWORD=$(openssl rand -base64 20)
- Chạy lệnh
gcloud sql instances create
để tạo phiên bản Cloud SQLgcloud 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
Có thể mất vài phút để hoàn tất lệnh này.
- Chạy lệnh
gcloud sql databases create
để tạo cơ sở dữ liệu Cloud SQL trongquickstart-instance
.gcloud sql databases create quickstart_db \ --instance=quickstart-instance
8. Chuẩn bị ứng dụng
Chuẩn bị một ứng dụng Next.js phản hồi các yêu cầu HTTP.
- Để tạo một dự án Next.js mới có tên là
task-app
, hãy sử dụng lệnh:npx create-next-app@15.0.3 task-app \ --ts \ --eslint \ --tailwind \ --no-src-dir \ --turbopack \ --app \ --no-import-alias
- Nếu được yêu cầu cài đặt
create-next-app
, hãy nhấnEnter
để tiếp tục:Need to install the following packages: create-next-app@15.0.3 Ok to proceed? (y)
- Thay đổi thư mục thành
task-app
:cd task-app
- Cài đặt
pg
để tương tác với cơ sở dữ liệu PostgreSQL.npm install pg
- Cài đặt
@types/pg
dưới dạng phần phụ thuộc dành cho nhà phát triển để sử dụng ứng dụng TypeScript Next.js.npm install --save-dev @types/pg
- Tạo tệp
actions.ts
.touch app/actions.ts
- Mở tệp
actions.ts
trong Trình chỉnh sửa Cloud Shell: 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ệpcloudshell edit app/actions.ts
actions.ts
này. - Sao chép mã sau và 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 visits ( id SERIAL NOT NULL, created_at timestamp NOT NULL, PRIMARY KEY (id) );`); 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`); console.table(rows); // logs the last 5 visits on the server return rows; // sends the last 5 visits to the client } // 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; }
- Mở tệp
page.tsx
trong Trình chỉnh sửa Cloud Shell: 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ệpcloudshell edit app/page.tsx
page.tsx
này. - Xoá nội dung hiện có của tệp
page.tsx
. - Sao chép mã sau và 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 addTask() { 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" > Delete </button> </td> </tr> ); })} </tbody> </table> </main> ); }
Ứng dụng hiện đã sẵn sàng để triển khai.
9. Triển khai ứng dụng trên Cloud Run
- Chạy lệnh gcloud projects add-iam-policy-binding như sau để thêm vai trò 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"
- Chạy lệnh dưới đây để 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
- Nếu được nhắc, hãy nhấn
Y
vàEnter
để 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 công việc.
10. Thêm tính năng bằng Gemini Code Assist
Bây giờ, 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 tính năng hỗ trợ AI.
- Quay lại Trình chỉnh sửa Cloud Shell
- Mở lại
page.tsx
cd ~/task-app cloudshell edit app/page.tsx
- Chuyển đến tính năng Trợ giúp mã Gemini trong Trình chỉnh sửa Cloud Shell:
- Nhấp vào biểu tượng Gemini
trong thanh công cụ ở bên trái màn hình
- Đăng nhập bằng thông tin đăng nhập của Tài khoản Google nếu được nhắc
- Nếu bạn được nhắc chọn một dự án, hãy chọn dự án bạn đã tạo cho lớp học lập trình này
- Nhấp vào biểu tượng Gemini
- 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 các đoạn mã như sau để thêm hàmhandleEditStart
vàhandleEditCancel
:const [editingTaskId, setEditingTaskId] = useState<string>(''); const [editedTaskTitle, setEditedTaskTitle] = useState(''); const handleEditStart = (task: Task) => { setEditingTaskId(task.id); setEditedTaskTitle(task.title); }; const handleEditCancel = () => { setEditingTaskId(''); setEditedTaskTitle(''); };
<td className="py-2 px-4"> {editingTaskId === task.id ? ( <form onSubmit={(e) => { e.preventDefault(); updateTask(task, { title: editedTaskTitle }); }}> <input type="text" value={editedTaskTitle} onChange={(e) => setEditedTaskTitle(e.target.value)} onBlur={() => handleEditCancel} // Handle clicking outside input className="border border-gray-400 rounded px-3 py-1 mr-2" /> <button type="submit" className="text-green-600 hover:text-green-900 mr-1">Save</button> </form> ) : ( <span onClick={() => handleEditStart(task)} className="cursor-pointer"> {task.title} </span> )} </td>
- Thay thế
page.tsx
bằng kết quả của Gemini Code Assist. Sau đây là ví dụ về cách hoạt động:'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<string>(''); const [editedTaskTitle, setEditedTaskTitle] = useState(''); async function getTasks() { const updatedListOfTasks = await getTasksFromDatabase(); setTasks(updatedListOfTasks); } useEffect(() => { getTasks(); }, []); async function addTask() { await addNewTaskToDatabase(newTaskTitle); await getTasks(); setNewTaskTitle(''); } async function updateTask(task: Task, newTaskValues: Partial<Task>) { await updateTaskInDatabase({ ...task, ...newTaskValues }); await getTasks(); setEditingTaskId(''); // Clear editing state after update setEditedTaskTitle(''); } async function deleteTask(taskId: string) { await deleteTaskFromDatabase(taskId); await getTasks(); } const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); addTask(); }; const handleEditStart = (task: Task) => { setEditingTaskId(task.id); setEditedTaskTitle(task.title); }; const handleEditCancel = () => { setEditingTaskId(''); setEditedTaskTitle(''); }; return ( <main className="p-4"> <h2 className="text-2xl font-bold mb-4">To Do List</h2> <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> <table className="w-full"> <tbody> {tasks.map(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={() => handleEditCancel()} // Handle clicking outside input className="flex-grow border border-gray-400 rounded px-3 py-1 mr-2 bg-inherit" /> <button type="submit" className="bg-green-600 hover:bg-green-900 m-1 text-white font-bold py-2 px-4 rounded" > Save </button> </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
- Chạy lệnh dưới đây để 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
- Nếu được nhắc, hãy nhấn
Y
vàEnter
để 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 lớp học lập trình này, bạn đã tìm hiểu cách thực hiện các việc sau:
- Tạo phiên bản Cloud SQL cho PostgreSQL (được định cấu hình để sử dụng Private Service Connect)
- Triển khai một ứng dụng trên Cloud Run để kết nối với cơ sở dữ liệu Cloud SQL
- Sử dụng tính năng Hỗ trợ mã Gemini để thêm chức năng vào ứng dụng
Dọn dẹp
Cloud SQL không có cấp miễn phí và sẽ tính phí nếu bạn tiếp tục sử dụng. Bạn có thể xoá dự án trên Google Cloud để tránh bị tính thêm phí.
Mặc dù Cloud Run không tính phí khi bạn không sử dụng dịch vụ, nhưng bạn vẫn có thể phải trả phí để lưu trữ hình ảnh vùng chứa trong CSDL cấu phần phần mềm. Việc xoá dự án trên Google Cloud sẽ dừng tính phí cho tất cả tài nguyên được sử dụng trong dự án đó.
Xoá dự án nếu bạn muố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ể:
- Xoá thư mục dự án lớp học lập trình:
rm -rf ~/task-app
- Cảnh báo! Bạn không thể huỷ thao tác tiếp theo! 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 lưu mọi thứ bạn muốn giữ lại ở nơi khác.
sudo rm -rf $HOME