Full-Stack-JavaScript-Anwendung mit Cloud SQL for PostgreSQL in Cloud Run bereitstellen

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 Next.js-Anwendung in Cloud Run mit einer Cloud SQL for PostgreSQL-Datenbank verbinden.

Aufgaben in diesem Lab:

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

2. Vorbereitung

  1. Wenn Sie noch kein Google-Konto haben, müssen Sie ein Google-Konto erstellen.
    • Verwenden Sie stattdessen ein privates Konto. Bei Arbeitskonten und Konten von Bildungseinrichtungen kann es Einschränkungen geben, die verhindern, dass Sie die für dieses Lab erforderlichen APIs aktivieren.

3. Projekt einrichten

  1. Melden Sie sich in der Google Cloud Console an.
  2. Abrechnung in der Cloud Console aktivieren
    • Die Kosten für Cloud-Ressourcen für dieses Lab sollten weniger als 1 $betragen.
    • Sie können die Schritte am Ende dieses Labs ausführen, um Ressourcen zu löschen und so weitere Kosten zu vermeiden.
    • Neuen Nutzern steht die kostenlose Testversion mit einem Guthaben von 300$ zur Verfügung.
  3. Erstellen Sie ein neues Projekt oder wählen Sie ein vorhandenes Projekt aus.

4. Cloud Shell-Editor öffnen

  1. Cloud Shell-Editor aufrufen
  2. Wenn das Terminal nicht unten auf dem Bildschirm angezeigt wird, öffnen Sie es:
    • Klicken Sie auf das Dreistrich-Menü Symbol für das Dreistrich-Menü.
    • Klicken Sie auf Terminal.
    • Klicken Sie auf Neues TerminalNeues Terminal im Cloud Shell-Editor öffnen.
  3. Legen Sie im Terminal Ihr Projekt mit diesem Befehl fest:
    • Format:
      gcloud config set project [PROJECT_ID]
      
    • Beispiel:
      gcloud config set project lab-project-id-example
      
    • Wenn Sie sich nicht an Ihre Projekt-ID erinnern können,
      • Sie können alle Ihre Projekt-IDs mit folgendem Befehl auflisten:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Projekt-ID im Cloud Shell Editor-Terminal festlegen
  4. Wenn Sie zur Autorisierung 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 Sie WARNING sehen und Do you want to continue (Y/N)? gefragt werden, haben Sie die Projekt-ID wahrscheinlich falsch eingegeben. Drücken Sie N, dann Enter und versuchen Sie, den Befehl gcloud config set project noch einmal auszuführen.

5. APIs aktivieren

Aktivieren Sie die APIs im Terminal:

gcloud services enable \
  compute.googleapis.com \
  sqladmin.googleapis.com \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com \
  networkconnectivity.googleapis.com \
  servicenetworking.googleapis.com \
  cloudaicompanion.googleapis.com

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

Es kann einige Minuten dauern, bis dieser Befehl ausgeführt wird. Wenn die Ausführung erfolgreich war, erhalten Sie eine Meldung, die ungefähr so aussieht:

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

6. Dienstkonto einrichten

