DockerコンテナからPlanetScaleに接続する方法とSSLエラーの対処法

# はじめに
サーバレスだから導入も簡単なんだろうな🤗
と思っていたら盛大にハマってしまったので備忘録を兼ねて投稿します。

DockerコンテナからPlanetScaleに接続しようとすると下記エラーに遭遇したのでその対処法です。
また、記事の後半では動作確認のためNuxt3のCRUD機能も実装しちゃいます。

```bash
server does not allow insecure connections, client must use SSL/TLS
```

以下を前提として解説しています。
* PlanetScaleにデータベース作成済みであること
* Nuxt.jsプロジェクト作成済みであること

# docker-compose.ymlの設定
PlanetScaleとの通信はSSL接続が必要らしい。
https://planetscale.com/docs/concepts/secure-connections
そのため、volumesでSSL証明書をマウントしておきます。

```yml:docker-compose.yml
version: '3'
services:
  web:
    container_name: web
    build: .
    ports:
      - "3000:3000" # Nuxt.js用のポート
      - "5555:5555" # Prisma Studio用のポート
    volumes:
      - "../src:/var/www/html"
      - "/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt" # ←重要
    tty: true
```

# Dockerfileの設定
```Dockerfile
FROM node:latest

WORKDIR /var/www/html

RUN apt update && \
    npm install -g npm

EXPOSE 3000
ENV HOST 0.0.0.0
```

# Dockerコンテナを起動
コンテナをビルド、起動します。
```bash
docker compose up -d --build
```

webコンテナに入ります。
```bash
docker compose exec web bash
```
# PlanetScaleの設定
登録～DB作成までの流れは下記の記事が参考になると思います。
[PlanetScaleというサーバレスDBが凄く勢いのあるサービスらしいのでQuick Startやってみた - Qiita](https://qiita.com/tak001/items/cfbaa9dcb542929ff235)

PlanetScaleのブランチ管理画面にアクセスします。
「New password」ボタンをクリックして接続パスワードを生成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140967/740bf6b9-5022-e3cb-6daa-a3e8beb95acd.png)

この時、画面に表示される以下の情報を控えておいてください。
* database
* username
* host
* password

# .envファイルの設定
先ほど控えた情報をもとに.envファイルに接続情報を追記します。
```.env
DATABASE_URL="mysql://username:password@host/database?sslaccept=strict"
```

# Prismaの設定
Prismaについての解説は本記事ではしません。
参考URLをご覧ください。
[Prismaの導入とメリットを考える - Qiita](https://qiita.com/am_765/items/5e42bd5f87b296f61fbc)

既存のNuxtプロジェクトのディレクトリに移動します。
```bash
cd hoge-project
```

Prismaをインストールします。
```bash
yarn add -D prisma
```

Prismaを初期化します。
```bash
npx prisma init
```

プロジェクトディレクトリに「prisma/schema.prisma」というファイルが生成されます。
schema.prismaファイルを開いてモデルを定義します。
```bash:schema.prisma
generator client {
  provider = "prisma-client-js"
}

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

model User {
  id Int @id @default(autoincrement())
  uid String @unique @db.VarChar(255)
  display_name String @db.VarChar(255)
  created_at DateTime @default(now()) @db.Timestamp(0)
  updated_at DateTime @default(now()) @updatedAt @db.Timestamp(0)
}
```

モデルの定義情報をPlanetScaleに反映させます。
```bash
npx prisma db push
```

Prisma Clientを生成します。
以下は、モデル定義を変更するたびに実行してください。
```bash
npx prisma generate
```

# 作成されたDBを確認する
Prisma Studioを起動してDBを確認します。
```bash
npx prisma studio
```

「localhost:5555」をブラウザで開くとアクセスできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140967/502d83c3-db30-7b0a-46fe-68cf91a29cf0.png)

# CRUDを実装する
### ファイル・ディレクトリ構造
CRUD機能を実装するにあたり、以下3つのファイルを作成します。
* composables/useUser.ts
* pages/test-prisma.vue
* server/api/user.ts

### 表示する側
`<template>`タグ内にはシンプルにボタンのみを配置しています。
CSSフレームワークにVuetifyを使用しています。

