1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GCP(Google Cloud Platform)Advent Calendar 2024

Day 3

Cloud SQL Proxyを用いたCloud SQLへの接続(with Prisma)

Last updated at Posted at 2024-12-02

本ドキュメントのスコープ

この記事では,Google CloudのCloud SQLをローカルに構築したアプリケーションから利用する方法について解説します.
また,データベース操作にはORM(Object-Relational Mapping)の一つであるPrismaを利用します.

解説すること

  • 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

ローカル開発環境は

  • macOS 15.1
  • apple M2
    です.

事前準備1: プロジェクトディレクトリの作成

今回のプロジェクトを格納するディレクトリは次を想定しています(必ずしもこの構成である必要はありません).

demo/ # プロジェクトルート
  ├── api # DB接続用API
  ├── app # ウェブアプリケーション
  └── proxy # Cloud SQL認証用プロキシ

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

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

まず,Google Cloud ConsoleからCloud SQLのインスタンスを作成します.
今回はRDBMSとしてPostgreSQLを選択します.

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

スクリーンショット 2024-11-26 7.37.28.png

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

スクリーンショット 2024-11-28 7.07.36.png

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

接続許可の設定

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

  • Cloud SQL Admin API」を有効にします
  • サービスアカウントに「Cloud SQL クライアント」のロールを付与します(「IAMと管理 > IAM」に@developer.gserviceaccount.comドメインのサービスアカウントが作成されています)
  • 「IAMと管理 > サービスアカウント」から,サービスアカウントに紐づいた秘密鍵を作成し,JSONファイルとしてローカルに格納します.

スクリーンショット 2024-11-28 7.25.00.png

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

アプリ用Reactコード

手始めに,検証用のウェブアプリをReact.jsを使って作成します.

必要なパッケージはあらかじめ導入しておきます.

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
今回利用するReact.jsコード

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

'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;

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

手順2: Proxyサーバを立てる

次に,APIサーバからのリクエストを受け付け,認証情報とともにCloud SQLに渡すProxyサーバを立てます.

Cloud SQLの接続を「パブリックIP」に設定すると,データベースにアクセスするためには

  • 認可済みネットワーク利用する
  • Cloud SQL Proxyを経由する

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

Cloud SQL Auth Proxyのダウンロード

Cloud SQL Auth Proxyをダウンロードし実行可能にします.
ダウンロード先はCloud SQL Auth Proxy についてに記載されています.

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}:{リージョン}:{インスタンス名}はコンソールに表示されている「接続名」と同じです.

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

手順3: APIサーバを立てる(Express.js)

Prismaのインストールと初期設定

cd ./api

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

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

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

この情報を.envに記述します.

DATABASE_URL="postgres://{ユーザー名}:{パスワード}@{IPアドレス}:{ポート}/{データベース名}"
# ex.) "postgres://dailyuser:admin@0.0.0.0:5432/demo-database"

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

データモデルの定義

利用するデータモデルの定義は./prisma/schema.prismaに記述します.

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に変換

# 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が格納されています.

-- 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をクラウド上のデータベースに適用します.

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

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

ウェブアプリの動作確認

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

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

demo.gif

まとめ

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?