Men-deploy aplikasi Angular stack lengkap ke Cloud Run dengan Cloud SQL untuk PostgreSQL menggunakan Konektor Node.js Cloud SQL

1. Ringkasan

Cloud Run adalah platform terkelola sepenuhnya yang memungkinkan Anda menjalankan kode langsung di atas infrastruktur Google yang skalabel. Codelab ini akan mendemonstrasikan cara menghubungkan aplikasi Angular di Cloud Run ke database Cloud SQL untuk PostgreSQL menggunakan Cloud SQL Node.js Connector.

Di lab ini, Anda akan mempelajari cara:

  • Membuat instance Cloud SQL for PostgreSQL
  • Deploy aplikasi ke Cloud Run yang terhubung ke database Cloud SQL Anda

2. Prasyarat

  1. Jika Anda belum memiliki Akun Google, Anda harus membuat Akun Google.
    • Gunakan akun pribadi, bukan akun kantor atau sekolah. Akun kantor dan sekolah mungkin memiliki batasan yang mencegah Anda mengaktifkan API yang diperlukan untuk lab ini.

3. Penyiapan project

  1. Login ke Konsol Google Cloud.
  2. Aktifkan penagihan di Konsol Cloud.
    • Menyelesaikan lab ini akan dikenai biaya kurang dari $1 USD untuk resource Cloud.
    • Anda dapat mengikuti langkah-langkah di akhir lab ini untuk menghapus resource agar tidak dikenai biaya lebih lanjut.
    • Pengguna baru memenuhi syarat untuk mengikuti Uji Coba Gratis senilai$300 USD.
  3. Buat project baru atau pilih untuk menggunakan kembali project yang ada.

4. Buka Cloud Shell Editor

  1. Buka Cloud Shell Editor
  2. Jika terminal tidak muncul di bagian bawah layar, buka terminal:
    • Klik menu tiga garis Ikon menu tiga garis
    • Klik Terminal
    • Klik New TerminalMembuka terminal baru di Cloud Shell Editor
  3. Di terminal, tetapkan project Anda dengan perintah ini:
    • Format:
      gcloud config set project [PROJECT_ID]
      
    • Contoh:
      gcloud config set project lab-project-id-example
      
    • Jika Anda tidak ingat project ID Anda:
      • Anda dapat mencantumkan semua project ID Anda dengan:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Menetapkan project ID di terminal Cloud Shell Editor
  4. Jika diminta untuk memberikan otorisasi, klik Authorize untuk melanjutkan. Klik untuk memberikan otorisasi pada Cloud Shell
  5. Anda akan melihat pesan ini:
    Updated property [core/project].
    
    Jika Anda melihat WARNING dan diminta Do you want to continue (Y/N)?, berarti Anda kemungkinan telah memasukkan ID project dengan salah. Tekan N, tekan Enter, lalu coba jalankan perintah gcloud config set project lagi.

5. Mengaktifkan API

Di terminal, aktifkan API:

gcloud services enable \
  sqladmin.googleapis.com \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com

Jika diminta untuk memberikan otorisasi, klik Authorize untuk melanjutkan. Klik untuk memberikan otorisasi pada Cloud Shell

Pemrosesan perintah ini mungkin membutuhkan waktu beberapa menit, tetapi pada akhirnya akan menghasilkan pesan keberhasilan yang mirip dengan yang berikut:

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

6. Menyiapkan Akun Layanan

Buat dan konfigurasi akun layanan Google Cloud yang akan digunakan oleh Cloud Run sehingga akun tersebut memiliki izin yang benar untuk terhubung ke Cloud SQL.

  1. Jalankan perintah gcloud iam service-accounts create sebagai berikut untuk membuat akun layanan baru:
    gcloud iam service-accounts create quickstart-service-account \
      --display-name="Quickstart Service Account"
    
  2. Jalankan perintah gcloud projects add-iam-policy-binding sebagai berikut untuk menambahkan peran Cloud SQL Client ke akun layanan Google Cloud yang baru saja Anda buat.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
      --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
      --role="roles/cloudsql.client"
    
  3. Jalankan perintah gcloud projects add-iam-policy-binding sebagai berikut untuk menambahkan peran Cloud SQL Instance User ke akun layanan Google Cloud yang baru saja Anda buat.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
      --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
      --role="roles/cloudsql.instanceUser"
    
  4. Jalankan perintah gcloud projects add-iam-policy-binding sebagai berikut untuk menambahkan peran Log Writer ke akun layanan Google Cloud yang baru saja Anda buat.
    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. Buat Database Cloud SQL

  1. Jalankan perintah gcloud sql instances create untuk membuat instance 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
    

