Full-Stack-Angular-Anwendung mit Firestore mit dem Node.js Admin SDK in Cloud Run bereitstellen

Full-Stack-Angular-Anwendung mit Firestore mit dem Node.js Admin SDK in Cloud Run bereitstellen

Informationen zu diesem Codelab

subjectZuletzt aktualisiert: Apr. 2, 2025
account_circleVerfasst von Luke Schlangen

1. Übersicht

Cloud Run ist eine vollständig verwaltete Plattform, mit der Sie Ihren Code direkt auf der skalierbaren Infrastruktur von Google ausführen können. In diesem Codelab wird gezeigt, wie Sie eine Angular-Anwendung in Cloud Run mit einer Firestore-Datenbank über das Node.js Admin SDK verbinden.

Aufgaben in diesem Lab:

  • Firestore-Datenbank erstellen
  • Anwendung in Cloud Run bereitstellen, die eine Verbindung zu Ihrer Firestore-Datenbank herstellt

2. Vorbereitung

  1. Wenn Sie noch kein Google-Konto haben, müssen Sie ein Google-Konto erstellen.
    • Sie verwenden ein privates Konto anstelle eines Arbeits- oder Schulkontos. Für geschäftliche und schulische Konten gelten möglicherweise Einschränkungen, die Sie daran hindern, die für dieses Lab erforderlichen APIs zu aktivieren.

3. Projekt einrichten

  1. Melden Sie sich in der Google Cloud Console an.
  2. Aktivieren Sie die Abrechnung in der Cloud Console.
    • Die Kosten für die Durchführung dieses Labs sollten unter 1 $liegen.
    • Sie können die Schritte am Ende dieses Labs ausführen, um Ressourcen zu löschen und weitere Kosten zu vermeiden.
    • Neue Nutzer können das kostenlose Testabo mit einem Guthaben von 300$ nutzen.
  3. Erstellen Sie ein neues Projekt oder verwenden Sie ein vorhandenes.

4. Cloud Shell-Editor öffnen

  1. Rufen Sie den Cloud Shell-Editor auf.
  2. Wenn das Terminal nicht unten auf dem Bildschirm angezeigt wird, öffnen Sie es:
    • Klicke auf das Dreistrich-Menü Dreistrich-Menüsymbol.
    • Klicken Sie auf Terminal.
    • Klicken Sie auf Neues TerminalNeues Terminal im Cloud Shell-Editor öffnen.
  3. Legen Sie das Projekt im Terminal mit diesem Befehl fest:
    • Format:
      gcloud config set project [PROJECT_ID]
    • Beispiel:
      gcloud config set project lab-project-id-example
    • Wenn Sie sich Ihre Projekt-ID nicht merken können:
      • So listen Sie alle Ihre Projekt-IDs auf:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
      Projekt-ID im Cloud Shell-Editor-Terminal festlegen
  4. Wenn Sie dazu aufgefordert werden, klicken Sie auf Autorisieren, um fortzufahren. Klicken Sie, um Cloud Shell zu autorisieren.
  5. Es sollte folgende Meldung angezeigt werden:
    Updated property [core/project].
    
    Wenn du die Meldung WARNING und die Frage Do you want to continue (Y/N)? siehst, hast du wahrscheinlich die Projekt-ID falsch eingegeben. Drücken Sie N, Enter und versuchen Sie noch einmal, den Befehl gcloud config set project auszuführen.

5. APIs aktivieren

Aktivieren Sie im Terminal die APIs:

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

Wenn Sie dazu aufgefordert werden, klicken Sie auf Autorisieren, um fortzufahren. Klicken Sie, um Cloud Shell zu autorisieren.

Die Verarbeitung dieses Befehls kann einige Minuten dauern. Sie sollten aber schließlich eine Meldung ähnlich der folgenden erhalten:

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

6. Firestore-Datenbank erstellen

  1. Befehl gcloud firestore databases create ausführen, um eine Firestore-Datenbank zu erstellen
    gcloud firestore databases create --location=nam5

7. Anwendung vorbereiten

