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

1. Omówienie

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

W tym module nauczysz się, jak:

  • Tworzenie instancji Cloud SQL for PostgreSQL (skonfigurowanej do korzystania z funkcji Private Service Connect)
  • Wdrażanie w Cloud Run aplikacji, która łączy się z bazą danych Cloud SQL
  • Dodawanie funkcji do aplikacji za pomocą Gemini Code Assist

2. Wymagania wstępne

  1. Jeśli nie masz jeszcze konta Google, utwórz je.
    • Używasz konta osobistego, a nie służbowego ani szkolnego. Konta służbowe i szkolne mogą mieć ograniczenia, które uniemożliwiają włączenie interfejsów API potrzebnych w tym laboratorium.

3. Konfigurowanie projektu

  1. Zaloguj się w konsoli Google Cloud.
  2. Włącz rozliczenia w Cloud Console.
  3. Utwórz nowy projekt lub użyj istniejącego.

4. Otwórz edytor Cloud Shell

  1. Otwórz Edytor Cloud Shell.
  2. Jeśli terminal nie pojawia się u dołu ekranu, otwórz go:
    • Kliknij menu z 3 kreskami Ikona menu z 3 kreskami
    • Kliknij Terminal.
    • Kliknij Nowy terminalOtwórz nowy terminal 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ć wszystkie identyfikatory projektów, użyj:
        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 kontynuować. Kliknij, aby autoryzować Cloud Shell
  5. Powinien wyświetlić się ten komunikat:
    Updated property [core/project].
    
    Jeśli widzisz WARNING i pojawia się pytanie Do you want to continue (Y/N)?, prawdopodobnie nieprawidłowo wpisano identyfikator projektu. Naciśnij N, naciśnij Enter i spróbuj ponownie uruchomić polecenie gcloud config set project.

5. Włącz interfejsy API

Włącz w terminalu te 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 kontynuować. Kliknij, aby autoryzować Cloud Shell

Wykonanie tego polecenia może potrwać kilka minut, ale ostatecznie powinno wyświetlić komunikat 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 ono odpowiednie uprawnienia do nawiązywania połączeń 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ć do utworzonego przed chwilą konta usługi Google Cloud rolę pisarz dzienników, 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 instancji 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ą funkcji 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. Aby utworzyć instancję Cloud SQL, uruchom polecenie gcloud sql instances create
    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 ramach usługi quickstart-instance, uruchom polecenie gcloud sql databases create.
    gcloud sql databases create quickstart_db \
      --instance=quickstart-instance
    

8. Przygotowanie aplikacji

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

  1. Aby utworzyć nowy projekt Next.js o nazwie task-app, użyj polecenia:
    npx create-next-app@15.0.3 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. Jeśli pojawi się prośba o zainstalowanie create-next-app, naciśnij Enter, aby kontynuować:
    Need to install the following packages:
    create-next-app@15.0.3
    Ok to proceed? (y)
    
  3. Zmień katalog na task-app:
    cd task-app
    
  4. Zainstaluj pg, aby korzystać z bazy danych PostgreSQL.
    npm install pg
    
  5. Aby używać aplikacji Next.js w TypeScript, zainstaluj @types/pg jako zależność programistyczną.
    npm install --save-dev @types/pg
    
  6. Utwórz plik actions.ts.
    touch app/actions.ts
    
  7. Otwórz plik actions.ts w edytorze Cloud Shell:
    cloudshell edit app/actions.ts
    
    W górnej części ekranu powinien pojawić się pusty plik. Tutaj możesz edytować plik actions.ts. Pokaż, że kod znajduje się w górnej części ekranu
  8. 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 visits (
        id SERIAL NOT NULL,
        created_at timestamp NOT NULL,
        PRIMARY KEY (id)
      );`);
      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`);
      console.table(rows); // logs the last 5 visits on the server
      return rows; // sends the last 5 visits to the client
    }
    
    // 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;
    }
    
  9. Otwórz plik page.tsx w edytorze Cloud Shell:
    cloudshell edit app/page.tsx
    
    Istniejący plik powinien pojawić się w górnej części ekranu. Tutaj możesz edytować plik page.tsx. Pokaż, że kod znajduje się w górnej części ekranu
  10. Usuń istniejące treści z pliku page.tsx.
  11. 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 addTask() {
        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"
                      >
                        Delete
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </main>
      );
    }
    

Aplikacja jest teraz gotowa do wdrożenia.

9. Wdrażanie aplikacji w Cloud Run

  1. Aby dodać rolę Użytkownik sieci do konta usługi Cloud Run dla tworzonej usługi Cloud Run, wykonaj polecenie gcloud projects add-iam-policy-binding w ten sposób:
    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 wdrożyć aplikację do 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ę taka prośba, naciśnij YEnter, aby potwierdzić, że chcesz kontynuować:
    Do you want to continue (Y/n)? Y
    

Po kilku minutach aplikacja powinna wyświetlić adres URL, który należy otworzyć.

