Esegui il deployment di un'applicazione Angular full stack in Cloud Run con Firestore utilizzando l'SDK Admin di Node.js

Esegui il deployment di un'applicazione Angular full stack in Cloud Run con Firestore utilizzando l'SDK Admin di Node.js

Informazioni su questo codelab

subjectUltimo aggiornamento: apr 2, 2025
account_circleScritto da: Luke Schlangen

1. Panoramica

Cloud Run è una piattaforma completamente gestita che consente di eseguire il codice direttamente sull'infrastruttura scalabile di Google. Questo Codelab dimostrerà come collegare un'applicazione Angular su Cloud Run a un database Firestore utilizzando l'SDK Admin di Node.js.

In questo lab, imparerai a:

  • Crea un database Firestore
  • Esegui il deployment di un'applicazione in Cloud Run che si connette al tuo database Firestore

2. Prerequisiti

  1. Se non hai ancora un Account Google, devi crearne uno.
    • Utilizzi un account personale anziché un account di lavoro o della scuola. Gli account di lavoro e della scuola potrebbero avere limitazioni che ti impediscono di attivare le API necessarie per questo lab.

3. Configurazione del progetto

  1. Accedi alla console Google Cloud.
  2. Attiva la fatturazione in Cloud Console.
    • Il completamento di questo lab dovrebbe costare meno di 1 $in risorse Cloud.
    • Per eliminare le risorse ed evitare ulteriori addebiti, puoi seguire i passaggi alla fine di questo lab.
    • I nuovi utenti sono idonei per la prova senza costi di 300$.
  3. Crea un nuovo progetto o scegli di riutilizzare un progetto esistente.

4. Apri editor di Cloud Shell

  1. Vai a Cloud Shell Editor.
  2. Se il terminale non viene visualizzato nella parte inferiore dello schermo, aprilo:
    • Fai clic sul menu a tre linee Icona del menu di navigazione.
    • Fai clic su Terminale.
    • Fai clic su Nuovo terminaleAprire un nuovo terminale nell'editor di Cloud Shell.
  3. Nel terminale, imposta il progetto con questo comando:
    • Formato:
      gcloud config set project [PROJECT_ID]
    • Esempio:
      gcloud config set project lab-project-id-example
    • Se non ricordi l'ID progetto:
      • Puoi elencare tutti gli ID progetto con:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
      Impostare l'ID progetto nel terminale dell'editor di Cloud Shell
  4. Se ti viene chiesto di concedere l'autorizzazione, fai clic su Autorizza per continuare. Fai clic per autorizzare Cloud Shell
  5. Dovresti vedere questo messaggio:
    Updated property [core/project].
    
    Se vedi un WARNING e ti viene chiesto Do you want to continue (Y/N)?, è probabile che tu abbia inserito l'ID progetto in modo errato. Premi N, premi Enter e prova a eseguire di nuovo il comando gcloud config set project.

5. Abilita API

Nel terminale, abilita le API:

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

Se ti viene chiesto di concedere l'autorizzazione, fai clic su Autorizza per continuare. Fai clic per autorizzare Cloud Shell

Il completamento di questo comando potrebbe richiedere alcuni minuti, ma alla fine dovrebbe essere visualizzato un messaggio di operazione riuscita simile a questo:

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

6. Crea il database Firestore

  1. Esegui il comando gcloud firestore databases create per creare un database Firestore
    gcloud firestore databases create --location=nam5

7. Preparare la richiesta

