Wdrażanie w Cloud Run pełnego pakietu aplikacji JavaScript z Cloud SQL for PostgreSQL

1. Przegląd

Cloud Run to w pełni zarządzana platforma, która umożliwia uruchamianie kodu bezpośrednio w skalowalnej infrastrukturze Google. W tym laboratorium dowiesz się, jak połączyć aplikację Next.js w Cloud Run z bazą danych Cloud SQL dla PostgreSQL.

W tym module nauczysz się, jak:

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

2. Wymagania wstępne

  1. Jeśli nie masz jeszcze konta Google, musisz je utworzyć.
    • Używaj konta osobistego zamiast konta służbowego lub szkolnego. Konta służbowe i szkolne mogą mieć ograniczenia, które uniemożliwiają włączenie interfejsów API potrzebnych w tym module.

3. Konfigurowanie projektu

  1. Zaloguj się w konsoli Google Cloud.
  2. Włącz płatności w konsoli Google Cloud.
  3. Utwórz nowy projekt lub użyj już istniejącego.

4. Otwórz edytor Cloud Shell

  1. Otwórz edytor Cloud Shell.
  2. Jeśli terminal nie pojawi się u dołu ekranu, otwórz go:
    • Kliknij menu Ikona menu z 3 kreskami.
    • Kliknij Terminal.
    • Kliknij Nowy terminalOtwieranie nowego terminala w edytorze Cloud Shell.
  3. W terminalu ustaw 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ć listę wszystkich identyfikatorów projektów, użyj tego polecenia:
        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 przejść dalej. Kliknij, aby uwierzytelnić się w Cloud Shell
  5. Powinien wyświetlić się ten komunikat:
    Updated property [core/project].
    
    Jeśli widzisz symbol WARNING i pojawia się pytanie Do you want to continue (Y/N)?, prawdopodobnie identyfikator projektu został wpisany nieprawidłowo. Naciśnij N, a potem Enter i spróbuj ponownie uruchomić polecenie gcloud config set project.

5. Włącz interfejsy API

W terminalu włącz interfejsy API:

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

Jeśli pojawi się pytanie o autoryzację, kliknij Autoryzuj, aby przejść dalej. Kliknij, aby uwierzytelnić się w Cloud Shell

Wykonanie tego polecenia może potrwać kilka minut, ale powinno ostatecznie wyświetlić komunikat o sukcesie 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 odpowiednie uprawnienia do łączenia się 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. Aby dodać rolę Zapisujący dzienniki do utworzonego przed chwilą konta usługi Google Cloud, 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. Utwórz zasadę połączenia z usługą, aby zezwolić na połączenie sieciowe z Cloud Run do Cloud SQL za pomocą Private Service Connect.
    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. Generowanie unikalnego hasła do bazy danych
    export DB_PASSWORD=$(openssl rand -base64 20)
    
  3. Uruchom polecenie gcloud sql instances create, aby utworzyć instancję Cloud SQL.
    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
    

Wykonanie tego polecenia może potrwać kilka minut.

  1. Aby utworzyć bazę danych Cloud SQL w usłudze quickstart-instance, uruchom polecenie gcloud sql databases create.
    gcloud sql databases create quickstart_db \
      --instance=quickstart-instance
    

8. Przygotowywanie aplikacji

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

  1. Aby utworzyć nowy projekt Next.js o nazwie task-app, użyj tego polecenia:
    npx --yes create-next-app@15 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. Przejdź do katalogu task-app:
    cd task-app
    
  3. Zainstaluj pg, aby korzystać z bazy danych PostgreSQL.
    npm install pg
    
  4. Zainstaluj @types/pg jako zależność deweloperską, aby używać aplikacji Next.js w TypeScript.
    npm install --save-dev @types/pg
    
  5. Otwórz plik actions.ts w edytorze Cloud Shell:
    cloudshell edit app/actions.ts
    
    U góry ekranu powinien pojawić się pusty plik. W tym miejscu możesz edytować plik actions.ts. Pokaż, że kod należy wpisać w górnej części ekranu
  6. Skopiuj ten kod i wklej go do otwartego pliku actions.ts:
    '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. Otwórz plik page.tsx w edytorze Cloud Shell:
    cloudshell edit app/page.tsx
    
    U góry ekranu powinien pojawić się istniejący plik. W tym miejscu możesz edytować plik page.tsx. Pokaż, że kod należy wpisać w górnej części ekranu
  8. Usuń dotychczasową zawartość pliku page.tsx.
  9. Skopiuj ten kod i wklej go do otwartego pliku page.tsx:
    '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>
      );
    }
    

