1. Présentation
Cloud Run est une plate-forme entièrement gérée qui vous permet d'exécuter votre code directement sur l'infrastructure évolutive de Google. Cet atelier de programmation vous explique comment connecter une application Next.js sur Cloud Run à une base de données Cloud SQL pour PostgreSQL.
Dans cet atelier, vous allez apprendre à effectuer les tâches suivantes :
- Créer une instance Cloud SQL pour PostgreSQL (configurée pour utiliser Private Service Connect)
- Déployer une application sur Cloud Run qui se connecte à votre base de données Cloud SQL
- Utiliser Gemini Code Assist pour ajouter des fonctionnalités à votre application
2. Prérequis
- Si vous ne possédez pas encore de compte Google, vous devez en créer un.
- Vous utilisez un compte personnel au lieu d'un compte professionnel ou scolaire. Les comptes professionnels et scolaires peuvent être soumis à des restrictions qui vous empêchent d'activer les API nécessaires à cet atelier.
3. Configuration du projet
- Connectez-vous à la console Google Cloud.
- Activez la facturation dans la console Cloud.
- La réalisation de cet atelier devrait coûter moins de 1 USD en ressources Cloud.
- Vous pouvez suivre les étapes à la fin de cet atelier pour supprimer les ressources afin d'éviter d'autres frais.
- Les nouveaux utilisateurs peuvent bénéficier d'un essai sans frais pour bénéficier d'un crédit de 300$.
- Créez un projet ou choisissez de réutiliser un projet existant.
4. Ouvrir l'éditeur Cloud Shell
- Accédez à l'éditeur Cloud Shell.
- Si le terminal ne s'affiche pas en bas de l'écran, ouvrez-le:
- Cliquez sur le menu hamburger
.
- Cliquez sur Terminal
- Cliquez sur Nouveau terminal
- Cliquez sur le menu hamburger
- Dans le terminal, définissez votre projet à l'aide de la commande suivante:
- Format :
gcloud config set project [PROJECT_ID]
- Exemple :
gcloud config set project lab-project-id-example
- Si vous ne vous souvenez pas de l'ID de votre projet:
- Vous pouvez lister tous vos ID de projet avec:
gcloud projects list | awk '/PROJECT_ID/{print $2}'
- Vous pouvez lister tous vos ID de projet avec:
- Format :
- Si vous y êtes invité, cliquez sur Autoriser pour continuer.
- Le message suivant doit s'afficher :
Si unUpdated property [core/project].
WARNING
s'affiche et que vous êtes invité à saisirDo you want to continue (Y/N)?
, vous avez probablement saisi l'ID de projet de manière incorrecte. Appuyez surN
, puis surEnter
, puis réessayez d'exécuter la commandegcloud config set project
.
5. Activer les API
Dans le terminal, activez les 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
Si vous y êtes invité, cliquez sur Autoriser pour continuer.
L'exécution de cette commande peut prendre quelques minutes, mais un message semblable à celui-ci devrait s'afficher pour vous indiquer que l'opération s'est correctement déroulée:
Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.
6. Configurer un compte de service
Créez et configurez un compte de service Google Cloud à utiliser par Cloud Run afin qu'il dispose des autorisations appropriées pour se connecter à Cloud SQL.
- Exécutez la commande
gcloud iam service-accounts create
comme suit pour créer un compte de service:gcloud iam service-accounts create quickstart-service-account \ --display-name="Quickstart Service Account"
- Exécutez la commande gcloud projects add-iam-policy-binding comme suit pour ajouter le rôle Rédacteur de journal au compte de service Google Cloud que vous venez de créer.
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. Créer une instance Cloud SQL
- Créer une stratégie de connexion de service pour autoriser la connectivité réseau de Cloud Run à Cloud SQL avec 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
- Générer un mot de passe unique pour votre base de données
export DB_PASSWORD=$(openssl rand -base64 20)
- Exécuter la commande
gcloud sql instances create
pour créer une instance Cloud SQLgcloud 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
L'exécution de cette commande peut prendre quelques minutes.
- Exécutez la commande
gcloud sql databases create
pour créer une base de données Cloud SQL dansquickstart-instance
.gcloud sql databases create quickstart_db \ --instance=quickstart-instance
8. Préparer l'application
Préparez une application Next.js qui répond aux requêtes HTTP.
- Pour créer un projet Next.js nommé
task-app
, utilisez la commande suivante :npx create-next-app@15.0.3 task-app \ --ts \ --eslint \ --tailwind \ --no-src-dir \ --turbopack \ --app \ --no-import-alias
- Si vous êtes invité à installer
create-next-app
, appuyez surEnter
pour continuer:Need to install the following packages: create-next-app@15.0.3 Ok to proceed? (y)
- Remplacez le répertoire par
task-app
:cd task-app
- Installez
pg
pour interagir avec la base de données PostgreSQL.npm install pg
- Installez
@types/pg
en tant que dépendance de développement pour utiliser l'application TypeScript Next.js.npm install --save-dev @types/pg
- Créez le fichier
actions.ts
.touch app/actions.ts
- Ouvrez le fichier
actions.ts
dans l'éditeur Cloud Shell: Un fichier vide devrait maintenant s'afficher en haut de l'écran. C'est là que vous pouvez modifier ce fichiercloudshell edit app/actions.ts
actions.ts
. - Copiez le code suivant et collez-le dans le fichier
actions.ts
ouvert:'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; }
- Ouvrez le fichier
page.tsx
dans l'éditeur Cloud Shell: Un fichier existant devrait maintenant s'afficher en haut de l'écran. C'est là que vous pouvez modifier ce fichiercloudshell edit app/page.tsx
page.tsx
. - Supprimez le contenu existant du fichier
page.tsx
. - Copiez le code suivant et collez-le dans le fichier
page.tsx
ouvert:'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'application est maintenant prête à être déployée.
9. Déployer l'application sur Cloud Run
- Exécutez la commande gcloud projects add-iam-policy-binding comme suit pour ajouter le rôle Utilisateur réseau au compte de service Cloud Run du service Cloud Run que vous êtes sur le point de créer.
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"
- Exécutez la commande ci-dessous pour déployer votre application sur 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
- Si vous y êtes invité, appuyez sur
Y
etEnter
pour confirmer que vous souhaitez continuer:Do you want to continue (Y/n)? Y
Après quelques minutes, l'application doit vous fournir une URL à laquelle vous pouvez accéder.
Accédez à l'URL pour voir votre application en action. Chaque fois que vous accéderez à l'URL ou actualiserez la page, l'application de tâches s'affichera.
10. Ajouter une fonctionnalité avec Gemini Code Assist
Vous avez maintenant déployé une application Web avec une base de données. Nous allons ensuite ajouter une nouvelle fonctionnalité à notre application next.js à l'aide de l'assistance de l'IA.
- Revenir à l'éditeur Cloud Shell
- Rouvrir
page.tsx
cd ~/task-app cloudshell edit app/page.tsx
- Accédez à Gemini Code Assist dans l'éditeur Cloud Shell:
- Cliquez sur l'icône Gemini
dans la barre d'outils située à gauche de l'écran.
- Si vous y êtes invité, connectez-vous avec les identifiants de votre compte Google.
- Si vous êtes invité à sélectionner un projet, sélectionnez celui que vous avez créé pour cet atelier de programmation.
- Cliquez sur l'icône Gemini
- Saisissez la requête:
Add the ability to update the title of the task. The code in your output should be complete and working code.
. La réponse doit inclure des extraits de code comme ceux-ci pour ajouter des fonctionshandleEditStart
ethandleEditCancel
: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>
- Remplacez
page.tsx
par la sortie de Gemini Code Assist. Voici un exemple concret:'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. Redéployer l'application dans Cloud Run
- Exécutez la commande ci-dessous pour déployer votre application sur 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
- Si vous y êtes invité, appuyez sur
Y
etEnter
pour confirmer que vous souhaitez continuer:Do you want to continue (Y/n)? Y
12. Félicitations
Dans cet atelier, vous avez appris à effectuer les tâches suivantes :
- Créer une instance Cloud SQL pour PostgreSQL (configurée pour utiliser Private Service Connect)
- Déployer une application sur Cloud Run qui se connecte à votre base de données Cloud SQL
- Utiliser Gemini Code Assist pour ajouter des fonctionnalités à votre application
Effectuer un nettoyage
Cloud SQL ne propose pas de niveau sans frais et vous sera facturé si vous continuez à l'utiliser. Vous pouvez supprimer votre projet Cloud pour éviter des frais supplémentaires.
Bien que Cloud Run ne facture pas lorsque le service n'est pas utilisé, il se peut que des frais vous soient facturés pour le stockage de l'image de conteneur dans Artifact Registry. La suppression de votre projet Cloud arrête la facturation de toutes les ressources utilisées dans ce projet.
Si vous le souhaitez, supprimez le projet:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
Vous pouvez également supprimer les ressources inutiles de votre disque cloudshell. Vous pouvez :
- Supprimez le répertoire du projet de l'atelier de programmation:
rm -rf ~/task-app
- Avertissement ! Cette action est irréversible. Si vous souhaitez supprimer tout ce qui se trouve sur votre Cloud Shell pour libérer de l'espace, vous pouvez supprimer l'intégralité de votre répertoire personnel. Assurez-vous que tout ce que vous souhaitez conserver est enregistré ailleurs.
sudo rm -rf $HOME