Cloud SQL Node.js Bağlantısı'nı kullanarak PostgreSQL için Cloud SQL ile Cloud Run'a tam yığın Angular uygulaması dağıtma

1. Genel Bakış

Cloud Run, kodunuzu doğrudan Google'ın ölçeklenebilir altyapısı üzerinde çalıştırmanıza olanak tanıyan, tümüyle yönetilen bir platformdur. Bu Codelab'de, Cloud SQL Node.js bağlayıcısını kullanarak Cloud Run'daki bir Angular uygulamasını PostgreSQL için Cloud SQL veritabanına bağlama işlemi gösterilecektir.

Bu laboratuvarda şunları öğreneceksiniz:

  • PostgreSQL için Cloud SQL örneği oluşturma
  • Cloud SQL veritabanınıza bağlanan bir uygulamayı Cloud Run'a dağıtma

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 için 1 ABD dolarından daha az tutarda bulut kaynağı kullanmanız gerekir.
    • Daha fazla ücret ödememek için bu laboratuvarın sonundaki adımları uygulayarak kaynakları silebilirsiniz.
    • Yeni kullanıcılar 300 ABD doları değerinde ücretsiz deneme sürümünden 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ın.Cloud Shell Düzenleyici'de yeni terminal açma
  3. Terminalde şu komutla projenizi 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. Yetkilendirmeniz istenirse devam etmek için Yetkilendir'i tıklayın. Cloud Shell'e yetki vermek için tıklayın.
  5. Şu mesajı görmeniz gerekir:
    Updated property [core/project].
    
    WARNING simgesini görüyorsanız ve Do you want to continue (Y/N)? soruluyorsa proje kimliğini yanlış girmiş olabilirsiniz. N tuşuna, Enter tuşuna 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 \
  sqladmin.googleapis.com \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com

Yetkilendirmeniz istenirse 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 aşağıdakine benzer bir başarılı mesaj üretmesi gerekir:

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 hizmet hesabının Cloud SQL'e bağlanmak için doğru izinlere sahip olması gerekir.

  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. Yeni oluşturduğunuz Google Cloud hizmet hesabına Cloud SQL İstemcisi 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/cloudsql.client"
    
  3. Yeni oluşturduğunuz Google Cloud hizmet hesabına Cloud SQL Örneği 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:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
      --role="roles/cloudsql.instanceUser"
    
  4. Yeni oluşturduğunuz Google Cloud hizmet hesabına Log Writer 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 veritabanı oluşturma

  1. Cloud SQL örneği oluşturmak için gcloud sql instances create komutunu çalıştırın.
    gcloud sql instances create quickstart-instance \
        --database-version=POSTGRES_14 \
        --cpu=4 \
        --memory=16GB \
        --region=us-central1 \
        --database-flags=cloudsql.iam_authentication=on
    

Bu komutun tamamlanması birkaç dakika sürebilir.

  1. gcloud sql databases create komutunu çalıştırarak quickstart-instance içinde bir Cloud SQL veritabanı oluşturun.
    gcloud sql databases create quickstart_db \
        --instance=quickstart-instance
    
  2. Veritabanına erişmek için daha önce oluşturduğunuz hizmet hesabı için bir PostgreSQL veritabanı kullanıcısı oluşturun.
    gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \
        --instance=quickstart-instance \
        --type=cloud_iam_service_account
    

