Implementa una aplicación de pila completa de Next.js en Cloud Run con Cloud SQL para PostgreSQL mediante el conector de Node.js de Cloud SQL

1. Descripción general

Cloud Run es una plataforma completamente administrada que te permite ejecutar tu código directamente en la infraestructura escalable de Google. En este codelab, se mostrará cómo conectar una aplicación de Next.js en Cloud Run a una base de datos de Cloud SQL para PostgreSQL con el conector de Node.js de Cloud SQL.

En este lab, aprenderás a hacer lo siguiente:

  • Crea una instancia de Cloud SQL para PostgreSQL
  • Implementa una aplicación en Cloud Run que se conecte a tu base de datos de Cloud SQL

2. Requisitos previos

  1. Si aún no tienes una Cuenta de Google, debes crear una.
    • Usa una cuenta personal en lugar de una cuenta de trabajo o de institución educativa. Es posible que las cuentas de trabajo o de instituciones educativas tengan restricciones que te impidan habilitar las APIs necesarias para este lab.

3. Configura el proyecto

  1. Accede a la consola de Google Cloud.
  2. Habilita la facturación en la consola de Cloud.
    • Completar este lab debería costar menos de USD 1 en recursos de Cloud.
    • Puedes seguir los pasos que se indican al final de este lab para borrar recursos y evitar cargos adicionales.
    • Los usuarios nuevos son aptos para la prueba gratuita de USD 300.
  3. Crea un proyecto nuevo o elige reutilizar uno existente.

4. Abrir el editor de Cloud Shell

  1. Navega al Editor de Cloud Shell.
  2. Si la terminal no aparece en la parte inferior de la pantalla, ábrela:
    • Haz clic en el menú de opciones Ícono de menú de opciones.
    • Haz clic en Terminal.
    • Haz clic en Terminal nueva. Cómo abrir una terminal nueva en el editor de Cloud Shell
  3. En la terminal, configura tu proyecto con este comando:
    • Formato:
      gcloud config set project [PROJECT_ID]
      
    • Ejemplo:
      gcloud config set project lab-project-id-example
      
    • Si no recuerdas el ID de tu proyecto, haz lo siguiente:
      • Puedes enumerar todos los IDs de tu proyecto con lo siguiente:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Establece el ID del proyecto en la terminal del Editor de Cloud Shell
  4. Si se te solicita autorización, haz clic en Autorizar para continuar. Haz clic para autorizar Cloud Shell
  5. Deberías ver el siguiente mensaje:
    Updated property [core/project].
    
    Si ves un WARNING y se te pregunta Do you want to continue (Y/N)?, es probable que hayas ingresado el ID del proyecto de forma incorrecta. Presiona N, presiona Enter y vuelve a intentar ejecutar el comando gcloud config set project.

5. Habilita las APIs

En la terminal, habilita las APIs:

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

Si se te solicita autorización, haz clic en Autorizar para continuar. Haz clic para autorizar Cloud Shell

Este comando puede tardar unos minutos en completarse, pero, en algún momento, debería mostrar un mensaje de éxito similar al siguiente:

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

6. Configura una cuenta de servicio

Crea y configura una cuenta de servicio de Google Cloud que Cloud Run pueda usar para que tenga los permisos correctos para conectarse a Cloud SQL.

  1. Ejecuta el comando gcloud iam service-accounts create de la siguiente manera para crear una cuenta de servicio nueva:
    gcloud iam service-accounts create quickstart-service-account \
      --display-name="Quickstart Service Account"
    
  2. Ejecuta el comando gcloud projects add-iam-policy-binding de la siguiente manera para agregar el rol de cliente de Cloud SQL a la cuenta de servicio de Google Cloud que acabas de crear.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
      --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
      --role="roles/cloudsql.client"
    
  3. Ejecuta el comando gcloud projects add-iam-policy-binding de la siguiente manera para agregar el rol de usuario de la instancia de Cloud SQL a la cuenta de servicio de Google Cloud que acabas de crear.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
      --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
      --role="roles/cloudsql.instanceUser"
    
  4. Ejecuta el comando gcloud projects add-iam-policy-binding de la siguiente manera para agregar el rol de escritor de registros a la cuenta de servicio de Google Cloud que acabas de crear.
    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 una base de datos de Cloud SQL

  1. Ejecuta el comando gcloud sql instances create para crear una instancia de Cloud SQL
    gcloud sql instances create quickstart-instance \
        --database-version=POSTGRES_14 \
        --cpu=4 \
        --memory=16GB \
        --region=us-central1 \
        --database-flags=cloudsql.iam_authentication=on
    