Bereiten Sie eine Next.js-Anwendung vor, die auf HTTP-Anfragen reagiert.

  1. Verwenden Sie den folgenden Befehl, um ein neues Next.js-Projekt mit dem Namen task-app zu erstellen:
    npx --yes @angular/cli@19.2.5 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --server-routing \
        --defaults
  2. Ändern Sie das Verzeichnis in task-app:
    cd task-app
  1. Installieren Sie firebase-admin, um mit der Firestore-Datenbank zu interagieren.
    npm install firebase-admin
  1. Öffnen Sie die Datei server.ts im Cloud Shell-Editor:
    cloudshell edit src/server.ts
    Oben auf dem Bildschirm sollte jetzt eine Datei angezeigt werden. Hier können Sie die Datei server.ts bearbeiten. Der Code wird oben auf dem Bildschirm angezeigt.
  2. Löschen Sie den vorhandenen Inhalt der Datei server.ts.
  3. Kopieren Sie den folgenden Code und fügen Sie ihn in die geöffnete server.ts-Datei ein:
    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. Öffnen Sie die Datei angular.json im Cloud Shell-Editor:
    cloudshell edit angular.json
    Fügen wir der Datei angular.json jetzt die Zeile "externalDependencies": ["firebase-admin"] hinzu.
  5. Löschen Sie den vorhandenen Inhalt der Datei angular.json.
  6. Kopieren Sie den folgenden Code und fügen Sie ihn in die geöffnete angular.json-Datei ein:
    {
      "$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. Öffnen Sie die Datei app.component.ts im Cloud Shell-Editor:
    cloudshell edit src/app/app.component.ts
    Oben auf dem Bildschirm sollte jetzt eine vorhandene Datei angezeigt werden. Hier können Sie die Datei app.component.ts bearbeiten. Der Code wird oben auf dem Bildschirm angezeigt.
  2. Löschen Sie den vorhandenen Inhalt der Datei app.component.ts.
  3. Kopieren Sie den folgenden Code und fügen Sie ihn in die geöffnete app.component.ts-Datei ein:
    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();
      }
    }

Die Anwendung kann jetzt bereitgestellt werden.

8. Anwendung in Cloud Run bereitstellen

  1. Führen Sie den folgenden Befehl aus, um die Anwendung in Cloud Run bereitzustellen:
    gcloud run deploy helloworld \
      --region=us-central1 \
      --source=.
  2. Wenn Sie dazu aufgefordert werden, drücken Sie Y und Enter, um zu bestätigen, dass Sie fortfahren möchten:
    Do you want to continue (Y/n)? Y
    

Nach einigen Minuten sollte die Anwendung eine URL anzeigen, die Sie aufrufen können.

Rufen Sie die URL auf, um Ihre Anwendung in Aktion zu sehen. Jedes Mal, wenn Sie die URL aufrufen oder die Seite aktualisieren, wird die Aufgaben-App angezeigt.

9. Glückwunsch

In diesem Lab haben Sie Folgendes gelernt:

  • Cloud SQL for PostgreSQL-Instanz erstellen
  • Anwendung in Cloud Run bereitstellen, die eine Verbindung zu Ihrer Cloud SQL-Datenbank herstellt

Bereinigen

Cloud SQL ist nicht kostenlos. Wenn Sie die Plattform weiter nutzen, werden Ihnen Kosten in Rechnung gestellt. Sie können Ihr Cloud-Projekt löschen, um zusätzliche Kosten zu vermeiden.

Für Cloud Run fallen keine Kosten an, wenn der Dienst nicht verwendet wird. Allerdings können für das Speichern des Container-Images in Artifact Registry Gebühren berechnet werden. Wenn Sie Ihr Cloud-Projekt löschen, wird die Abrechnung für alle in diesem Projekt verwendeten Ressourcen beendet.

Sie können das Projekt löschen:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Sie können auch unnötige Ressourcen von Ihrem CloudShell-Laufwerk löschen. Sie haben folgende Möglichkeiten:

  1. Löschen Sie das Codelab-Projektverzeichnis:
    rm -rf ~/task-app
  2. Warnung! Diese nächste Aktion kann nicht rückgängig gemacht werden. Wenn Sie alles in Cloud Shell löschen möchten, um Speicherplatz freizugeben, können Sie Ihr ganzes Basisverzeichnis löschen. Achten Sie darauf, dass alles, was Sie behalten möchten, an einem anderen Ort gespeichert ist.
    sudo rm -rf $HOME