PostgreSQL için Cloud SQL ile Cloud Run'a tam yığın JavaScript uygulaması dağıtma

1. Genel Bakış

Cloud Run, kodunuzu doğrudan Google'ın ölçeklenebilir altyapısında çalıştırmanıza olanak tanıyan, tümüyle yönetilen bir platformdur. Bu Codelab'de, Cloud Run'daki bir Next.js uygulamasının PostgreSQL için Cloud SQL veritabanına nasıl bağlanacağı gösterilmektedir.

Bu laboratuvarda şunları öğreneceksiniz:

  • PostgreSQL için Cloud SQL örneği oluşturun (Private Service Connect'i kullanacak şekilde yapılandırılmış)
  • Cloud Run'a, Cloud SQL veritabanınıza bağlanan bir uygulama dağıtma
  • Uygulamanıza işlev eklemek için Gemini Code Assist'i kullanma

2. Ön koşullar

  1. Google Hesabınız yoksa Google Hesabı oluşturmanız gerekir.
    • İş veya okul hesabı yerine kişisel hesap kullanıyorsanız. İş ve okul hesaplarında, bu laboratuvar için gereken API'leri etkinleştirmenizi engelleyen kısıtlamalar olabilir.

3. Proje ayarlama

  1. Google Cloud Console'da oturum açın.
  2. Cloud Console'da faturalandırmayı etkinleştirin.
    • Bu laboratuvarı tamamlamak, bulut kaynaklarında 1 ABD dolarından az maliyete mal olur.
    • Daha fazla ödeme alınmaması için bu laboratuvarın sonundaki adımları uygulayarak kaynakları silebilirsiniz.
    • Yeni kullanıcılar 300 ABD doları tutarındaki ücretsiz denemeden yararlanabilir.
  3. Yeni bir proje oluşturun veya mevcut bir projeyi yeniden kullanmayı seçin.

4. Cloud Shell Düzenleyici'yi açma

  1. Cloud Shell Düzenleyici'ye gidin
  2. Terminal ekranın alt kısmında görünmüyorsa açın:
    • Hamburger menüsünü Hamburger menü simgesi tıklayın.
    • Terminal'i tıklayın.
    • Yeni Terminal'i tıklayınCloud Shell Düzenleyici'de yeni terminal açma
  3. Terminalde projenizi şu komutla ayarlayın:
    • Biçim:
      gcloud config set project [PROJECT_ID]
      
    • Örnek:
      gcloud config set project lab-project-id-example
      
    • Proje kimliğinizi hatırlamıyorsanız:
      • Tüm proje kimliklerinizi şu komutla listeleyebilirsiniz:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Cloud Shell Düzenleyici terminalinde proje kimliğini ayarlama
  4. Yetkilendirme isteğinde bulunulursa devam etmek için Yetkilendir'i tıklayın. Cloud Shell'e yetki vermek için tıklayın
  5. Aşağıdaki mesajı görürsünüz:
    Updated property [core/project].
    
    WARNING görüyorsanız ve Do you want to continue (Y/N)? soruyorsanız proje kimliğini yanlış girdiğiniz muhtemeldir. N, Enter tuşlarına basın ve gcloud config set project komutunu tekrar çalıştırmayı deneyin.

5. API'leri etkinleştir

Terminalde API'leri etkinleştirin:

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

Yetkilendirme isteğinde bulunulursa devam etmek için Yetkilendir'i tıklayın. Cloud Shell'e yetki vermek için tıklayın

Bu komutun tamamlanması birkaç dakika sürebilir ancak sonunda şuna benzer bir başarılı mesaj gösterilir:

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

6. Hizmet hesabı oluşturma

Cloud Run tarafından kullanılacak bir Google Cloud hizmet hesabı oluşturup yapılandırın. Bu hesap, Cloud SQL'e bağlanmak için doğru izinlere sahip olmalıdır.

  1. Yeni bir hizmet hesabı oluşturmak için gcloud iam service-accounts create komutunu aşağıdaki gibi çalıştırın:
    gcloud iam service-accounts create quickstart-service-account \
      --display-name="Quickstart Service Account"
    
  2. Az önce oluşturduğunuz Google Cloud hizmet hesabına Günlük Yazıcı rolünü eklemek için gcloud projects add-iam-policy-binding komutunu aşağıdaki gibi çalıştırın.
    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. Cloud SQL örneği oluşturma

  1. Private Service Connect ile Cloud Run'dan Cloud SQL'e ağ bağlantısına izin vermek için bir Hizmet Bağlantısı Politikası oluşturun
    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. Veritabanı için benzersiz bir şifre oluşturma
    export DB_PASSWORD=$(openssl rand -base64 20)
    
  3. Cloud SQL örneği oluşturmak için gcloud sql instances create komutunu çalıştırma
    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
    

Bu komutun çalıştırılması birkaç dakika sürebilir.

  1. quickstart-instance içinde bir Cloud SQL veritabanı oluşturmak için gcloud sql databases create komutunu çalıştırın.
    gcloud sql databases create quickstart_db \
      --instance=quickstart-instance
    

8. Başvuruyu hazırlama

HTTP isteklerine yanıt veren bir Next.js uygulaması hazırlayın.

  1. task-app adlı yeni bir Next.js projesi oluşturmak için şu komutu kullanın:
    npx create-next-app@15.0.3 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. create-next-app yüklemeniz istenirse devam etmek için Enter tuşuna basın:
    Need to install the following packages:
    create-next-app@15.0.3
    Ok to proceed? (y)
    
  3. Dizin task-app olarak değiştirilir:
    cd task-app
    
  4. PostgreSQL veritabanıyla etkileşimde bulunmak için pg'ü yükleyin.
    npm install pg
    
  5. TypeScript Next.js uygulamasını kullanmak için @types/pg'ü geliştirici bağımlılığı olarak yükleyin.
    npm install --save-dev @types/pg
    
  6. actions.ts dosyasını oluşturun.
    touch app/actions.ts
    
  7. actions.ts dosyasını Cloud Shell Düzenleyici'de açın:
    cloudshell edit app/actions.ts
    
    Ekranın üst kısmında boş bir dosya görünür. Bu actions.ts dosyasını buradan düzenleyebilirsiniz. Kodun ekranın üst kısmına yerleştirildiğini gösterin
  8. Aşağıdaki kodu kopyalayıp açılan actions.ts dosyasına yapıştırın:
    '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. page.tsx dosyasını Cloud Shell Düzenleyici'de açın:
    cloudshell edit app/page.tsx
    
    Ekranın üst kısmında mevcut bir dosya görünür. Bu page.tsx dosyasını buradan düzenleyebilirsiniz. Kodun ekranın üst kısmına yerleştirildiğini gösterin
  10. page.tsx dosyasının mevcut içeriğini silin.
  11. Aşağıdaki kodu kopyalayıp açılan page.tsx dosyasına yapıştırın:
    '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>
      );
    }
    