Este comando puede tardar unos minutos en completarse.

  1. Ejecuta el comando gcloud sql databases create para crear una base de datos de Cloud SQL dentro de quickstart-instance.
    gcloud sql databases create quickstart_db \
        --instance=quickstart-instance
    
  2. Crea un usuario de la base de datos de PostgreSQL para la cuenta de servicio que creaste antes para acceder a la base de datos.
    gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \
        --instance=quickstart-instance \
        --type=cloud_iam_service_account
    

8. Prepara la solicitud

Prepara una aplicación de Next.js que responda a las solicitudes HTTP.

  1. Para crear un proyecto de Next.js nuevo llamado task-app, usa el siguiente comando:
    npx --yes create-next-app@15.2.4 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. Cambia el directorio a task-app:
    cd task-app
    
  1. Instala pg y la biblioteca del conector de Node.js de Cloud SQL para interactuar con la base de datos de PostgreSQL.
    npm install pg @google-cloud/cloud-sql-connector google-auth-library
    
  2. Instala @types/pg como dependencia de dev para usar una aplicación de TypeScript Next.js.
    npm install --save-dev @types/pg
    
  1. Abre el archivo actions.ts en el editor de código de Cloud Shell:
    cloudshell edit app/actions.ts
    
    Ahora debería aparecer un archivo vacío en la parte superior de la pantalla. Aquí es donde puedes editar el archivo actions.ts. Muestra que el código va en la sección superior de la pantalla
  2. Copia el siguiente código y pégalo en el archivo actions.ts abierto:
    'use server'
    import pg from 'pg';
    import { AuthTypes, Connector } from '@google-cloud/cloud-sql-connector';
    import { GoogleAuth } from 'google-auth-library';
    const auth = new GoogleAuth();
    
    const { Pool } = pg;
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
    };
    
    const projectId = await auth.getProjectId();
    
    const connector = new Connector();
    const clientOpts = await connector.getOptions({
      instanceConnectionName: `${projectId}:us-central1:quickstart-instance`,
      authType: AuthTypes.IAM,
    });
    
    const pool = new Pool({
      ...clientOpts,
      user: `quickstart-service-account@${projectId}.iam`,
      database: 'quickstart_db',
    });
    
    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;
    }
    
  1. Abre el archivo page.tsx en el editor de código de Cloud Shell:
    cloudshell edit app/page.tsx
    
    Ahora debería aparecer un archivo existente en la parte superior de la pantalla. Aquí es donde puedes editar el archivo page.tsx. Muestra que el código va en la sección superior de la pantalla
  2. Borra el contenido existente del archivo page.tsx.
  3. Copia el siguiente código y pégalo en el archivo page.tsx abierto:
    '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';
      createdAt: number;
    };
    
    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}
                        onChange={() => 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>
      );
    }
    

La aplicación ya está lista para implementarse.

9. Implementa la aplicación en Cloud Run

  1. Ejecuta el siguiente comando para implementar tu aplicación en Cloud Run:
    gcloud run deploy to-do-tracker \
        --region=us-central1 \
        --source=. \
        --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
        --allow-unauthenticated
    
  2. Si se te solicita, presiona Y y Enter para confirmar que deseas continuar:
    Do you want to continue (Y/n)? Y
    

Después de unos minutos, la aplicación debería proporcionarte una URL para que la visites.

Navega a la URL para ver tu aplicación en acción. Cada vez que visites la URL o actualices la página, verás la app de tareas.

10. Felicitaciones

En este lab, aprendiste a hacer las siguientes tareas:

  • Crea una instancia de Cloud SQL para PostgreSQL
  • Implementa una aplicación en Cloud Run que se conecte a tu base de datos de Cloud SQL

Limpia

Cloud SQL no tiene un nivel gratuito y se te cobrará si continúas usándolo. Puedes borrar tu proyecto de Cloud para evitar que se generen cargos adicionales.

Si bien Cloud Run no cobra cuando el servicio no se usa, es posible que se te cobre por el almacenamiento de la imagen del contenedor en Artifact Registry. Si borras tu proyecto de Cloud, se detendrá la facturación de todos los recursos que usaste en ese proyecto.

Si lo deseas, borra el proyecto:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

También te recomendamos que borres los recursos innecesarios del disco de CloudShell. Puedes hacer lo siguiente:

  1. Borra el directorio del proyecto del codelab:
    rm -rf ~/task-app
    
  2. Advertencia. No podrás deshacer esta acción. Si quieres borrar todo en Cloud Shell para liberar espacio, puedes borrar todo el directorio principal. Asegúrate de que todo lo que quieras conservar esté guardado en otro lugar.
    sudo rm -rf $HOME