Neon(Postgres)のDockerをローカルで稼働させ、Drizzleを使ったマイグレーションとCloudflare Workersのローカル実行からの接続を行ったメモ

# 概要
[前回はFirebase Authのローカル化](https://qiita.com/hibohiboo/items/67a410e851fa4a1f4137)を行った。
今回はNeonDBをローカルで動作させた。

[ソースコード](https://github.com/hibohiboo/odyssage/blob/ad518a8f5532ccc9b09633120f242eb8db90184a/packages/database/src/db.ts)

# ローカル環境
- Windows 11 Home 24H2 26100.2605
- Docker version 27.3.1, build ce12230
- Node v22.11.0
- bun 1.1.42
- ⛅️ wrangler 3.99.0

# DBの準備

[公式がローカル用の設定を明記している。](https://neon.tech/guides/local-development-with-neon#local-postgresql)

こちらにならってdocker-composeを記載。

```yml:infra/local/neon/docker-compose.yml
services:
  postgres:
    image: postgres:17
    command: '-d 1'
    volumes:
      - db_data:/var/lib/postgresql/data
    ports:
      - '5432:5432'
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=main
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 10s
      timeout: 5s
      retries: 5
  neon-proxy:
    image: ghcr.io/timowilhelm/local-neon-http-proxy:main
    environment:
      - PG_CONNECTION_STRING=postgres://postgres:postgres@postgres:5432/main
    ports:
      - '4444:4444'
    depends_on:
      postgres:
        condition: service_healthy
volumes:
  db_data:
```

`docker-compose up`で無事起動した。

# Drizzleを使ったマイグレーション
ローカルのneonDBへの接続のために`postgresql`が必要なので追加する。

```
bun add -D postgres 
```

ローカル用と本番用と２つの環境ができたので、環境変数も二つ用意。

```conf:packages/database/.env.local
NEON_CONNECTION_STRING=postgres://postgres:postgres@127.0.0.1:5432/main
```

```conf:packages/database/.env.prod
NEON_CONNECTION_STRING=postgresql://<DB_USER_NAME>:<DB_PASSWORD>@<NEON_DB_DOMAIN>.us-west-2.aws.neon.tech/main?sslmode=require
```

```diff:packages/database/drizzle.config.ts
import { config } from 'dotenv';
import { defineConfig } from 'drizzle-kit';

- config({ path: '.env.local' });
+ config({ path: process.env.DOTENV_CONFIG_PATH });

export default defineConfig({
  schema: './src/schema.ts',
  out: './migrations',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.NEON_CONNECTION_STRING!,
  },
});
```

```diff:packages/database/package.json
{
  "name": "@odyssage/database",
  "version": "0.0.0",
  "type": "module",
  "private": true,
  "scripts": {
    "apply": "drizzle-kit generate",
-    "migrate": "drizzle-kit migrate",
+    "migrate": "cross-env DOTENV_CONFIG_PATH=.env.prod drizzle-kit migrate",
+    "migrate-local": "cross-env DOTENV_CONFIG_PATH=.env.local drizzle-kit migrate"
  },
  "devDependencies": {
+    "cross-env": "^7.0.3",
    "dotenv": "^16.4.7",
    "drizzle-kit": "^0.30.1",
+    "postgres": "^3.4.5"
  },
  "dependencies": {
    "@neondatabase/serverless": "^0.10.4",
    "drizzle-orm": "^0.38.3"
  }
}

```

`bun run migrate-local`を行いローカルのDBにマイグレーションを行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/170e4df7-134e-57e9-0d70-0e600d478548.png)

なお、このとき`postgres`を追加せずにmigrationのコマンドを実行すると下記のようなエラーとなる。

```
$ drizzle-kit migrate
No config path provided, using default 'drizzle.config.ts'
Reading config file 'D:\projects\odyssage\packages\database\drizzle.config.ts'
Using '@neondatabase/serverless' driver for database querying
 Warning  '@neondatabase/serverless' can only connect to remote Neon/Vercel Postgres/Supabase instances through a websocket
[⣷] applying migrations...ErrorEvent {
  [Symbol(kTarget)]: _WebSocket {
    _events: [Object: null prototype] {
...
省略
...
  [Symbol(kError)]: Error: Client network socket disconnected before secure TLS connection was established
      at TLSSocket.onConnectEnd (node:_tls_wrap:1732:19)
      at TLSSocket.emit (node:events:530:35)
      at endReadableNT (node:internal/streams/readable:1698:12)
      at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
    code: 'ECONNRESET',
    path: undefined,
    host: '127.0.0.1',
    port: 443,
    localAddress: undefined
  },
  [Symbol(kMessage)]: 'Client network socket disconnected before secure TLS connection was established'
}
error: script "migrate" exited with code 1
```

## マイグレーション結果の確認
a5SQLを使って確認した。

接続設定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/f4f8471f-0583-a95a-b8b5-aa27b4603277.png)

マイグレーションしたスキーマが作成されていることを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/fd3c77a6-4d58-716e-b698-6109eecc34bf.png)