Prepara un'applicazione Next.js che risponda alle richieste HTTP.

  1. Per creare un nuovo progetto Next.js denominato task-app, utilizza il comando:
    npx --yes @angular/cli@19.2.5 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --server-routing \
        --defaults
  2. Cambia la directory in task-app:
    cd task-app
  1. Installa firebase-admin per interagire con il database Firestore.
    npm install firebase-admin
  1. Apri il file server.ts nell'editor di Cloud Shell:
    cloudshell edit src/server.ts
    Ora nella parte superiore dello schermo dovrebbe apparire un file. Qui puoi modificare il file server.ts. Mostra che il codice deve essere inserito nella sezione superiore dello schermo
  2. Elimina i contenuti esistenti del file server.ts.
  3. Copia il seguente codice e incollalo nel file server.ts aperto:
    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 { initializeApp, applicationDefault, getApps } from 'firebase-admin/app';
    import { getFirestore } from 'firebase-admin/firestore';

    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };

    const credential = applicationDefault();

    // Only initialize app if it does not already exist
    if (getApps().length === 0) {
      initializeApp({ credential });
    }

    const db = getFirestore();
    const tasksRef = db.collection('tasks');

    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) => {
      const snapshot = await tasksRef.orderBy('createdAt', 'desc').limit(100).get();
      const tasks: Task[] = snapshot.docs.map(doc => ({
        id: doc.id,
        title: doc.data()['title'],
        status: doc.data()['status'],
        createdAt: doc.data()['createdAt'],
      }));
      res.send(tasks);
    });

    app.post('/api/tasks', async (req, res) => {
      const newTaskTitle = req.body.title;
      if(!newTaskTitle){
        res.status(400).send("Title is required");
        return;
      }
      await tasksRef.doc().create({
        title: newTaskTitle,
        status: 'IN_PROGRESS',
        createdAt: Date.now(),
      });
      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 tasksRef.doc(task.id).set(task);
      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 tasksRef.doc(task.id).delete();
      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);
  4. Apri il file angular.json nell'editor di Cloud Shell:
    cloudshell edit angular.json
    Ora aggiungeremo la riga "externalDependencies": ["firebase-admin"] al file angular.json.
  5. Elimina i contenuti esistenti del file angular.json.
  6. Copia il seguente codice e incollalo nel file angular.json aperto:
    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "task-app": {
          "projectType": "application",
          "schematics": {
            "@schematics/angular:component": {
              "inlineTemplate": true,
              "inlineStyle": true,
              "skipTests": true
            },
            "@schematics/angular:class": {
              "skipTests": true
            },
            "@schematics/angular:directive": {
              "skipTests": true
            },
            "@schematics/angular:guard": {
              "skipTests": true
            },
            "@schematics/angular:interceptor": {
              "skipTests": true
            },
            "@schematics/angular:pipe": {
              "skipTests": true
            },
            "@schematics/angular:resolver": {
              "skipTests": true
            },
            "@schematics/angular:service": {
              "skipTests": true
            }
          },
          "root": "",
          "sourceRoot": "src",
          "prefix": "app",
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:application",
              "options": {
                "outputPath": "dist/task-app",
                "index": "src/index.html",
                "browser": "src/main.ts",
                "polyfills": [
                  "zone.js"
                ],
                "tsConfig": "tsconfig.app.json",
                "assets": [
                  {
                    "glob": "**/*",
                    "input": "public"
                  }
                ],
                "styles": [
                  "src/styles.css"
                ],
                "scripts": [],
                "server": "src/main.server.ts",
                "outputMode": "server",
                "ssr": {
                  "entry": "src/server.ts"
                },
                "externalDependencies": ["firebase-admin"]
              },
              "configurations": {
                "production": {
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "500kB",
                      "maximumError": "1MB"
                    },
                    {
                      "type": "anyComponentStyle",
                      "maximumWarning": "4kB",
                      "maximumError": "8kB"
                    }
                  ],
                  "outputHashing": "all"
                },
                "development": {
                  "optimization": false,
                  "extractLicenses": false,
                  "sourceMap": true
                }
              },
              "defaultConfiguration": "production"
            },
            "serve": {
              "builder": "@angular-devkit/build-angular:dev-server",
              "configurations": {
                "production": {
                  "buildTarget": "task-app:build:production"
                },
                "development": {
                  "buildTarget": "task-app:build:development"
                }
              },
              "defaultConfiguration": "development"
            },
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n"
            }
          }
        }
      }
    }

"externalDependencies": ["firebase-admin"]

  1. Apri il file app.component.ts nell'editor di Cloud Shell:
    cloudshell edit src/app/app.component.ts
    Ora nella parte superiore dello schermo dovrebbe apparire un file esistente. Qui puoi modificare il file app.component.ts. Mostra che il codice deve essere inserito nella sezione superiore dello schermo
  2. Elimina i contenuti esistenti del file app.component.ts.
  3. Copia il seguente codice e incollalo nel file app.component.ts aperto:
    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();
      }
    }

L'applicazione è ora pronta per il deployment.

8. Esegui il deployment dell&#39;applicazione in Cloud Run

  1. Esegui il comando seguente per eseguire il deployment dell'applicazione in Cloud Run:
    gcloud run deploy helloworld \
      --region=us-central1 \
      --source=.
  2. Se richiesto, premi Y e Enter per confermare che vuoi continuare:
    Do you want to continue (Y/n)? Y
    

Dopo alcuni minuti, l'applicazione dovrebbe fornire un URL da visitare.

Vai all'URL per vedere l'applicazione in azione. Ogni volta che visiti l'URL o aggiorni la pagina, vedrai l'app di attività.

9. Complimenti

In questo lab hai imparato a:

  • Crea un'istanza Cloud SQL per PostgreSQL
  • Esegui il deployment di un'applicazione su Cloud Run che si connette al tuo database Cloud SQL

Esegui la pulizia

Cloud SQL non dispone di un livello senza costi e ti verranno addebitati costi se continui a utilizzarlo. Puoi eliminare il tuo progetto Cloud per evitare di incorrere in addebiti aggiuntivi.

Sebbene non siano previsti addebiti per Cloud Run quando il servizio non è in uso, ti potrebbero comunque essere addebitati i costi di archiviazione dell'immagine container in Artifact Registry. L'eliminazione del progetto Cloud interrompe la fatturazione per tutte le risorse utilizzate al suo interno.

Se vuoi, elimina il progetto:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Ti consigliamo inoltre di eliminare le risorse non necessarie dal disco CloudShell. Puoi:

  1. Elimina la directory del progetto codelab:
    rm -rf ~/task-app
  2. Attenzione. Questa azione non può essere annullata. Se vuoi eliminare tutto su Cloud Shell per liberare spazio, puoi eliminare l'intera home directory. Assicurati che tutto ciò che vuoi conservare sia salvato altrove.
    sudo rm -rf $HOME