Esegui il deployment di un'applicazione JavaScript full stack in Cloud Run con Cloud SQL per PostgreSQL

1. Panoramica

Cloud Run è una piattaforma completamente gestita che consente di eseguire il codice direttamente sull'infrastruttura scalabile di Google. Questo Codelab dimostrerà come connettere un'applicazione Next.js su Cloud Run a un database Cloud SQL per PostgreSQL.

In questo lab, imparerai a:

  • Crea un'istanza Cloud SQL per PostgreSQL (configurata per l'utilizzo di Private Service Connect)
  • Esegui il deployment di un'applicazione su Cloud Run che si connette al tuo database Cloud SQL
  • Utilizzare Gemini Code Assist per aggiungere funzionalità all'applicazione

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 \
  compute.googleapis.com \
  sqladmin.googleapis.com \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com \
  networkconnectivity.googleapis.com \
  servicenetworking.googleapis.com \
  cloudaicompanion.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. Configurare un account di servizio

Crea e configura un account di servizio Google Cloud da utilizzare da Cloud Run in modo che disponga delle autorizzazioni corrette per connettersi a Cloud SQL.

  1. Esegui il comando gcloud iam service-accounts create come segue per creare un nuovo account di servizio:
    gcloud iam service-accounts create quickstart-service-account \
      --display-name="Quickstart Service Account"
    
  2. Esegui il comando gcloud projects add-iam-policy-binding come indicato di seguito per aggiungere il ruolo Autori di log all'account di servizio Google Cloud appena creato.
    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. Crea istanza Cloud SQL

  1. Crea un criterio di connessione al servizio per consentire la connettività di rete da Cloud Run a Cloud SQL con 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. Genera una password univoca per il database
    export DB_PASSWORD=$(openssl rand -base64 20)
    
  3. Esegui il comando gcloud sql instances create per creare un'istanza 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
    

Il completamento di questo comando potrebbe richiedere alcuni minuti.

  1. Esegui il comando gcloud sql databases create per creare un database Cloud SQL all'interno di quickstart-instance.
    gcloud sql databases create quickstart_db \
      --instance=quickstart-instance
    

8. 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 create-next-app@15.0.3 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. Se ti viene chiesto di installare create-next-app, premi Enter per continuare:
    Need to install the following packages:
    create-next-app@15.0.3
    Ok to proceed? (y)
    
  3. Cambia la directory in task-app:
    cd task-app
    
  4. Installa pg per interagire con il database PostgreSQL.
    npm install pg
    
  5. Installa @types/pg come dipendenza di sviluppo per utilizzare l'applicazione TypeScript Next.js.
    npm install --save-dev @types/pg
    
  6. Crea il file actions.ts.
    touch app/actions.ts
    
  7. Apri il file actions.ts nell'editor di Cloud Shell:
    cloudshell edit app/actions.ts
    
    Nella parte superiore dello schermo dovrebbe apparire un file vuoto. Qui puoi modificare il file actions.ts. Mostra che il codice deve essere inserito nella sezione superiore dello schermo
  8. Copia il seguente codice e incollalo nel file actions.ts aperto:
    '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. Apri il file page.tsx nell'editor di Cloud Shell:
    cloudshell edit app/page.tsx
    
    Ora nella parte superiore dello schermo dovrebbe apparire un file esistente. Qui puoi modificare il file page.tsx. Mostra che il codice deve essere inserito nella sezione superiore dello schermo
  10. Elimina i contenuti esistenti del file page.tsx.
  11. Copia il seguente codice e incollalo nel file page.tsx aperto:
    '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>
      );
    }
    

L'applicazione è ora pronta per il deployment.

9. Esegui il deployment dell'applicazione in Cloud Run

  1. Esegui il comando gcloud projects add-iam-policy-binding come indicato di seguito per aggiungere il ruolo Utente di rete all'account di servizio Cloud Run per il servizio Cloud Run che stai per creare.
    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. Esegui il comando seguente per eseguire il deployment dell'applicazione in Cloud Run:
    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. 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à.

10. Aggiungere una funzionalità con Gemini Code Assist

Ora hai eseguito il deployment di un'app web con un database. A questo punto, aggiungeremo una nuova funzionalità alla nostra app next.js sfruttando la potenza dell'assistenza IA.

  1. Torna all'editor di Cloud Shell
  2. Apri di nuovo page.tsx
    cd ~/task-app
    cloudshell edit app/page.tsx
    
  3. Vai a Gemini Code Assist nell'editor di Cloud Shell:
    • Fai clic sull'icona di Gemini Icona di Gemini Code Assist nella barra degli strumenti sul lato sinistro dello schermo.
    • Se richiesto, accedi con le credenziali del tuo Account Google
    • Se ti viene chiesto di selezionare un progetto, seleziona il progetto che hai creato per questo Codelab Seleziona progetto Gemini
  4. Inserisci il prompt: Add the ability to update the title of the task. The code in your output should be complete and working code.. La risposta dovrebbe includere qualcosa di simile a questi snippet per aggiungere le funzioni handleEditStart e handleEditCancel:
    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. Sostituisci page.tsx con l'output di Gemini Code Assist. Ecco un esempio funzionante:
    '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. Esegui di nuovo il deployment dell'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=. \
      --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. Se richiesto, premi Y e Enter per confermare che vuoi continuare:
    Do you want to continue (Y/n)? Y
    

12. Complimenti

In questo lab hai imparato a:

  • Crea un'istanza Cloud SQL per PostgreSQL (configurata per l'utilizzo di Private Service Connect)
  • Esegui il deployment di un'applicazione su Cloud Run che si connette al tuo database Cloud SQL
  • Utilizzare Gemini Code Assist per aggiungere funzionalità all'applicazione

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