1. 개요
Cloud Run은 Google의 확장 가능한 인프라에서 직접 코드를 실행할 수 있게 해주는 완전 관리형 플랫폼입니다. 이 Codelab에서는 Cloud SQL Node.js 커넥터를 사용하여 Cloud Run의 Angular 애플리케이션을 PostgreSQL용 Cloud SQL 데이터베이스에 연결하는 방법을 보여줍니다.
이 실습에서는 다음 작업을 수행하는 방법을 배웁니다.
- PostgreSQL용 Cloud SQL 인스턴스 만들기
- Cloud SQL 데이터베이스에 연결되는 애플리케이션을 Cloud Run에 배포
2. 기본 요건
- 아직 Google 계정이 없다면 Google 계정을 만들어야 합니다.
- 직장 또는 학교 계정이 아닌 개인 계정을 사용합니다. 직장 및 학교 계정에는 이 실습에 필요한 API를 사용 설정할 수 없도록 하는 제한사항이 있을 수 있습니다.
3. 프로젝트 설정
- Google Cloud 콘솔에 로그인합니다.
- Cloud 콘솔에서 결제를 사용 설정합니다.
- 이 실습을 완료하는 데 드는 Cloud 리소스 비용은 미화 1달러 미만입니다.
- 이 실습의 끝에 있는 단계에 따라 리소스를 삭제하여 추가 비용이 청구되지 않도록 할 수 있습니다.
- 신규 사용자는 미화$300 상당의 무료 체험판을 이용할 수 있습니다.
- 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다.
4. Cloud Shell 편집기 열기
- Cloud Shell 편집기로 이동합니다.
- 터미널이 화면 하단에 표시되지 않으면 다음과 같이 엽니다.
- 햄버거 메뉴
를 클릭합니다.
- 터미널을 클릭합니다.
- 새 터미널을 클릭합니다.
- 햄버거 메뉴
- 터미널에서 다음 명령어를 사용하여 프로젝트를 설정합니다.
- 형식:
gcloud config set project [PROJECT_ID]
- 예:
gcloud config set project lab-project-id-example
- 프로젝트 ID를 기억할 수 없는 경우 다음 안내를 따르세요.
- 다음을 사용하여 모든 프로젝트 ID를 나열할 수 있습니다.
gcloud projects list | awk '/PROJECT_ID/{print $2}'
- 다음을 사용하여 모든 프로젝트 ID를 나열할 수 있습니다.
- 형식:
- 승인하라는 메시지가 표시되면 승인을 클릭하여 계속 진행합니다.
- 다음 메시지가 표시되어야 합니다.
Updated property [core/project].
WARNING
이 표시되고Do you want to continue (Y/N)?
메시지가 표시되면 프로젝트 ID를 잘못 입력했을 가능성이 큽니다.N
를 누른 다음Enter
를 누르고gcloud config set project
명령어를 다시 실행해 봅니다.
5. API 사용 설정
터미널에서 API를 사용 설정합니다.
gcloud services enable \
sqladmin.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com
승인하라는 메시지가 표시되면 승인을 클릭하여 계속 진행합니다.
이 명령어를 완료하는 데 몇 분 정도 걸릴 수 있지만, 결국 다음과 유사한 성공 메시지가 표시됩니다.
Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.
6. 서비스 계정 설정
Cloud SQL에 연결할 수 있는 올바른 권한이 있도록 Cloud Run에서 사용할 Google Cloud 서비스 계정을 만들고 구성합니다.
- 다음과 같이
gcloud iam service-accounts create
명령어를 실행하여 새 서비스 계정을 만듭니다.gcloud iam service-accounts create quickstart-service-account \ --display-name="Quickstart Service Account"
- 다음과 같이 gcloud projects add-iam-policy-binding 명령어를 실행하여 방금 만든 Google Cloud 서비스 계정에 Cloud SQL 클라이언트 역할을 추가합니다.
gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \ --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \ --role="roles/cloudsql.client"
- 다음과 같이 gcloud projects add-iam-policy-binding 명령어를 실행하여 방금 만든 Google Cloud 서비스 계정에 Cloud SQL 인스턴스 사용자 역할을 추가합니다.
gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \ --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \ --role="roles/cloudsql.instanceUser"
- 다음과 같이 gcloud projects add-iam-policy-binding 명령어를 실행하여 방금 만든 Google Cloud 서비스 계정에 로그 작성자 역할을 추가합니다.
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. Cloud SQL 데이터베이스 만들기
gcloud sql instances create
명령어를 실행하여 Cloud SQL 인스턴스 만들기gcloud sql instances create quickstart-instance \ --database-version=POSTGRES_14 \ --cpu=4 \ --memory=16GB \ --region=us-central1 \ --database-flags=cloudsql.iam_authentication=on
이 명령어를 완료하는 데 몇 분 정도 걸릴 수 있습니다.
gcloud sql databases create
명령어를 실행하여quickstart-instance
내에 Cloud SQL 데이터베이스를 만듭니다.gcloud sql databases create quickstart_db \ --instance=quickstart-instance
- 데이터베이스에 액세스할 수 있도록 앞서 만든 서비스 계정에 PostgreSQL 데이터베이스 사용자를 만듭니다.
gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \ --instance=quickstart-instance \ --type=cloud_iam_service_account
8. 신청 준비
HTTP 요청에 응답하는 Next.js 애플리케이션을 준비합니다.
task-app
라는 새 Next.js 프로젝트를 만들려면 다음 명령어를 사용합니다.npx --yes @angular/cli@19.2.5 new task-app \ --minimal \ --inline-template \ --inline-style \ --ssr \ --server-routing \ --defaults
- 디렉터리를
task-app
으로 변경합니다.cd task-app
pg
및 Cloud SQL Node.js 커넥터 라이브러리를 설치하여 PostgreSQL 데이터베이스와 상호작용합니다.npm install pg @google-cloud/cloud-sql-connector google-auth-library
- TypeScript Next.js 애플리케이션을 사용하려면
@types/pg
를 개발 종속 항목으로 설치합니다.npm install --save-dev @types/pg
- Cloud Shell 편집기에서
server.ts
파일을 엽니다. 이제 파일이 화면 상단에 표시됩니다. 여기에서 이cloudshell edit src/server.ts
server.ts
파일을 수정할 수 있습니다. server.ts
파일의 기존 콘텐츠를 삭제합니다.- 다음 코드를 복사하여 열려 있는
server.ts
파일에 붙여넣습니다.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);
- Cloud Shell 편집기에서
app.component.ts
파일을 엽니다. 이제 기존 파일이 화면 상단에 표시됩니다. 여기에서 이cloudshell edit src/app/app.component.ts
app.component.ts
파일을 수정할 수 있습니다. app.component.ts
파일의 기존 콘텐츠를 삭제합니다.- 다음 코드를 복사하여 열려 있는
app.component.ts
파일에 붙여넣습니다.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(); } }
이제 애플리케이션을 배포할 준비가 되었습니다.
9. Cloud Run에 애플리케이션 배포
- 아래 명령어를 실행하여 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
- 메시지가 표시되면
Y
및Enter
를 눌러 계속 진행하겠다고 확인합니다.Do you want to continue (Y/n)? Y
몇 분 후 애플리케이션에서 방문할 URL을 제공합니다.
URL로 이동하여 애플리케이션이 작동하는 모습을 확인합니다. URL을 방문하거나 페이지를 새로고침할 때마다 할 일 앱이 표시됩니다.
10. 축하합니다
이 실습에서는 다음을 수행하는 방법을 알아봤습니다.
- PostgreSQL용 Cloud SQL 인스턴스 만들기
- Cloud SQL 데이터베이스에 연결되는 애플리케이션을 Cloud Run에 배포
삭제
Cloud SQL에는 무료 등급이 없으며 계속 사용하면 요금이 청구됩니다. Cloud 프로젝트를 삭제하여 추가 비용이 청구되지 않도록 할 수 있습니다.
Cloud Run에서는 서비스를 사용하지 않을 때 비용이 청구되지 않지만 Artifact Registry에 컨테이너 이미지를 저장하는 데 요금이 부과될 수 있습니다. Cloud 프로젝트를 삭제하면 해당 프로젝트 내에서 사용되는 모든 리소스에 대한 청구가 중단됩니다.
원하는 경우 프로젝트를 삭제합니다.
gcloud projects delete $GOOGLE_CLOUD_PROJECT
CloudShell 디스크에서 불필요한 리소스를 삭제할 수도 있습니다. 다음과 같은 작업을 할 수 있습니다.
- Codelab 프로젝트 디렉터리를 삭제합니다.
rm -rf ~/task-app
- 경고 다음 작업은 실행취소할 수 없습니다. Cloud Shell에서 모든 항목을 삭제하여 공간을 확보하려면 전체 홈 디렉터리를 삭제하면 됩니다. 보관하려는 모든 항목이 다른 곳에 저장되어 있는지 확인하세요.
sudo rm -rf $HOME
계속 학습
- Cloud SQL Node.js 커넥터를 사용하여 PostgreSQL용 Cloud SQL과 함께 Cloud Run에 풀 스택 Next.js 애플리케이션 배포
- Cloud SQL Node.js 커넥터를 사용하여 PostgreSQL용 Cloud SQL과 함께 Cloud Run에 풀 스택 Angular 애플리케이션 배포
- Node.js Admin SDK를 사용하여 Firestore와 함께 Cloud Run에 풀 스택 Angular 애플리케이션 배포
- Node.js Admin SDK를 사용하여 Firestore와 함께 Cloud Run에 풀 스택 Next.js 애플리케이션 배포