8. Uygulamayı 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 --yes @angular/cli@19.2.5 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --server-routing \
        --defaults
    
  2. Dizini task-app olarak değiştirin:
    cd task-app
    
  1. PostgreSQL veritabanıyla etkileşim kurmak için pg ve Cloud SQL Node.js bağlayıcı kitaplığını yükleyin.
    npm install pg @google-cloud/cloud-sql-connector google-auth-library
    
  2. TypeScript Next.js uygulaması kullanmak için @types/pg öğesini geliştirme bağımlılığı olarak yükleyin.
    npm install --save-dev @types/pg
    
  1. server.ts dosyasını Cloud Shell Düzenleyici'de açın:
    cloudshell edit src/server.ts
    
    Ekranın üst kısmında bir dosya görünür. Bu server.ts dosyasını buradan düzenleyebilirsiniz. Kodun ekranın üst kısmına yerleştirildiğini göster
  2. server.ts dosyasının mevcut içeriğini silin.
  3. Aşağıdaki kodu kopyalayın ve açılan server.ts dosyasına yapıştırın:
    import {
      AngularNodeAppEngine,
      createNodeRequestHandler,
      isMainModule,
      writeResponseToNodeResponse,
    } from '@angular/ssr/node';
    import express from 'express';
    import { dirname, resolve } from 'node:path';
    import { fileURLToPath } from 'node:url';
    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';
      createdAt: number;
    };
    
    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)
        );`);
    }
    
    const serverDistFolder = dirname(fileURLToPath(import.meta.url));
    const browserDistFolder = resolve(serverDistFolder, '../browser');
    
    const app = express();
    const angularApp = new AngularNodeAppEngine();
    
    app.use(express.json());
    
    app.get('/api/tasks', async (req, res) => {
      await tableCreationIfDoesNotExist();
      const { rows } = await pool.query(`SELECT id, created_at, status, title FROM tasks ORDER BY created_at DESC LIMIT 100`);
      res.send(rows);
    });
    
    app.post('/api/tasks', async (req, res) => {
      const newTaskTitle = req.body.title;
      if (!newTaskTitle) {
        res.status(400).send("Title is required");
        return;
      }
      await tableCreationIfDoesNotExist();
      await pool.query(`INSERT INTO tasks(created_at, status, title) VALUES(NOW(), 'IN_PROGRESS', $1)`, [newTaskTitle]);
      res.sendStatus(200);
    });
    
    app.put('/api/tasks', async (req, res) => {
      const task: Task = req.body;
      if (!task || !task.id || !task.title || !task.status) {
        res.status(400).send("Invalid task data");
        return;
      }
      await tableCreationIfDoesNotExist();
      await pool.query(
        `UPDATE tasks SET status = $1, title = $2 WHERE id = $3`,
        [task.status, task.title, task.id]
      );
      res.sendStatus(200);
    });
    
    app.delete('/api/tasks', async (req, res) => {
      const task: Task = req.body;
      if (!task || !task.id) {
        res.status(400).send("Task ID is required");
        return;
      }
      await tableCreationIfDoesNotExist();
      await pool.query(`DELETE FROM tasks WHERE id = $1`, [task.id]);
      res.sendStatus(200);
    });
    
    /**
    * Serve static files from /browser
    */
    app.use(
      express.static(browserDistFolder, {
        maxAge: '1y',
        index: false,
        redirect: false,
      }),
    );
    
    /**
    * Handle all other requests by rendering the Angular application.
    */
    app.use('/**', (req, res, next) => {
      angularApp
        .handle(req)
        .then((response) =>
          response ? writeResponseToNodeResponse(response, res) : next(),
        )
        .catch(next);
    });
    
    /**
    * Start the server if this module is the main entry point.
    * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
    */
    if (isMainModule(import.meta.url)) {
      const port = process.env['PORT'] || 4000;
      app.listen(port, () => {
        console.log(`Node Express server listening on http://localhost:${port}`);
      });
    }
    
    /**
    * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
    */
    export const reqHandler = createNodeRequestHandler(app);
    
  1. app.component.ts dosyasını Cloud Shell Düzenleyici'de açın:
    cloudshell edit src/app/app.component.ts
    
    Mevcut bir dosya, ekranın üst kısmında görünür. Bu app.component.ts dosyasını buradan düzenleyebilirsiniz. Kodun ekranın üst kısmına yerleştirildiğini göster
  2. app.component.ts dosyasının mevcut içeriğini silin.
  3. Aşağıdaki kodu kopyalayın ve açılan app.component.ts dosyasına yapıştırın:
    import { afterNextRender, Component, signal } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [FormsModule],
      template: `
        <section>
          <input
            type="text"
            placeholder="New Task Title"
            [(ngModel)]="newTaskTitle"
            class="text-black border-2 p-2 m-2 rounded"
          />
          <button (click)="addTask()">Add new task</button>
          <table>
            <tbody>
              @for (task of tasks(); track task) {
                @let isComplete = task.status === 'COMPLETE';
                <tr>
                  <td>
                    <input
                      (click)="updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })"
                      type="checkbox"
                      [checked]="isComplete"
                    />
                  </td>
                  <td>{{ task.title }}</td>
                  <td>{{ task.status }}</td>
                  <td>
                    <button (click)="deleteTask(task)">Delete</button>
                  </td>
                </tr>
              }
            </tbody>
          </table>
        </section>
      `,
      styles: '',
    })
    export class AppComponent {
      newTaskTitle = '';
      tasks = signal<Task[]>([]);
    
      constructor() {
        afterNextRender({
          earlyRead: () => this.getTasks()
        });
      }
    
      async getTasks() {
        const response = await fetch(`/api/tasks`);
        const tasks = await response.json();
        this.tasks.set(tasks);
      }
    
      async addTask() {
        await fetch(`/api/tasks`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            title: this.newTaskTitle,
            status: 'IN_PROGRESS',
            createdAt: Date.now(),
          }),
        });
        this.newTaskTitle = '';
        await this.getTasks();
      }
    
      async updateTask(task: Task, newTaskValues: Partial<Task>) {
        await fetch(`/api/tasks`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ ...task, ...newTaskValues }),
        });
        await this.getTasks();
      }
    
      async deleteTask(task: any) {
        await fetch('/api/tasks', {
          method: 'DELETE',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(task),
        });
        await this.getTasks();
      }
    }
    

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

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

  1. Uygulamanızı Cloud Run'a dağıtmak için aşağıdaki komutu çalıştırın:
    gcloud run deploy to-do-tracker \
        --region=us-central1 \
        --source=. \
        --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
        --allow-unauthenticated
    
  2. İstenirse devam etmek istediğinizi onaylamak için Y ve Enter tuşlarına 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. Tebrikler

Bu laboratuvarda şunları yapmayı öğrendiniz:

  • PostgreSQL için Cloud SQL örneği oluşturma
  • Cloud SQL veritabanınıza bağlanan bir uygulamayı Cloud Run'a dağıtma

Temizleme

Cloud SQL'in ücretsiz 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 depoladığınız için ücretlendirilebilirsiniz. Cloud projenizi sildiğinizde, bu projede kullanılan tüm kaynaklar için faturalandırma durdurulur.

İsterseniz projeyi silebilirsiniz:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

Ayrıca, gereksiz kaynakları cloudshell diskinizden de silebilirsiniz. Şunları yapabilirsiniz:

  1. Codelab proje dizinini silin:
    rm -rf ~/task-app
    
  2. Uyarı! Bu 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
    

Öğrenmeye devam edin