Cloudflare WorkersにGraphQLサーバを作る
GraphQLサーバをCloudflare Workersに立てることが出来るかの実験を書き留めるメモ
モチベーション
- GraphQLサーバは初手でCloud Runで立てるけど、欲が出てくる
- デプロイがもうちょい早くならないかな
- 料金が安ければ安いほど嬉しい(非同期とか使うとAlways CPU使うので料金もそこそこ)
- Cloud Runと同じくサーバレスで運用楽ちんでお願いしたい(勝手にスケールしてて)
- コールドスタンバイからの起動が早いに越したことはない
Runtime
上記の要望を叶えるにはほぼ選択肢はなくて、Cloudflare Workersを使う。
Webサーバ
Cloudflare Workersということで選択肢が限られるので初手 hono
と言いたいが、GraphQLとなると…なんで、Cloudflare Workersで動くGraphQLサーバの有力候補である graphql-yoga
を使う。
GraphQL schema builder
既存が使っているというのもあるが、コードファーストでスキーマを自動生成してくれるので非常に気に入ってる。なので引き続きコードファーストなスキーマビルダーの Pothos
を使用する。
ORM
Cloudflareなんで、DBはD1と言いたいがまだアルファなんで一旦は普通にSupbaseなどのDBを選択して、後にD1に変えれるようにアダプターを変えたら動くくらいのORMが嬉しい。そうなると drizzle
か Kysely
になるわけだけど、今回は既存のコードを置き換えるという自身の思惑が入っているのでPrismaを使ってるから Kysely
を選択。
drizzle
でもいいけど、若干動きが怪しかったりするということもある。
honoの可能性
- The Guildが
hono
に降臨して、hono
の互換性を持たす可能性を示唆 - 無理に
Request
オブジェクト渡したら動く的なサンプルコード - 何より @yusukebe がhonoの実績作ってほしそうにこっちを見てる
とりあえず、最後に hono
で動く状態にするというのを目標にはしておく(´・ω・`)
環境設定
The GuildにCloudflare Workers + GraphQL Yogaのテンプレートがある。
が、wranglerが古かったりするので、c3
を使ってゼロからセットアップする
Cloudflare Workesの作成
$ npm create cloudflare
GraphQLはテンプレートにないので、Hello Worldを出力するだけの真っ白なテンプレートを使う。
必要なライブラリの設定
とりあえずDBは無視して、GraphQLが動くところまで行く。 graphql-yoga
のドキュメントにインストール手順があるので入れていく。
$ npm install graphql graphql-yoga @pothos/core
GraphQLスキーマの作成
Pothos
でサンプルを以下のように作る
import SchemaBuilder from '@pothos/core'
const builder = new SchemaBuilder({})
builder.queryType({
fields: (t) => ({
hello: t.string({
resolve: () => 'world',
}),
}),
})
export const schema = builder.toSchema()
作ったスキーマを graphql-yoga
で読み込んでGraphQLサーバを立ち上げる
import { createYoga } from 'graphql-yoga'
import { schema } from './schema'
// Create a Yoga instance with a GraphQL schema.
const yoga = createYoga({ schema })
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return yoga(request, ctx)
},
}
npm run start
でサーバを立ち上げて、ブラウザで http://localhost:8787/graphql を開いてQueryを叩けば動く
DBとつなぎ込み
DBスキーマの作成
必要なライブラリのインストール
$ npm install -D prisma prisma-kysely @types/pg
$ npm install kysely pg
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator kysely {
provider = "prisma-kysely"
output = "../src"
fileName = "types.ts"
}
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
email String
}
npx prisma migrate dev
でマイグレーションの作成とKysely用の型ファイルを出力する
DBとの接続
PostgreSQLと接続を想定してライブラリをインストール
$ npm install -D @types/pg
$ npm install pg
KyselyでDBに接続するクライアントを作成する
import type { DB } from './types'
import { Pool } from 'pg'
import { Kysely, PostgresDialect, Generated, ColumnType, Selectable, Insertable, Updateable } from 'kysely'
export const db = new Kysely<DB>({
// Use MysqlDialect for MySQL and SqliteDialect for SQLite.
dialect: new PostgresDialect({
pool: new Pool({
host: 'localhost',
database: 'postgres',
user: 'postgres',
password: 'your password',
}),
}),
})
GraphQLスキーマの作成
必要なライブラリのインストール
$ npm install @pothos/plugin-simple-objects
import SchemaBuilder from '@pothos/core'
import SimpleObjectsPlugin from '@pothos/plugin-simple-objects'
export const builder = new SchemaBuilder({
plugins: [SimpleObjectsPlugin],
})
- import SchemaBuilder from '@pothos/core'
- const builder = new SchemaBuilder({})
+ import { builder } from './builder'
builder.queryType({
fields: (t) => ({
hello: t.string({
resolve: () => 'world',
}),
}),
})
+ export * from './models'
+ export * from './resolvers'
export const schema = builder.toSchema()
export * from './User'
import { builder } from '../builder'
export const UserType = builder.simpleObject('User', {
fields: (t) => ({
id: t.int(),
name: t.string({ nullable: true }),
email: t.string(),
}),
})
export * from './User'
export * from './query'
import { builder } from '../../builder'
import { UserType } from '../../models/User'
import { db } from '../../context'
builder.queryFields((t) => ({
User: t.field({
type: UserType,
nullable: true,
resolve: async () => await db.selectFrom('User').selectAll().executeTakeFirst(),
}),
}))
最後に pg
をCloudflare Workersで動作させるために、先日発表のあったTCPでの接続が必要なので wrangler.toml
に以下の設定が必要
node_compat = true
これで http://localhost:8787/graphql でQueryを投げるとデータが返ってくる
が、ブラウザ上で1回目は問題ないが、2回目がエラーとなる。
2回目のリクエストが失敗する原因
たぶん、これ
どうやらCloudflare WorkersのTCPはリクエストごとに切れる。Kyselyで使用するドライバーはconnection poolがあるので接続情報としてはオブジェクトが残っている。そこで2回目のリクエストの場合はKyselyはconnection poolから前の接続情報をオブジェクトととして持ってくるが、Cloudflare WorkersではTCPが既に切れてしまっているので接続が失敗して、リクエストが処理できないということ。
なので、若干乱暴だが接続部分を以下のように変える。
import type { DB } from './types'
import { Pool } from 'pg'
import { Kysely, PostgresDialect, Generated, ColumnType, Selectable, Insertable, Updateable } from 'kysely'
- export const db = new Kysely<DB>({
+ export const connection = () => new Kysely<DB>({
// Use MysqlDialect for MySQL and SqliteDialect for SQLite.
dialect: new PostgresDialect({
pool: new Pool({
host: 'localhost',
database: 'postgres',
user: 'postgres',
password: 'your password',
}),
}),
})
import { builder } from '../../builder'
import { UserType } from '../../models/User'
- import { db } from '../../context'
+ import { connection } from '../../context'
builder.queryFields((t) => ({
User: t.field({
type: UserType,
nullable: true,
resolve: async () => {
+ const db = connection()
return await db.selectFrom('User').selectAll().executeTakeFirst()
},
}),
}))
要は無理やりつなぎ直すようにする。これでconnection poolは効かなくなる。そのため、PostgreSQLならばPgBouncerを使用してコネクション数が問題にならないようにする必要がある。