# バックエンドからの接続

## hostsの修正

接続文字列に`127.0.0.1`を使うと下記エラーが発生する。そのため、`127.0.0.1 db.localtest.me`の名前解決をローカル環境で行う必要がある。

```
[wrangler:inf] GET /api/user/ 500 Internal Server Error (26ms)    
X [ERROR] NeonDbError: invalid hostname: Common name inferred from SNI ('0.0.1') is not known
```


[各OSごとの設定](https://www.hostinger.in/tutorials/how-to-edit-hosts-file#h-how-to-edit-hosts-file-in-windows)を参考に設定した。
`C:\Windows\System32\drivers\etc\host`をメモ帳の管理者モードで開いて、下記を追記。

```
127.0.0.1 db.localtest.me
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/f39e5330-148f-66e0-2d82-a832eb4503c1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/68863d16-aefb-dc47-5ee5-4d5bb11a66de.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/bbdc1729-06cd-6b2f-05f1-54ac7f22dde1.png)

## バックエンドソースの修正
```diff:apps/backend/.dev.vars
- NEON_CONNECTION_STRING=postgresql://<DB_USER_NAME>:<DB_PASSWORD>@<NEON_DB_DOMAIN>.us-west-2.aws.neon.tech/main?sslmode=require
+ NEON_CONNECTION_STRING=postgres://postgres:postgres@db.localtest.me:5432/main
```

```diff:packages/database/src/db.ts
- import { neon } from '@neondatabase/serverless';
+ import { neon, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import type { NeonQueryFunction } from '@neondatabase/serverless';
import type { NeonHttpDatabase } from 'drizzle-orm/neon-http';

type NeonDBClient = NeonHttpDatabase<Record<string, never>> & {
  $client: NeonQueryFunction<false, false>;
};

let db: NeonDBClient | null = null;

export const getDb = (
  connectionString = process.env.NEON_CONNECTION_STRING!,
) => {
  if (db != null) {
    return db;
  }
+  if ( connectionString === 'postgres://postgres:postgres@db.localtest.me:5432/main' ) {
+    neonConfig.fetchEndpoint = `http://db.localtest.me:4444/sql`;
+    neonConfig.useSecureWebSocket = false;
+  }
  const sql = neon(connectionString);
  db = drizzle({ client: sql });
  return db;
};
```

ちなみに、接続文字列だけ変更すると下記エラーが発生する。

```
X [ERROR] NeonDbError: Error connecting to database: Network connection lost.

      at execute
  (file:///D:/projects/odyssage/node_modules/@neondatabase/serverless/index.mjs:1549:24)
  ```

# 参考
[Local Development with Neon](https://neon.tech/guides/local-development-with-neon#local-postgresql)
[Neon (Postgres) をコンテナで動かす](https://qiita.com/MatchaDev/items/a326c4d5af75cc5a8e64)
[Neon Serverless Postgres: 便利な機能と導入パターン](https://zenn.dev/bm_sms/articles/0212ba653c2136)
[サーバレスDB(Postgresql : NeonDB ) の無料版にCloudflare Workers ( Node.js) から繋いだメモ](https://qiita.com/hibohiboo/items/e4809d6b63877010db4e)