Pemrosesan perintah ini dapat membutuhkan waktu beberapa menit.

  1. Jalankan perintah gcloud sql databases create untuk membuat database Cloud SQL dalam quickstart-instance.
    gcloud sql databases create quickstart_db \
        --instance=quickstart-instance
    
  2. Buat pengguna database PostgreSQL untuk akun layanan yang Anda buat sebelumnya guna mengakses database.
    gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \
        --instance=quickstart-instance \
        --type=cloud_iam_service_account
    

8. Menyiapkan Aplikasi

Siapkan aplikasi Next.js yang merespons permintaan HTTP.

  1. Untuk membuat project Next.js baru bernama task-app, gunakan perintah:
    npx --yes @angular/cli@19.2.5 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --server-routing \
        --defaults
    
  2. Ubah direktori menjadi task-app:
    cd task-app
    
  1. Instal pg dan library konektor Node.js Cloud SQL untuk berinteraksi dengan database PostgreSQL.
    npm install pg @google-cloud/cloud-sql-connector google-auth-library
    
  2. Instal @types/pg sebagai dependensi dev untuk menggunakan aplikasi Next.js TypeScript.
    npm install --save-dev @types/pg
    
  1. Buka file server.ts di Cloud Shell Editor:
    cloudshell edit src/server.ts
    
    File akan muncul di bagian atas layar. Di sinilah Anda dapat mengedit file server.ts ini. Tunjukkan bahwa kode masuk di bagian atas layar
  2. Hapus konten yang ada di file server.ts.
  3. Salin kode berikut dan tempelkan ke dalam file server.ts yang terbuka:
    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);
    
  1. Buka file app.component.ts di Cloud Shell Editor:
    cloudshell edit src/app/app.component.ts
    
    File yang ada akan muncul di bagian atas layar. Di sinilah Anda dapat mengedit file app.component.ts ini. Tunjukkan bahwa kode masuk di bagian atas layar
  2. Hapus konten yang ada di file app.component.ts.
  3. Salin kode berikut dan tempelkan ke dalam file app.component.ts yang terbuka:
    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();
      }
    }
    

Aplikasi kini siap di-deploy.

9. Men-deploy aplikasi ke Cloud Run

  1. Jalankan perintah di bawah untuk men-deploy aplikasi Anda ke 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
    
  2. Jika diminta, tekan Y dan Enter untuk mengonfirmasi bahwa Anda ingin melanjutkan:
    Do you want to continue (Y/n)? Y
    

Setelah beberapa menit, aplikasi akan memberikan URL yang dapat Anda kunjungi.

Buka URL untuk melihat cara kerja aplikasi Anda. Setiap kali Anda membuka URL atau memuat ulang halaman, Anda akan melihat aplikasi tugas.

10. Selamat

Di lab ini, Anda telah mempelajari cara melakukan hal-hal berikut:

  • Membuat instance Cloud SQL for PostgreSQL
  • Deploy aplikasi ke Cloud Run yang terhubung ke database Cloud SQL Anda

Pembersihan

Cloud SQL tidak memiliki paket gratis dan akan menagih Anda jika Anda terus menggunakannya. Anda dapat menghapus project Cloud untuk menghindari biaya tambahan.

Meskipun Cloud Run tidak mengenakan biaya saat layanannya tidak digunakan, Anda mungkin tetap ditagih atas penyimpanan image container di Artifact Registry. Menghapus project Cloud akan menghentikan penagihan untuk semua resource yang digunakan dalam project tersebut.

Jika Anda ingin, hapus project:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Anda juga dapat menghapus resource yang tidak diperlukan dari disk cloudshell. Anda dapat:

  1. Hapus direktori project codelab:
    rm -rf ~/task-app
    
  2. Peringatan! Tindakan berikutnya ini tidak dapat diurungkan. Jika ingin menghapus semua yang ada di Cloud Shell untuk mengosongkan ruang, Anda dapat menghapus seluruh direktori beranda Anda. Berhati-hatilah agar semua yang ingin Anda simpan disimpan di tempat lain.
    sudo rm -rf $HOME
    

Terus belajar