Erstellen und konfigurieren Sie ein Google Cloud-Dienstkonto, das von Cloud Run verwendet werden soll, damit es die richtigen Berechtigungen zum Herstellen einer Verbindung zu Cloud SQL hat.

  1. Führen Sie den Befehl gcloud iam service-accounts create so aus, um ein neues Dienstkonto zu erstellen:
    gcloud iam service-accounts create quickstart-service-account \
      --display-name="Quickstart Service Account"
    
  2. Führen Sie den Befehl „gcloud projects add-iam-policy-binding“ wie unten beschrieben aus, um dem soeben erstellten Google Cloud-Dienstkonto die Rolle Logautor hinzuzufügen.
    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. Cloud SQL-Datenbank erstellen

  1. Service Connection Policy erstellen, um die Netzwerkverbindung von Cloud Run zu Cloud SQL mit Private Service Connect zuzulassen
    gcloud network-connectivity service-connection-policies create quickstart-policy \
        --network=default \
        --project=${GOOGLE_CLOUD_PROJECT} \
        --region=us-central1 \
        --service-class=google-cloud-sql \
        --subnets=https://www.googleapis.com/compute/v1/projects/${GOOGLE_CLOUD_PROJECT}/regions/us-central1/subnetworks/default
    
  2. Eindeutiges Passwort für Ihre Datenbank generieren
    export DB_PASSWORD=$(openssl rand -base64 20)
    
  3. Führen Sie den Befehl gcloud sql instances create aus, um eine Cloud SQL-Instanz zu erstellen.
    gcloud sql instances create quickstart-instance \
        --project=${GOOGLE_CLOUD_PROJECT} \
        --root-password=${DB_PASSWORD} \
        --database-version=POSTGRES_17 \
        --tier=db-perf-optimized-N-2 \
        --region=us-central1 \
        --ssl-mode=ENCRYPTED_ONLY \
        --no-assign-ip \
        --enable-private-service-connect \
        --psc-auto-connections=network=projects/${GOOGLE_CLOUD_PROJECT}/global/networks/default
    

Die Ausführung dieses Befehls kann einige Minuten dauern.

  1. Führen Sie den Befehl gcloud sql databases create aus, um eine Cloud SQL-Datenbank in der quickstart-instance zu erstellen.
    gcloud sql databases create quickstart_db \
      --instance=quickstart-instance
    