Aplikacja jest teraz gotowa do wdrożenia.

9. Wdrażanie aplikacji w Cloud Run

  1. Aby dodać do konta usługi Cloud Run rolę Użytkownik sieci w przypadku usługi Cloud Run, którą zamierzasz utworzyć, wykonaj to polecenie gcloud projects add-iam-policy-binding:
    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. Aby dodać rolę Zapisujący w Artifact Registry do bieżącego użytkownika w usłudze Cloud Run, którą zamierzasz utworzyć, wykonaj to polecenie:
    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. Aby wdrożyć aplikację w Cloud Run, uruchom to polecenie:
    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. Jeśli pojawi się prośba, naciśnij YEnter, aby potwierdzić, że chcesz kontynuować:
    Do you want to continue (Y/n)? Y
    

Po kilku minutach aplikacja powinna podać adres URL, który możesz otworzyć.

Otwórz adres URL, aby zobaczyć działanie aplikacji. Za każdym razem, gdy otworzysz adres URL lub odświeżysz stronę, zobaczysz aplikację do zarządzania zadaniami.

10. Dodawanie funkcji za pomocą Gemini Code Assist

Aplikacja internetowa z bazą danych została wdrożona. Następnie dodamy nową funkcję do naszej aplikacji next.js, korzystając z pomocy AI.

  1. Powrót do edytora Cloud Shell
  2. Otwórz ponownie page.tsx
    cd ~/task-app
    cloudshell edit app/page.tsx
    
  3. Otwórz Gemini Code Assist w edytorze Cloud Shell:
    • Na pasku narzędzi po lewej stronie ekranu kliknij ikonę Gemini Ikona Gemini Code Assist.
    • Jeśli pojawi się taka prośba, zaloguj się przy użyciu danych logowania na konto Google.
    • Jeśli pojawi się prośba o wybranie projektu, wybierz projekt utworzony na potrzeby tego laboratorium Wybieranie projektu dla Gemini Code Assist.
  4. Wpisz prompt: Add the ability to update the title of the task. The code in your output should be complete and working code.. Odpowiedź powinna zawierać fragmenty kodu podobne do tych, które dodają funkcje handleEditStarthandleEditCancel:
    const [editingTaskId, setEditingTaskId] = useState('');
    const [editedTaskTitle, setEditedTaskTitle] = useState('');
    
    function handleEditStart(task: Task) {
      setEditingTaskId(task.id);
      setEditedTaskTitle(task.title);
    };
    
  5. Zastąp symbol page.tsx wynikiem działania Gemini Code Assist. Oto działający przykład:
    '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. Ponowne wdrażanie aplikacji w Cloud Run

  1. Aby wdrożyć aplikację w Cloud Run, uruchom to polecenie:
    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. Jeśli pojawi się prośba, naciśnij YEnter, aby potwierdzić, że chcesz kontynuować:
    Do you want to continue (Y/n)? Y
    

12. Gratulacje

Z tego modułu dowiesz się, jak:

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

Czyszczenie danych

Cloud SQL nie ma bezpłatnego poziomu i jeśli będziesz z niego korzystać, zostaniesz obciążony opłatami. Aby uniknąć dodatkowych opłat, możesz usunąć projekt w 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 wszelkie zasoby wykorzystywane w ramach tego projektu.

Jeśli chcesz, usuń projekt:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

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

  1. Usuń katalog projektu ćwiczeń z programowania:
    rm -rf ~/task-app
    
  2. Ostrzeżenie! Tej czynności nie będzie 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 w innym miejscu.
    sudo rm -rf $HOME
    

Ucz się dalej