各ボタンの`@click`に対応する処理を`<script>`タグ内に記載しています。
uuidの生成に@faker-js/fakerを使用しています。

onClickUpdateとonClickDeleteの「targetId」の値は必要に応じて修正してください。

```pages/test-prisma.vue
<template>
  <div>
    <v-btn @click="onClickCreate">create</v-btn>
    <v-btn @click="onClickRead">read</v-btn>
    <v-btn @click="onClickUpdate">update</v-btn>
    <v-btn @click="onClickDelete">delete</v-btn>
  </div>
</template>

<script setup>
  import {faker} from '@faker-js/faker'

  const { createUser, readUser, updateUser, deleteUser } = useUser()

  const onClickCreate = async () => {
    await createUser({
      uid: faker.datatype.uuid().substring(0, 16),
      display_name: 'Test User'
    })
  }

  const onClickRead  = async () => {
    const data = await readUser()
    console.log(data)
  }

  const onClickUpdate = async () => {
    await updateUser({
      targetId: 1,
      data: {
        uid: faker.datatype.uuid().substring(0, 16),
        display_name: 'テスト ユーザー'
      }
    })
  }

  const onClickDelete = async () => {
    await deleteUser({
      targetId: 1
    })
  }
</script>
```

### 実行するコード
再利用可能な関数としてVueコンポーザブルという機能を使います。
動作確認のため、かなりシンプルな実装になっています。

```composables/useUser.ts
const useUser = () => {
  /** CREATE */
  const createUser = async (values) => {
    const data = await $fetch('/api/user', {
      method: 'post',
      body: values,
    })
    await refreshNuxtData()
    return data
  }

  /** READ */
  const readUser = async () => {
    const data = await $fetch('/api/user')
    await refreshNuxtData()
    return data
  }

  /** UPDATE */
  const updateUser = async (values) => {
    const data = await $fetch('/api/user', {
      method: 'put',
      body: values,
    })
    await refreshNuxtData()
    return data
  }

  /** DELETE */
  const deleteUser = async (values) => {
    const data = await $fetch('/api/user', {
      method: 'delete',
      body: values,
    })
    await refreshNuxtData()
    return data
  }

  return {
    createUser, readUser, updateUser, deleteUser
  }
}
export default useUser
```

### API
データベースとデータをやり取りするためのAPIを作成します。

```server/api/user.ts
import { PrismaClient, Prisma } from '@prisma/client'

export default defineEventHandler(async (e) => {
  const prisma = new PrismaClient()
  const method = e.req.method

  if (method === 'POST') {
    const body = await readBody(e)
    const user = await prisma.user.create({
      data: body
    })
    return user
  }

  if (method === 'GET') {
    const user = await prisma.user.findMany()
    return user
  }

  if (method === 'PUT') {
    const body = await readBody(e)
    const user = await prisma.user.update({
      where: { id: body.targetId },
      data: body.data
    })
    return user
  }

  if (method === 'DELETE') {
    const body = await readBody(e)
    const user = await prisma.user.delete({
      where: { id: body.targetId }
    })
    return user
  }
})
```

### ビルド実行
お疲れさまでした🍺
ビルドを実行します。
```bash
yarn dev
```

「localhost:3000/test-prisma」へアクセスしてください。
下図のような、ボタンだけの画面が表示されているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140967/5c1a36f5-3fcd-9236-f5e5-c964db5eebd7.png)

### 動作確認
Prisma Studio (`localhost:5555`)へアクセスして各ボタンが機能しているか確認してみましょう。

#### Create
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140967/816778d0-43b6-4ee8-34e9-b943d78909ce.png)

#### Read
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140967/8f603ae7-5a48-b5f1-51d9-53c4660edab5.png)

#### Update
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140967/921a4cc6-598f-cda9-4fce-71508f386167.png)

#### Delete
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140967/ee94234e-1e39-51ec-1b2c-80a551ecebf8.png)

# 参考サイト
[Nuxt3、Firebase、Prismaを利用し、SQL文を書かずに簡単なToDoアプリを作ってみる](https://zenn.dev/kote2/articles/cec55a155fbae1)