Wdrażanie w Cloud Run pełnego pakietu aplikacji Angular z Cloud SQL for PostgreSQL przy użyciu łącznika Cloud SQL Node.js

1. Omówienie

Cloud Run to w pełni zarządzana platforma, która umożliwia uruchamianie kodu bezpośrednio w infrastrukturze Google o wysokiej skalowalności. W tym Codelab pokażemy, jak połączyć aplikację Angular w Cloud Run z bazą danych Cloud SQL dla PostgreSQL za pomocą łącznika Cloud SQL Node.js.

W tym module nauczysz się, jak:

  • Utwórz instancję Cloud SQL for PostgreSQL
  • Wdrażanie w Cloud Run aplikacji, która łączy się z bazą danych Cloud SQL

2. Wymagania wstępne

  1. Jeśli nie masz jeszcze konta Google, utwórz je.
    • Używasz konta osobistego, a nie służbowego ani szkolnego. Konta służbowe i szkolne mogą mieć ograniczenia, które uniemożliwiają włączenie interfejsów API potrzebnych w tym laboratorium.

3. Konfigurowanie projektu

  1. Zaloguj się w konsoli Google Cloud.
  2. Włącz rozliczenia w Cloud Console.
  3. Utwórz nowy projekt lub użyj istniejącego.

4. Otwórz edytor Cloud Shell

  1. Otwórz Edytor Cloud Shell.
  2. Jeśli terminal nie pojawia się u dołu ekranu, otwórz go:
    • Kliknij menu z 3 kreskami Ikona menu z 3 kreskami
    • Kliknij Terminal.
    • Kliknij Nowy terminalOtwieranie nowego terminala w edytorze Cloud Shell.
  3. W terminalu skonfiguruj projekt za pomocą tego polecenia:
    • Format:
      gcloud config set project [PROJECT_ID]
      
    • Przykład:
      gcloud config set project lab-project-id-example
      
    • Jeśli nie pamiętasz identyfikatora projektu:
      • Aby wyświetlić wszystkie identyfikatory projektów, użyj:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Ustawianie identyfikatora projektu w terminalu edytora Cloud Shell
  4. Jeśli pojawi się pytanie o autoryzację, kliknij Autoryzuj, aby kontynuować. Kliknij, aby autoryzować Cloud Shell
  5. Powinien wyświetlić się ten komunikat:
    Updated property [core/project].
    
    Jeśli widzisz WARNING i usłyszysz pytanie Do you want to continue (Y/N)?, prawdopodobnie nieprawidłowo wpisano identyfikator projektu. Naciśnij N, naciśnij Enter i spróbuj ponownie uruchomić polecenie gcloud config set project.

5. Włącz interfejsy API

Włącz w terminalu te interfejsy API:

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

Jeśli pojawi się pytanie o autoryzację, kliknij Autoryzuj, aby kontynuować. Kliknij, aby autoryzować Cloud Shell

Wykonanie tego polecenia może potrwać kilka minut, ale ostatecznie powinno wyświetlić komunikat podobny do tego:

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

6. Konfigurowanie konta usługi

Utwórz i skonfiguruj konto usługi Google Cloud, które będzie używane przez Cloud Run, aby miało ono odpowiednie uprawnienia do nawiązywania połączeń z Cloud SQL.

  1. Aby utworzyć nowe konto usługi, uruchom polecenie gcloud iam service-accounts create w ten sposób:
    gcloud iam service-accounts create quickstart-service-account \
      --display-name="Quickstart Service Account"
    
  2. Uruchom polecenie gcloud projects add-iam-policy-binding w ten sposób, aby dodać do utworzonego właśnie konta usługi Google Cloud rolę Klient 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"
    
  3. Aby dodać do utworzonego właśnie konta usługi Google Cloud rolę Użytkownik instancji Cloud SQL, wykonaj polecenie gcloud projects add-iam-policy-binding w ten sposób:
    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. Aby dodać do utworzonego przed chwilą konta usługi Google Cloud rolę pisarz dzienników, uruchom polecenie gcloud projects add-iam-policy-binding w ten sposób:
    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. Tworzenie bazy danych Cloud SQL

  1. Aby utworzyć instancję Cloud SQL, uruchom polecenie gcloud sql instances create
    gcloud sql instances create quickstart-instance \
        --database-version=POSTGRES_14 \
        --cpu=4 \
        --memory=16GB \
        --region=us-central1 \
        --database-flags=cloudsql.iam_authentication=on
    

