Implantar um aplicativo Angular de stack completa no Cloud Run com o Cloud SQL para PostgreSQL usando o conector Node.js do Cloud SQL

1. Visão geral

O Cloud Run é uma plataforma totalmente gerenciada que permite executar seu código diretamente na infraestrutura escalonável do Google. Este codelab vai demonstrar como conectar um aplicativo Angular no Cloud Run a um banco de dados do Cloud SQL para PostgreSQL usando o conector Node.js do Cloud SQL.

Neste laboratório, você vai aprender a:

  • Criar uma instância do Cloud SQL para PostgreSQL
  • Implantar um aplicativo no Cloud Run que se conecte ao seu banco de dados do Cloud SQL

2. Pré-requisitos

  1. Se você ainda não tem uma Conta do Google, crie uma.
    • Use uma conta pessoal em vez de uma conta de trabalho ou escolar. As contas escolares e de trabalho podem ter restrições que impedem a ativação das APIs necessárias para este laboratório.

3. Configurar o projeto

  1. Faça login no Console do Google Cloud.
  2. Ative o faturamento no Console do Cloud.
    • A conclusão deste laboratório deve custar menos de US $1 em recursos do Cloud.
    • Siga as etapas no final deste laboratório para excluir recursos e evitar cobranças.
    • Novos usuários estão qualificados para o teste sem custo financeiro de US$300.
  3. Crie um novo projeto ou escolha reutilizar um projeto atual.

4. Abrir editor do Cloud Shell

  1. Acesse o editor do Cloud Shell.
  2. Se o terminal não aparecer na parte de baixo da tela, abra-o:
    • Clique no menu de navegação Ícone do menu de navegação
    • Clique em Terminal.
    • Clique em Novo Terminal.Abrir um novo terminal no editor do Cloud Shell
  3. No terminal, defina seu projeto com este comando:
    • Formato:
      gcloud config set project [PROJECT_ID]
      
    • Exemplo:
      gcloud config set project lab-project-id-example
      
    • Se você não souber o ID do projeto:
      • É possível listar todos os IDs de projetos com:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Definir o ID do projeto no terminal do Editor do Cloud Shell
  4. Se for necessário autorizar, clique em Autorizar para continuar. Clique para autorizar o Cloud Shell
  5. Você vai receber esta mensagem:
    Updated property [core/project].
    
    Se você receber uma WARNING e receber uma solicitação Do you want to continue (Y/N)?, provavelmente inseriu o ID do projeto incorretamente. Pressione N, pressione Enter e tente executar o comando gcloud config set project novamente.

5. Ativar APIs

No terminal, ative as APIs:

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

Se for necessário autorizar, clique em Autorizar para continuar. Clique para autorizar o Cloud Shell

Esse comando pode levar alguns minutos para ser concluído, mas vai gerar uma mensagem de sucesso semelhante a esta:

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

6. Configurar uma conta de serviço

Crie e configure uma conta de serviço do Google Cloud para ser usada pelo Cloud Run, de modo que ela tenha as permissões corretas para se conectar ao Cloud SQL.

  1. Execute o comando gcloud iam service-accounts create da seguinte maneira para criar uma nova conta de serviço:
    gcloud iam service-accounts create quickstart-service-account \
      --display-name="Quickstart Service Account"
    
  2. Execute o comando gcloud projects add-iam-policy-binding da seguinte maneira para adicionar o papel Cliente do Cloud SQL à conta de serviço do Google Cloud que você acabou de criar.
    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. Execute o comando gcloud projects add-iam-policy-binding da seguinte maneira para adicionar o papel Usuário da instância do Cloud SQL à conta de serviço do Google Cloud que você acabou de criar.
    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. Execute o comando gcloud projects add-iam-policy-binding da seguinte maneira para adicionar o papel de Gravador de registros à conta de serviço do Google Cloud que você acabou de criar.
    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. Criar um banco de dados do Cloud SQL

  1. Execute o comando gcloud sql instances create para criar uma instância do 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
    

Isso pode demorar alguns minutos.

  1. Execute o comando gcloud sql databases create para criar um banco de dados do Cloud SQL no quickstart-instance.
    gcloud sql databases create quickstart_db \
        --instance=quickstart-instance
    
  2. Crie um usuário do banco de dados PostgreSQL para a conta de serviço que você criou anteriormente para acessar o banco de dados.
    gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \
        --instance=quickstart-instance \
        --type=cloud_iam_service_account
    

8. Preparar a inscrição

Prepare um aplicativo Next.js que responda a solicitações HTTP.

  1. Para criar um novo projeto Next.js chamado task-app, use o comando:
    npx --yes @angular/cli@19.2.5 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --server-routing \
        --defaults
    
  2. Altere o diretório para task-app:
    cd task-app
    
  1. Instale pg e a biblioteca de conector do Node.js do Cloud SQL para interagir com o banco de dados PostgreSQL.
    npm install pg @google-cloud/cloud-sql-connector google-auth-library
    
  2. Instale o @types/pg como uma dependência de desenvolvimento para usar um aplicativo TypeScript Next.js.
    npm install --save-dev @types/pg
    
  1. Abra o arquivo server.ts no editor do Cloud Shell:
    cloudshell edit src/server.ts
    
    Um arquivo vai aparecer na parte de cima da tela. É aqui que você pode editar o arquivo server.ts. Mostre que o código vai na seção de cima da tela
  2. Exclua o conteúdo do arquivo server.ts.
  3. Copie e cole o código abaixo no arquivo server.ts aberto:
    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. Abra o arquivo app.component.ts no editor do Cloud Shell:
    cloudshell edit src/app/app.component.ts
    
    Um arquivo vai aparecer na parte de cima da tela. É aqui que você pode editar o arquivo app.component.ts. Mostre que o código vai na seção de cima da tela
  2. Exclua o conteúdo do arquivo app.component.ts.
  3. Copie e cole o código abaixo no arquivo app.component.ts aberto:
    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();
      }
    }
    

O aplicativo está pronto para ser implantado.

9. Implantar o aplicativo no Cloud Run

  1. Execute o comando abaixo para implantar seu aplicativo no 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. Se necessário, pressione Y e Enter para confirmar que você quer continuar:
    Do you want to continue (Y/n)? Y
    

Após alguns minutos, o aplicativo vai fornecer um URL para você visitar.

Navegue até o URL para conferir o aplicativo em ação. O app de tarefas vai aparecer sempre que você visitar o URL ou atualizar a página.

10. Parabéns

Neste laboratório, você aprendeu a:

  • Criar uma instância do Cloud SQL para PostgreSQL
  • Implantar um aplicativo no Cloud Run que se conecte ao seu banco de dados do Cloud SQL

Limpar

O Cloud SQL não tem um nível sem custo financeiro e vai cobrar se você continuar usando. Você pode excluir seu projeto do Cloud para evitar cobranças adicionais.

O Cloud Run não gera custos quando o serviço não está em uso, mas você ainda pode receber cobranças pelo armazenamento da imagem do contêiner no Artifact Registry. A exclusão do projeto do Cloud interrompe o faturamento de todos os recursos usados nesse projeto.

Se quiser, exclua o projeto:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Você também pode excluir recursos desnecessários do disco do cloudshell. Você pode:

  1. Exclua o diretório do projeto do codelab:
    rm -rf ~/task-app
    
  2. Aviso: Não é possível desfazer esta ação. Se você quiser excluir tudo no Cloud Shell para liberar espaço, exclua o diretório principal inteiro. Confira se tudo o que você quer manter está salvo em outro lugar.
    sudo rm -rf $HOME
    

Continue aprendendo