Uygulama artık dağıtılmaya hazırdır.

9. Uygulamayı Cloud Run'a dağıtma

  1. Oluşturacağınız Cloud Run hizmetinin Cloud Run hizmet hesabına Ağ Kullanıcısı rolünü eklemek için gcloud projects add-iam-policy-binding komutunu aşağıdaki gibi çalıştırın.
    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. Uygulamanızı Cloud Run'a dağıtmak için aşağıdaki komutu çalıştırın:
    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. İstenirse devam etmek istediğinizi onaylamak için Y ve Enter'ye basın:
    Do you want to continue (Y/n)? Y
    

Birkaç dakika sonra uygulama, ziyaret etmeniz için bir URL sağlar.

Uygulamanızı çalışırken görmek için URL'ye gidin. URL'yi her ziyaret ettiğinizde veya sayfayı her yenilediğinizde görev uygulamasını görürsünüz.

10. Gemini Code Assist ile özellik ekleme

Artık veritabanı içeren bir web uygulaması dağıttınız. Ardından, yapay zeka desteğinin gücünden yararlanarak next.js uygulamamıza yeni bir özellik ekleyeceğiz.

  1. Cloud Shell Düzenleyici'ye dönme
  2. page.tsx dosyasını tekrar aç
    cd ~/task-app
    cloudshell edit app/page.tsx
    
  3. Cloud Shell Düzenleyici'de Gemini Code Assist'e gidin:
    • Ekranın sol tarafındaki araç çubuğunda İkizler simgesini Gemini Code Assist simgesi tıklayın.
    • İstenirse Google Hesabı kimlik bilgilerinizle oturum açın.
    • Proje seçmeniz istenirse bu Codelab için oluşturduğunuz projeyi seçin Gemini projesini seçme
  4. İstemi girin: Add the ability to update the title of the task. The code in your output should be complete and working code.. Yanıt, handleEditStart ve handleEditCancel işlevleri eklemek için aşağıdaki snippet'lere benzer bir şey içermelidir:
    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. page.tsx kısmını Gemini Code Assist'in çıktısıyla değiştirin. Aşağıda çalışan bir örnek verilmiştir:
    '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. Uygulamayı Cloud Run'a yeniden dağıtma

  1. Uygulamanızı Cloud Run'a dağıtmak için aşağıdaki komutu çalıştırın:
    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. İstenirse devam etmek istediğinizi onaylamak için Y ve Enter'ye basın:
    Do you want to continue (Y/n)? Y
    

12. Tebrikler

Bu laboratuvarda şunları nasıl yapacağınızı öğrendiniz:

  • PostgreSQL için Cloud SQL örneği oluşturun (Private Service Connect'i kullanacak şekilde yapılandırılmış)
  • Cloud Run'a, Cloud SQL veritabanınıza bağlanan bir uygulama dağıtma
  • Uygulamanıza işlev eklemek için Gemini Code Assist'i kullanma

Temizleme

Cloud SQL'in ücretsiz bir katmanı yoktur ve kullanmaya devam ederseniz sizden ücret alınır. Ek ücret ödememek için Cloud projenizi silebilirsiniz.

Cloud Run, hizmet kullanılmadığında ücret almaz ancak container görüntüsünü Artifact Registry'de depolamak için sizden ücret alınabilir. Cloud projenizi sildiğinizde, söz konusu projede kullanılan tüm kaynakların faturalandırması durdurulur.

İsterseniz projeyi silin:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Gereksiz kaynakları CloudShell diskinizden de silebilirsiniz. Şunları yapabilirsiniz:

  1. Codelab proje dizinini silin:
    rm -rf ~/task-app
    
  2. Uyarı! Sonraki işlem geri alınamaz. Yer açmak için Cloud Shell'inizdeki her şeyi silmek istiyorsanız tüm ana dizininizi silebilirsiniz. Saklamak istediğiniz her şeyin başka bir yere kaydedildiğinden emin olun.
    sudo rm -rf $HOME