Cloud SQL Proxyを用いたCloud SQLへの接続（with Prisma）

## 本ドキュメントのスコープ
この記事では，Google CloudのCloud SQLをローカルに構築したアプリケーションから利用する方法について解説します．
また，データベース操作にはORM（Object-Relational Mapping）の一つである[Prisma](https://www.prisma.io/)を利用します．

### 解説すること
- Cloud SQLの設定
- Cloud SQL Proxyを利用したCloud SQLへの接続認証
- ローカル・クラウドの構成

### 解説しないこと
- ORMとは何か
- Prismaの詳しい利用方法

## アーキテクチャの全体像

- ウェブアプリケーション: React.js
- APIサーバ（DBアクセス用）: Express.js
- ORM: Prisma
- データベースサーバ: Cloud SQL
- RDBMS: PostgreSQL
- データベース認証: Cloud SQL Auth Proxy

![architecture.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611822/17755649-b1c5-9165-7872-c28537f111dd.jpeg)

ローカル開発環境は
- macOS 15.1
- apple M2
です．

## 事前準備1: プロジェクトディレクトリの作成
今回のプロジェクトを格納するディレクトリは次を想定しています（必ずしもこの構成である必要はありません）．
```sh
demo/ # プロジェクトルート
  ├── api # DB接続用API
  ├── app # ウェブアプリケーション
  └── proxy # Cloud SQL認証用プロキシ
```

## 事前準備2: Cloud SQL データベースの作成，認証設定

### Cloud SQL インスタンス・データベースの作成

まず，[Google Cloud Console](https://console.cloud.google.com/)からCloud SQLのインスタンスを作成します．
今回はRDBMSとしてPostgreSQLを選択します．

マシンスペックは実際に構築するシステムのデータ量等を考慮して設定してください．
検証環境として利用する場合は．最低スペック&料金の安いリージョンを選択しましょう．

![スクリーンショット 2024-11-26 7.37.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611822/34dea204-64a5-d365-aa67-67800959e808.png)

このとき，接続方式として「パブリックIP」を選択します．
（インスタンス作成後に「接続 > ネットワーキング > インスタンスIPの割り当て」から変更することもできます）

![スクリーンショット 2024-11-28 7.07.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611822/11a44e44-6658-1417-6a6a-6439a5b93e21.png)

インスタンスが起動したら，データベースを作成します．

### 接続許可の設定

次に，ローカルのProxyサーバから接続できるようにするための設定を行います．

- 「[Cloud SQL Admin API](https://cloud.google.com/sql/docs/mysql/admin-api?hl=ja)」を有効にします
- サービスアカウントに「Cloud SQL クライアント」のロールを付与します（「IAMと管理 > IAM」に`@developer.gserviceaccount.com`ドメインのサービスアカウントが作成されています）
- 「IAMと管理 > サービスアカウント」から，サービスアカウントに紐づいた[秘密鍵を作成](https://cloud.google.com/iam/docs/creating-managing-service-account-keys?hl=ja#iam-service-account-keys-create-console)し，JSONファイルとしてローカルに格納します．

![スクリーンショット 2024-11-28 7.25.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611822/314ececc-7254-f11c-6d46-defe24d56b42.png)


## 手順1: アプリケーションサーバを立てる（React.js）

### アプリ用Reactコード
手始めに，検証用のウェブアプリをReact.jsを使って作成します．

必要なパッケージはあらかじめ導入しておきます．
```sh
cd ./app

npx create-next-app@latest # react appの作成

npm install @mui/material @emotion/react @emotion/styled # Material-UI
npm install @axios # HTTPクライアント
npm install @prisma/client # Prisma Client
```

<details><summary>今回利用するReact.jsコード</summary>

以下のコードを`./app/src/page.tsx`に記載します．

```typescript
'use client';
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import {
  Box,
  Container,
  Grid,
  Card,
  CardContent,
  Typography,
  TextField,
  Checkbox,
  Button,
  FormControlLabel,
} from '@mui/material';

const API_URL = 'http://localhost:3001'; // APIサーバのエンドポイント

function App() {
  const [posts, setPosts] = useState([]);
  const [newPost, setNewPost] = useState({ title: '', content: '', published: false });

  // Fetch posts
  useEffect(() => {
    axios.get(`${API_URL}/posts`).then((response) => {
      setPosts(response.data);
    });
  }, []);

  // Add new post
  const handleSubmit = (e) => {
    e.preventDefault();
    axios.post(`${API_URL}/posts`, newPost).then((response) => {
      setPosts([...posts, response.data]);
      setNewPost({ title: '', content: '', published: false });
    });
  };

  return (
    <Container maxWidth="md">

      <Typography variant="h4" gutterBottom align="center" style={{ marginTop: '2rem' }}>
        Create New Post
      </Typography>
      <form onSubmit={handleSubmit} style={{ marginTop: '1rem', marginBottom: '1rem' }}>
        <Grid container spacing={2}>
          <Grid item xs={12}>
            <TextField
              label="Title"
              fullWidth
              value={newPost.title}
              onChange={(e) => setNewPost({ ...newPost, title: e.target.value })}
            />
          </Grid>
          <Grid item xs={12}>
            <TextField
              label="Content"
              multiline
              rows={4}
              fullWidth
              value={newPost.content}
              onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
            />
          </Grid>
          <Grid item xs={12}>
            <FormControlLabel
              control={
                <Checkbox
                  checked={newPost.published}
                  onChange={(e) => setNewPost({ ...newPost, published: e.target.checked })}
                />
              }
              label="Published"
            />
          </Grid>
          <Grid item xs={12}>
            <Button type="submit" variant="contained" color="primary" fullWidth>
              Add Post
            </Button>
          </Grid>
        </Grid>
      </form>

      <Typography variant="h4" gutterBottom align="center">
        Posts
      </Typography>
      <Grid container spacing={3}>
        {posts.map((post) => (
          <Grid item xs={12} key={post.id}>
            <Box>
              <Typography variant="h5" gutterBottom>
                {post.title}
              </Typography>
              <Typography variant="body1">{post.content}</Typography>
            </Box>
          </Grid>
        ))}
      </Grid>
    </Container>
  );
}

export default App;
```
</details>

`npm run start`などでサーバを起動することができますが，
今はまだAPIサーバ・Proxyサーバの設定が終わっていないのでエラーが生じます．

## 手順2: Proxyサーバを立てる
次に，APIサーバからのリクエストを受け付け，認証情報とともにCloud SQLに渡すProxyサーバを立てます．

:::note info
Cloud SQLの接続を「パブリックIP」に設定すると，データベースにアクセスするためには
- 認可済みネットワーク利用する
- Cloud SQL Proxyを経由する

のどちらかの対応を行う必要があります．
今回はCloud SQL Proxyを用いて，プロキシサーバを経由してCloud SQLに接続する方法について説明しています．
:::


### Cloud SQL Auth Proxyのダウンロード

Cloud SQL Auth Proxyをダウンロードし実行可能にします．
ダウンロード先は[Cloud SQL Auth Proxy について](https://cloud.google.com/sql/docs/mysql/sql-proxy?hl=ja)に記載されています．
```sh
cd ./proxy

# OSとアーキテクチャに応じて適切なバイナリをダウンロード
curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.6.1/cloud-sql-proxy.{os}.{arch}
chmod +x cloud-sql-proxy
```

### Cloud SQL Auth Proxyの起動

「手順1」でダウンロードしたCloud SQLの秘密鍵のパスを確認し，Cloud SQL Auth Proxyを起動します．

なお，設定するポート番号は，Proxyサーバーがlistenするポート番号です．
例えば`--port 5432`に設定すると，APIサーバは`0.0.0.0:5432`に向けてリクエストを送信する必要があります．

また，`{プロジェクトID}:{リージョン}:{インスタンス名}`はコンソールに表示されている「接続名」と同じです．

```sh
./cloud-sql-proxy \
--address 0.0.0.0 \
--port {任意のポート番号} \
--credentials-file {秘密鍵のパス} {プロジェクトID}:{リージョン}:{インスタンス名}
```

## 手順3: APIサーバを立てる（Express.js）

### Prismaのインストールと初期設定
```sh
cd ./api

npm init # npm プロジェクトの初期化
npm install prisma # prismaのインストール
npx prisma init # prismaの初期化
```

### データベース接続情報の設定

APIサーバはProxyサーバにアクセスすることでCloud SQLに接続します．
そのため，データベース接続先は，手順2で設定したProxyサーバのアドレスとポート番号になります．

この情報を`.env`に記述します．
```sh
DATABASE_URL="postgres://{ユーザー名}:{パスワード}@{IPアドレス}:{ポート}/{データベース名}"
# ex.) "postgres://dailyuser:admin@0.0.0.0:5432/demo-database"
```

ここでのユーザー名・パスワード・データベース名は，事前準備2で作成したユーザー名とパスワード，作成したデータベースの名称です．

### データモデルの定義

利用するデータモデルの定義は`./prisma/schema.prisma`に記述します．
```sh
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  createdAt DateTime @default(now())
  content   String?
  published Boolean  @default(false)
}
```

### 記述したデータモデルをSQLに変換

```sh
# SQLマイグレーションスクリプトの生成
npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql
```

`./prisma/migration/0_init/migration.sql`に生成されたSQLが格納されています．
```SQL
-- CreateTable
CREATE TABLE "Post" (
    "id" SERIAL NOT NULL,
    "title" VARCHAR(255) NOT NULL,
    "createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "content" TEXT,
    "published" BOOLEAN NOT NULL DEFAULT false,
);
```

### SQLをCloud SQL データベースに適用

生成されたSQLをクラウド上のデータベースに適用します．

```sh
# マイグレーションの適用
npx prisma migrate resolve --applied 0_init
```

`schema.prisma`に変更があった場合は`npx prisma migrate dev --name ${migrade_name}` で変更させることができます．

### ウェブアプリの動作確認

手順1で立ち上げたウェブアプリのエンドポイントにアクセスしてみましょう．

タイトルとコンテンツを指定して投稿ができる簡易的なウェブアプリが完成しているはずです．

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611822/6836b496-8ad7-6245-8f7b-89e2fed77358.gif)


## まとめ

今回は，Google CloudのCloud SQLを，Cloud SQL Auth Proxyを用いてPrisma経由でアクセスする方法を検証・解説しました．

これ以外の接続方法については，今後検証のうえ記事化したいと思っています．