Otwórz adres URL, aby zobaczyć aplikację w akcji. Za każdym razem, gdy otworzysz adres URL lub odświeżysz stronę, zobaczysz aplikację do zadań.

10. Dodawanie funkcji za pomocą Gemini Code Assist

Aplikacja internetowa z bazą danych została wdrożona. Następnie dodamy do aplikacji next.js nową funkcję, która będzie korzystać z mocy AI.

  1. Wróć 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:
    • Kliknij ikonę Gemini Ikona Gemini Code Assist na pasku narzędzi po lewej stronie ekranu.
    • Jeśli pojawi się taka prośba, zaloguj się przy użyciu danych logowania do konta Google.
    • Jeśli pojawi się prośba o wybranie projektu, wybierz projekt utworzony na potrzeby tego Codelab.Wybierz projekt Gemini
  4. Wpisz prompt: Add the ability to update the title of the task. The code in your output should be complete and working code.. Aby dodać funkcje handleEditStarthandleEditCancel, odpowiedź powinna zawierać fragmenty kodu podobne do tych:
    const [editingTaskId, setEditingTaskId] = useState<string>('');
    const [editedTaskTitle, setEditedTaskTitle] = useState('');
    
    const handleEditStart = (task: Task) => {
      setEditingTaskId(task.id);
      setEditedTaskTitle(task.title);
    };
    
    const handleEditCancel = () => {
      setEditingTaskId('');
      setEditedTaskTitle('');
    };
    
    <td className="py-2 px-4">
      {editingTaskId === task.id ? (
        <form onSubmit={(e) => {
          e.preventDefault();
          updateTask(task, { title: editedTaskTitle });
        }}>
          <input
            type="text"
            value={editedTaskTitle}
            onChange={(e) => setEditedTaskTitle(e.target.value)}
            onBlur={() => handleEditCancel} // Handle clicking outside input
            className="border border-gray-400 rounded px-3 py-1 mr-2"
          />
            <button type="submit" className="text-green-600 hover:text-green-900 mr-1">Save</button>
        </form>
    
      ) : (
        <span onClick={() => handleEditStart(task)} className="cursor-pointer">
          {task.title}
        </span>
      )}
    </td>
    
  5. Zastąp page.tsx wynikiem 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<string>('');
      const [editedTaskTitle, setEditedTaskTitle] = useState('');
    
      async function getTasks() {
        const updatedListOfTasks = await getTasksFromDatabase();
        setTasks(updatedListOfTasks);
      }
    
      useEffect(() => {
        getTasks();
      }, []);
    
      async function addTask() {
        await addNewTaskToDatabase(newTaskTitle);
        await getTasks();
        setNewTaskTitle('');
      }
    
      async function updateTask(task: Task, newTaskValues: Partial<Task>) {
        await updateTaskInDatabase({ ...task, ...newTaskValues });
        await getTasks();
        setEditingTaskId(''); // Clear editing state after update
        setEditedTaskTitle('');
      }
    
      async function deleteTask(taskId: string) {
        await deleteTaskFromDatabase(taskId);
        await getTasks();
      }
    
      const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        addTask();
      };
    
    
      const handleEditStart = (task: Task) => {
        setEditingTaskId(task.id);
        setEditedTaskTitle(task.title);
      };
    
      const handleEditCancel = () => {
        setEditingTaskId('');
        setEditedTaskTitle('');
      };
    
      return (
        <main className="p-4">
          <h2 className="text-2xl font-bold mb-4">To Do List</h2>
          <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>
          <table className="w-full">
            <tbody>
              {tasks.map(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={() => handleEditCancel()} // Handle clicking outside input
                            className="flex-grow border border-gray-400 rounded px-3 py-1 mr-2 bg-inherit"
                          />
                          <button
                            type="submit"
                            className="bg-green-600 hover:bg-green-900 m-1 text-white font-bold py-2 px-4 rounded"
                          >
                            Save
                          </button>
                        </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. Ponownie wdrożenie aplikacji w Cloud Run

  1. Aby wdrożyć aplikację do 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ę taka prośba, naciśnij YEnter, aby potwierdzić, że chcesz kontynuować:
    Do you want to continue (Y/n)? Y
    

12. Gratulacje

Z tego modułu nauczysz się:

  • Tworzenie instancji Cloud SQL for PostgreSQL (skonfigurowanej do korzystania z funkcji Private Service Connect)
  • Wdrażanie w Cloud Run aplikacji, która łączy się z bazą danych Cloud SQL
  • Dodawanie funkcji do aplikacji za pomocą Gemini Code Assist

Czyszczenie danych

Cloud SQL nie ma bezpłatnego poziomu i będzie pobierać opłaty za dalsze korzystanie z usługi. Aby uniknąć dodatkowych opłat, możesz usunąć projekt 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 wszystkie zasoby używane w tym projekcie.

Jeśli chcesz, możesz usunąć projekt:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

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

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