8. 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 create-next-app@15 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. Ändern Sie das Verzeichnis in task-app:
    cd task-app
    
  3. Installieren Sie pg, um mit der PostgreSQL-Datenbank zu interagieren.
    npm install pg
    
  4. Installieren Sie @types/pg als Entwicklungsabhängigkeit, um TypeScript-Next.js-Anwendungen zu verwenden.
    npm install --save-dev @types/pg
    
  5. Öffnen Sie die Datei actions.ts im Cloud Shell-Editor:
    cloudshell edit app/actions.ts
    
    Oben auf dem Bildschirm sollte jetzt eine leere Datei angezeigt werden. Hier können Sie die Datei actions.ts bearbeiten. Zeigen Sie, dass der Code im oberen Bereich des Bildschirms eingegeben wird.
  6. Kopieren Sie den folgenden Code und fügen Sie ihn in die geöffnete Datei actions.ts ein:
    'use server'
    import pg from 'pg';
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
    };
    
    const { Pool } = pg;
    
    const pool = new Pool({
      host: process.env.DB_HOST,
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      ssl: {
        // @ts-expect-error require true is not recognized by @types/pg, but does exist on pg
        require: true,
        rejectUnauthorized: false, // required for self-signed certs
        // https://node-postgres.com/features/ssl#self-signed-cert
      }
    });
    
    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)
      );`);
    }
    
    // CREATE
    export async function addNewTaskToDatabase(newTask: string) {
      await tableCreationIfDoesNotExist();
      await pool.query(`INSERT INTO tasks(created_at, status, title) VALUES(NOW(), 'IN_PROGRESS', $1)`, [newTask]);
      return;
    }
    
    // READ
    export async function getTasksFromDatabase() {
      await tableCreationIfDoesNotExist();
      const { rows } = await pool.query(`SELECT id, created_at, status, title FROM tasks ORDER BY created_at DESC LIMIT 100`);
      return rows;
    }
    
    // UPDATE
    export async function updateTaskInDatabase(task: Task) {
      await tableCreationIfDoesNotExist();
      await pool.query(
        `UPDATE tasks SET status = $1, title = $2 WHERE id = $3`,
        [task.status, task.title, task.id]
      );
      return;
    }
    
    // DELETE
    export async function deleteTaskFromDatabase(taskId: string) {
      await tableCreationIfDoesNotExist();
      await pool.query(`DELETE FROM tasks WHERE id = $1`, [taskId]);
      return;
    }
    
  7. Öffnen Sie die Datei page.tsx im Cloud Shell-Editor:
    cloudshell edit app/page.tsx
    
    Oben auf dem Bildschirm sollte jetzt eine vorhandene Datei angezeigt werden. Hier können Sie die Datei page.tsx bearbeiten. Zeigen Sie, dass der Code im oberen Bereich des Bildschirms eingegeben wird.
  8. Löschen Sie den vorhandenen Inhalt der Datei page.tsx.
  9. Kopieren Sie den folgenden Code und fügen Sie ihn in die geöffnete Datei page.tsx ein:
    'use client'
    import React, { useEffect, useState } from "react";
    import { addNewTaskToDatabase, getTasksFromDatabase, deleteTaskFromDatabase, updateTaskInDatabase } from "./actions";
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
    };
    
    export default function Home() {
      const [newTaskTitle, setNewTaskTitle] = useState('');
      const [tasks, setTasks] = useState<Task[]>([]);
    
      async function getTasks() {
        const updatedListOfTasks = await getTasksFromDatabase();
        setTasks(updatedListOfTasks);
      }
    
      useEffect(() => {
        getTasks();
      }, []);
    
      async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();
        await addNewTaskToDatabase(newTaskTitle);
        await getTasks();
        setNewTaskTitle('');
      };
    
      async function updateTask(task: Task, newTaskValues: Partial<Task>) {
        await updateTaskInDatabase({ ...task, ...newTaskValues });
        await getTasks();
      }
    
      async function deleteTask(taskId: string) {
        await deleteTaskFromDatabase(taskId);
        await getTasks();
      }
    
      return (
        <main className="p-4">
          <h2 className="text-2xl font-bold mb-4">To Do List</h2>
          <div className="flex mb-4">
            <form onSubmit={handleSubmit} className="flex mb-8">
              <input
                type="text"
                placeholder="New Task Title"
                value={newTaskTitle}
                onChange={(e) => setNewTaskTitle(e.target.value)}
                className="flex-grow border border-gray-400 rounded px-3 py-2 mr-2 bg-inherit"
              />
              <button
                type="submit"
                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-nowrap"
              >
                Add New Task
              </button>
            </form>
          </div>
          <table className="w-full">
            <tbody>
              {tasks.map(function (task) {
                const isComplete = task.status === 'COMPLETE';
                return (
                  <tr key={task.id} className="border-b border-gray-200">
                    <td className="py-2 px-4">
                      <input
                        type="checkbox"
                        checked={isComplete}
                        onClick={() => updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })}
                        className="transition-transform duration-300 ease-in-out transform scale-100 checked:scale-125 checked:bg-green-500"
                      />
                    </td>
                    <td className="py-2 px-4">
                      <span
                        className={`transition-all duration-300 ease-in-out ${isComplete ? 'line-through text-gray-400 opacity-50' : 'opacity-100'}`}
                      >
                        {task.title}
                      </span>
                    </td>
                    <td className="py-2 px-4">
                      <button
                        onClick={() => deleteTask(task.id)}
                        className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded float-right"
                      >
                        Delete
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </main>
      );
    }
    

Die Anwendung kann jetzt bereitgestellt werden.

9. Anwendung in Cloud Run bereitstellen

  1. Führen Sie den Befehl „gcloud projects add-iam-policy-binding“ so aus, um dem Cloud Run-Dienstkonto für den Cloud Run-Dienst, den Sie gerade erstellen, die Rolle Netzwerknutzer hinzuzufügen.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
        --member "serviceAccount:service-$(gcloud projects describe ${GOOGLE_CLOUD_PROJECT} --format="value(projectNumber)")@serverless-robot-prod.iam.gserviceaccount.com" \
        --role "roles/compute.networkUser"
    
  1. Führen Sie den Befehl „gcloud projects add-iam-policy-binding“ wie unten beschrieben aus, um dem aktuellen Nutzer die Rolle Artifact Registry Writer für den Cloud Run-Dienst hinzuzufügen, den Sie gleich erstellen.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
        --member=user:$(gcloud auth list --filter=status:ACTIVE --format="value(account)") \
        --role="roles/artifactregistry.writer"
    
  1. Führen Sie den folgenden Befehl aus, um die Anwendung in Cloud Run bereitzustellen:
    gcloud run deploy helloworld \
      --region=us-central1 \
      --source=. \
      --set-env-vars DB_NAME="quickstart_db" \
      --set-env-vars DB_USER="postgres" \
      --set-env-vars DB_PASSWORD=${DB_PASSWORD} \
      --set-env-vars DB_HOST="$(gcloud sql instances describe quickstart-instance --project=${GOOGLE_CLOUD_PROJECT} --format='value(settings.ipConfiguration.pscConfig.pscAutoConnections.ipAddress)')" \
      --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
      --network=default \
      --subnet=default \
      --allow-unauthenticated
    
  2. Drücken Sie bei Aufforderung 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 bereitstellen, 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.

10. Funktion mit Gemini Code Assist hinzufügen

Sie haben jetzt eine Webanwendung mit einer Datenbank bereitgestellt. Als Nächstes fügen wir unserer Next.js-App mithilfe von KI-Unterstützung eine neue Funktion hinzu.

  1. Zum Cloud Shell-Editor zurückkehren
  2. page.tsx wieder öffnen
    cd ~/task-app
    cloudshell edit app/page.tsx
    
  3. So rufen Sie Gemini Code Assist im Cloud Shell-Editor auf:
    • Klicken Sie in der Symbolleiste auf der linken Seite des Bildschirms auf das Gemini-Symbol Gemini Code Assist-Symbol.
    • Melden Sie sich mit Ihren Google-Konto-Anmeldedaten an, wenn Sie dazu aufgefordert werden.
    • Wenn Sie aufgefordert werden, ein Projekt auszuwählen, wählen Sie das Projekt aus, das Sie für dieses Codelab erstellt haben Projekt für Gemini Code Assist auswählen.
  4. Geben Sie den Prompt Add the ability to update the title of the task. The code in your output should be complete and working code. ein. Die Antwort sollte in etwa so aussehen, um die Funktionen handleEditStart und handleEditCancel hinzuzufügen:
    const [editingTaskId, setEditingTaskId] = useState('');
    const [editedTaskTitle, setEditedTaskTitle] = useState('');
    
    function handleEditStart(task: Task) {
      setEditingTaskId(task.id);
      setEditedTaskTitle(task.title);
    };
    
  5. Ersetzen Sie page.tsx durch die Ausgabe von Gemini Code Assist. Hier ein Beispiel:
    'use client'
    import React, { useEffect, useState } from "react";
    import { addNewTaskToDatabase, getTasksFromDatabase, deleteTaskFromDatabase, updateTaskInDatabase } from "./actions";
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
    };
    
    export default function Home() {
      const [newTaskTitle, setNewTaskTitle] = useState('');
      const [tasks, setTasks] = useState<Task[]>([]);
      const [editingTaskId, setEditingTaskId] = useState('');
      const [editedTaskTitle, setEditedTaskTitle] = useState('');
    
      async function getTasks() {
        const updatedListOfTasks = await getTasksFromDatabase();
        setTasks(updatedListOfTasks);
      }
    
      useEffect(() => {
        getTasks();
      }, []);
    
      async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();
        await addNewTaskToDatabase(newTaskTitle);
        await getTasks();
        setNewTaskTitle('');
      };
    
      async function updateTask(task: Task, newTaskValues: Partial<Task>) {
        await updateTaskInDatabase({ ...task, ...newTaskValues });
        await getTasks();
        setEditingTaskId('');
        setEditedTaskTitle('');
      }
    
      async function deleteTask(taskId: string) {
        await deleteTaskFromDatabase(taskId);
        await getTasks();
      }
    
      function handleEditStart(task: Task) {
        setEditingTaskId(task.id);
        setEditedTaskTitle(task.title);
      };
    
      return (
        <main className="p-4">
          <h2 className="text-2xl font-bold mb-4">To Do List</h2>
          <div className="flex mb-4">
            <form onSubmit={handleSubmit} className="flex mb-8">
              <input
                type="text"
                placeholder="New Task Title"
                value={newTaskTitle}
                onChange={(e) => setNewTaskTitle(e.target.value)}
                className="flex-grow border border-gray-400 rounded px-3 py-2 mr-2 bg-inherit"
              />
              <button
                type="submit"
                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-nowrap"
              >
                Add New Task
              </button>
            </form>
          </div>
          <table className="w-full">
            <tbody>
              {tasks.map(function (task) {
                const isComplete = task.status === 'COMPLETE';
                return (
                  <tr key={task.id} className="border-b border-gray-200">
                    <td className="py-2 px-4">
                      <input
                        type="checkbox"
                        checked={isComplete}
                        onClick={() => updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })}
                        className="transition-transform duration-300 ease-in-out transform scale-100 checked:scale-125 checked:bg-green-500"
                      />
                    </td>
                    <td className="py-2 px-4">
                      {editingTaskId === task.id ? (
                        <form
                          onSubmit={(e) => {
                            e.preventDefault();
                            updateTask(task, { title: editedTaskTitle });
                          }}
                          className="flex"
                        >
                          <input
                            type="text"
                            value={editedTaskTitle}
                            onChange={(e) => setEditedTaskTitle(e.target.value)}
                            onBlur={() => updateTask(task, { title: editedTaskTitle })} // Handle clicking outside input
                            className="flex-grow border border-gray-400 rounded px-3 py-1 mr-2 bg-inherit"
                          />
                        </form>
                      ) : (
                        <span
                          onClick={() => handleEditStart(task)}
                          className={`transition-all duration-300 ease-in-out cursor-pointer ${isComplete ? 'line-through text-gray-400 opacity-50' : 'opacity-100'}`}
                        >
                          {task.title}
                        </span>
                      )}
                    </td>
                    <td className="py-2 px-4">
                      <button
                        onClick={() => deleteTask(task.id)}
                        className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded float-right"
                      >
                        Delete
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </main>
      );
    }
    

11. Anwendung noch einmal 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=. \
      --set-env-vars DB_NAME="quickstart_db" \
      --set-env-vars DB_USER="postgres" \
      --set-env-vars DB_PASSWORD=${DB_PASSWORD} \
      --set-env-vars DB_HOST="$(gcloud sql instances describe quickstart-instance --project=${GOOGLE_CLOUD_PROJECT} --format='value(settings.ipConfiguration.pscConfig.pscAutoConnections.ipAddress)')" \
      --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
      --network=default \
      --subnet=default \
      --allow-unauthenticated
    
  2. Drücken Sie bei Aufforderung Y und Enter, um zu bestätigen, dass Sie fortfahren möchten:
    Do you want to continue (Y/n)? Y
    

12. 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 bietet kein kostenloses Kontingent. Wenn Sie den Dienst weiterhin nutzen, werden Ihnen die Kosten in Rechnung gestellt. Sie können Ihr Cloud-Projekt löschen, um zusätzliche Gebühren zu vermeiden.

Während für Cloud Run keine Kosten anfallen, wenn der Dienst nicht verwendet wird, wird Ihnen dennoch das Speichern des Container-Images in Artifact Registry möglicherweise in Rechnung gestellt. Durch das Löschen des Cloud-Projekts wird die Abrechnung für alle in diesem Projekt verwendeten Ressourcen beendet.

Wenn Sie möchten, können Sie das Projekt löschen:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Möglicherweise möchten Sie auch unnötige Ressourcen von Ihrer Cloud Shell-Festplatte löschen. Sie haben folgende Möglichkeiten:

  1. Löschen Sie das Codelab-Projektverzeichnis:
    rm -rf ~/task-app
    
  2. Warnung! Die 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 gesamtes Basisverzeichnis löschen. Achten Sie darauf, dass alles, was Sie behalten möchten, an einem anderen Ort gespeichert ist.
    sudo rm -rf $HOME
    

Weiterlernen