Wykonanie tego polecenia może potrwać kilka minut.

  1. Aby utworzyć bazę danych Cloud SQL w ramach usługi quickstart-instance, uruchom polecenie gcloud sql databases create.
    gcloud sql databases create quickstart_db \
        --instance=quickstart-instance
    
  2. Utwórz użytkownika bazy danych PostgreSQL dla utworzonego wcześniej konta usługi, aby uzyskać dostęp do bazy danych.
    gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \
        --instance=quickstart-instance \
        --type=cloud_iam_service_account
    

8. Przygotowanie aplikacji

Przygotuj aplikację Next.js, która odpowiada na żądania HTTP.

  1. Aby utworzyć nowy projekt Next.js o nazwie task-app, użyj polecenia:
    npx --yes @angular/cli@19.2.5 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --server-routing \
        --defaults
    
  2. Zmień katalog na task-app:
    cd task-app
    
  1. Zainstaluj pg i bibliotekę sprzęgającej Cloud SQL Node.js, aby korzystać z bazy danych PostgreSQL.
    npm install pg @google-cloud/cloud-sql-connector google-auth-library
    
  2. Aby używać aplikacji Next.js w TypeScript, zainstaluj @types/pg jako zależność programistyczną.
    npm install --save-dev @types/pg
    
  1. Otwórz plik server.ts w edytorze Cloud Shell:
    cloudshell edit src/server.ts
    
    plik powinien pojawić się w górnej części ekranu. Tutaj możesz edytować plik server.ts. Pokaż, że kod znajduje się w górnej części ekranu
  2. Usuń istniejące treści z pliku server.ts.
  3. Skopiuj ten kod i wklej go do otwartego pliku 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);
    
  1. Otwórz plik app.component.ts w edytorze Cloud Shell:
    cloudshell edit src/app/app.component.ts
    
    W górnej części ekranu powinien pojawić się istniejący plik. Tutaj możesz edytować plik app.component.ts. Pokaż, że kod znajduje się w górnej części ekranu
  2. Usuń istniejące treści z pliku app.component.ts.
  3. Skopiuj ten kod i wklej go do otwartego pliku 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();
      }
    }
    

Aplikacja jest teraz gotowa do wdrożenia.

9. Wdrażanie aplikacji w Cloud Run

  1. Aby wdrożyć aplikację do Cloud Run, uruchom to polecenie:
    gcloud run deploy to-do-tracker \
        --region=us-central1 \
        --source=. \
        --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
        --allow-unauthenticated
    
  2. Jeśli pojawi się taka prośba, naciśnij YEnter, aby potwierdzić, że chcesz kontynuować:
    Do you want to continue (Y/n)? Y
    

Po kilku minutach aplikacja powinna wyświetlić adres URL, który należy otworzyć.

Otwórz adres URL, aby zobaczyć aplikację w akcji. Za każdym razem, gdy otworzysz adres URL lub odświeżysz stronę, zobaczysz aplikację do zadań.

10. Gratulacje

Z tego modułu nauczysz się:

  • Utwórz instancję Cloud SQL for PostgreSQL
  • Wdrażanie w Cloud Run aplikacji, która łączy się z bazą danych Cloud SQL

Czyszczenie danych

Cloud SQL nie ma bezpłatnego poziomu i będzie pobierać opłaty za dalsze korzystanie z usługi. Aby uniknąć dodatkowych opłat, możesz usunąć projekt Cloud.

Cloud Run nie nalicza opłat, gdy usługa nie jest używana, ale może zostać pobrana należność za przechowywanie obrazu kontenera w Artifact Registry. Usunięcie projektu Cloud powoduje zaprzestanie naliczania opłat za wszystkie zasoby używane w tym projekcie.

Jeśli chcesz, możesz usunąć projekt:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Możesz też usunąć niepotrzebne zasoby z dysku CloudShell. Możesz:

  1. Usuń katalog projektu ćwiczeń z programowania:
    rm -rf ~/task-app
    
  2. Ostrzeżenie! Tej czynności nie można cofnąć. Jeśli chcesz usunąć wszystko z Cloud Shell, aby zwolnić miejsce, możesz usunąć cały katalog domowy. Upewnij się, że wszystko, co chcesz zachować, jest zapisane gdzie indziej.
    sudo rm -rf $HOME
    

Ucz się dalej