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 Angular trên Cloud Run với cơ sở dữ liệu Cloud SQL cho PostgreSQL bằng Trình kết nối Cloud SQL Node.js.
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
- 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 trường học hoặc tài khoản do nơi làm việc cấp. Tài khoản do nơi làm việc hoặc trường học cấp có thể có các hạn chế khiến bạn không bật được những 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.
- Việc hoàn thành bài tập 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 đủ điều kiện dùng thử miễn phí trị giá 300 USD.
- 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
- Chuyển đến Cloud Shell Editor
- 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

- 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 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 sử dụng:
gcloud projects list | awk '/PROJECT_ID/{print $2}'

- Bạn có thể liệt kê tất cả mã dự án bằng cách sử dụng:
- Định dạng:
- Nếu đượ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 sau:
Nếu thấy biểu tượngUpdated property [core/project].
WARNINGvà được yêu cầuDo you want to continue (Y/N)?, thì có thể bạn đã nhập sai mã dự án. NhấnN, nhấnEnterrồ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 \
sqladmin.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com
Nếu được nhắc uỷ quyền, hãy nhấp vào Uỷ quyền để tiếp tục. 
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.
- Chạy lệnh
gcloud iam service-accounts createnhư 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" - Chạy lệnh gcloud projects add-iam-policy-binding như sau để thêm vai trò Cloud SQL Client (Ứng dụng Cloud SQL) 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/cloudsql.client" - Chạy lệnh gcloud projects add-iam-policy-binding như sau để thêm vai trò Cloud SQL Instance User (Người dùng phiên bản Cloud SQL) 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/cloudsql.instanceUser" - 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
- Chạy lệnh
gcloud sql instances createđể tạo một phiên bản Cloud SQLgcloud sql instances create quickstart-instance \ --database-version=POSTGRES_14 \ --cpu=4 \ --memory=16GB \ --region=us-central1 \ --database-flags=cloudsql.iam_authentication=on
Lệnh này có thể mất vài phút để hoàn tất.
- 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 - Tạo một người dùng cơ sở dữ liệu PostgreSQL cho tài khoản dịch vụ mà bạn đã tạo trước đó để truy cập vào cơ sở dữ liệu.
gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \ --instance=quickstart-instance \ --type=cloud_iam_service_account
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.
- Để tạo một dự án Next.js mới có tên là
task-app, hãy dùng lệnh:npx --yes @angular/cli@19.2.5 new task-app \ --minimal \ --inline-template \ --inline-style \ --ssr \ --server-routing \ --defaults - Thay đổi thư mục thành
task-app:cd task-app
- Cài đặt
pgvà thư viện trình kết nối Cloud SQL Node.js để tương tác với cơ sở dữ liệu PostgreSQL.npm install pg @google-cloud/cloud-sql-connector google-auth-library - Cài đặt
@types/pgdưới dạng một 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
- Mở tệp
server.tstrong Cloud Shell Editor: Giờ đây, một tệp 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 src/server.tsserver.tsnày.
- Xoá nội dung hiện có của tệp
server.ts. - Sao chép mã sau đây rồi dán vào tệp
server.tsđã mở:import { AngularNodeAppEngine, createNodeRequestHandler, isMainModule, writeResponseToNodeResponse, } from '@angular/ssr/node'; import express from 'express'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import pg from 'pg'; import { AuthTypes, Connector } from '@google-cloud/cloud-sql-connector'; import { GoogleAuth } from 'google-auth-library'; const auth = new GoogleAuth(); const { Pool } = pg; type Task = { id: string; title: string; status: 'IN_PROGRESS' | 'COMPLETE'; createdAt: number; }; const projectId = await auth.getProjectId(); const connector = new Connector(); const clientOpts = await connector.getOptions({ instanceConnectionName: `${projectId}:us-central1:quickstart-instance`, authType: AuthTypes.IAM, }); const pool = new Pool({ ...clientOpts, user: `quickstart-service-account@${projectId}.iam`, database: 'quickstart_db', }); 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) );`); } const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const browserDistFolder = resolve(serverDistFolder, '../browser'); const app = express(); const angularApp = new AngularNodeAppEngine(); app.use(express.json()); app.get('/api/tasks', async (req, res) => { await tableCreationIfDoesNotExist(); const { rows } = await pool.query(`SELECT id, created_at, status, title FROM tasks ORDER BY created_at DESC LIMIT 100`); res.send(rows); }); app.post('/api/tasks', async (req, res) => { const newTaskTitle = req.body.title; if (!newTaskTitle) { res.status(400).send("Title is required"); return; } await tableCreationIfDoesNotExist(); await pool.query(`INSERT INTO tasks(created_at, status, title) VALUES(NOW(), 'IN_PROGRESS', $1)`, [newTaskTitle]); res.sendStatus(200); }); app.put('/api/tasks', async (req, res) => { const task: Task = req.body; if (!task || !task.id || !task.title || !task.status) { res.status(400).send("Invalid task data"); return; } await tableCreationIfDoesNotExist(); await pool.query( `UPDATE tasks SET status = $1, title = $2 WHERE id = $3`, [task.status, task.title, task.id] ); res.sendStatus(200); }); app.delete('/api/tasks', async (req, res) => { const task: Task = req.body; if (!task || !task.id) { res.status(400).send("Task ID is required"); return; } await tableCreationIfDoesNotExist(); await pool.query(`DELETE FROM tasks WHERE id = $1`, [task.id]); res.sendStatus(200); }); /** * Serve static files from /browser */ app.use( express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false, }), ); /** * Handle all other requests by rendering the Angular application. */ app.use('/**', (req, res, next) => { angularApp .handle(req) .then((response) => response ? writeResponseToNodeResponse(response, res) : next(), ) .catch(next); }); /** * Start the server if this module is the main entry point. * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. */ if (isMainModule(import.meta.url)) { const port = process.env['PORT'] || 4000; app.listen(port, () => { console.log(`Node Express server listening on http://localhost:${port}`); }); } /** * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions. */ export const reqHandler = createNodeRequestHandler(app);
- Mở tệp
app.component.tstrong Cloud Shell Editor: 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ệpcloudshell edit src/app/app.component.tsapp.component.tsnày.
- Xoá nội dung hiện có của tệp
app.component.ts. - Sao chép mã sau đây rồi dán vào tệp
app.component.tsđã mở:import { afterNextRender, Component, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; type Task = { id: string; title: string; status: 'IN_PROGRESS' | 'COMPLETE'; createdAt: number; }; @Component({ selector: 'app-root', standalone: true, imports: [FormsModule], template: ` <section> <input type="text" placeholder="New Task Title" [(ngModel)]="newTaskTitle" class="text-black border-2 p-2 m-2 rounded" /> <button (click)="addTask()">Add new task</button> <table> <tbody> @for (task of tasks(); track task) { @let isComplete = task.status === 'COMPLETE'; <tr> <td> <input (click)="updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })" type="checkbox" [checked]="isComplete" /> </td> <td>{{ task.title }}</td> <td>{{ task.status }}</td> <td> <button (click)="deleteTask(task)">Delete</button> </td> </tr> } </tbody> </table> </section> `, styles: '', }) export class AppComponent { newTaskTitle = ''; tasks = signal<Task[]>([]); constructor() { afterNextRender({ earlyRead: () => this.getTasks() }); } async getTasks() { const response = await fetch(`/api/tasks`); const tasks = await response.json(); this.tasks.set(tasks); } async addTask() { await fetch(`/api/tasks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: this.newTaskTitle, status: 'IN_PROGRESS', createdAt: Date.now(), }), }); this.newTaskTitle = ''; await this.getTasks(); } async updateTask(task: Task, newTaskValues: Partial<Task>) { await fetch(`/api/tasks`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...task, ...newTaskValues }), }); await this.getTasks(); } async deleteTask(task: any) { await fetch('/api/tasks', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task), }); await this.getTasks(); } }
Giờ đây, ứng dụng đã sẵn sàng được triển khai.
9. Triển khai ứng dụng lên Cloud Run
- 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 to-do-tracker \ --region=us-central1 \ --source=. \ --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \ --allow-unauthenticated - Nếu được nhắc, hãy nhấn
Yvà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 Tasks.
10. 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 đám mây để 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 đám mây, 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 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ể:
- Xoá thư mục dự án của 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ày sau khi thực hiện! 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 rằng 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
- Triển khai một ứng dụng Next.js đầy đủ lên Cloud Run bằng Cloud SQL cho PostgreSQL bằng Trình kết nối Cloud SQL Node.js
- Triển khai một ứng dụng Angular có đầy đủ chức năng lên Cloud Run bằng Cloud SQL cho PostgreSQL thông qua Trình kết nối Node.js Cloud SQL
- Triển khai một ứng dụng Angular toàn diện lên Cloud Run bằng Firestore bằng cách sử dụng SDK quản trị Node.js
- Triển khai một ứng dụng Next.js đầy đủ lên Cloud Run bằng Firestore bằng cách sử dụng Node.js Admin SDK