Closed5

Cloudflare WorkersにGraphQLサーバを作る

chimamechimame

GraphQLサーバをCloudflare Workersに立てることが出来るかの実験を書き留めるメモ

モチベーション

  • GraphQLサーバは初手でCloud Runで立てるけど、欲が出てくる
    • デプロイがもうちょい早くならないかな
    • 料金が安ければ安いほど嬉しい(非同期とか使うとAlways CPU使うので料金もそこそこ)
  • Cloud Runと同じくサーバレスで運用楽ちんでお願いしたい(勝手にスケールしてて)
  • コールドスタンバイからの起動が早いに越したことはない

Runtime

上記の要望を叶えるにはほぼ選択肢はなくて、Cloudflare Workersを使う。

https://workers.cloudflare.com/

Webサーバ

Cloudflare Workersということで選択肢が限られるので初手 hono と言いたいが、GraphQLとなると…なんで、Cloudflare Workersで動くGraphQLサーバの有力候補である graphql-yoga を使う。

https://the-guild.dev/graphql/yoga-server

GraphQL schema builder

既存が使っているというのもあるが、コードファーストでスキーマを自動生成してくれるので非常に気に入ってる。なので引き続きコードファーストなスキーマビルダーの Pothos を使用する。

https://pothos-graphql.dev/

ORM

Cloudflareなんで、DBはD1と言いたいがまだアルファなんで一旦は普通にSupbaseなどのDBを選択して、後にD1に変えれるようにアダプターを変えたら動くくらいのORMが嬉しい。そうなると drizzleKysely になるわけだけど、今回は既存のコードを置き換えるという自身の思惑が入っているのでPrismaを使ってるから Kysely を選択。

https://github.com/kysely-org/kysely

drizzle でもいいけど、若干動きが怪しかったりするということもある。

https://zenn.dev/chimame/scraps/17c8dd38eefa0c

chimamechimame

honoの可能性

https://twitter.com/yusukebe/status/1661651168182046720

  • The Guildが hono に降臨して、 hono の互換性を持たす可能性を示唆
  • 無理に Request オブジェクト渡したら動く的なサンプルコード
  • 何より @yusukebe がhonoの実績作ってほしそうにこっちを見てる

とりあえず、最後に hono で動く状態にするというのを目標にはしておく(´・ω・`)

chimamechimame

環境設定

The GuildにCloudflare Workers + GraphQL Yogaのテンプレートがある。

https://github.com/the-guild-org/yoga-cloudflare-workers-template

が、wranglerが古かったりするので、c3 を使ってゼロからセットアップする

Cloudflare Workesの作成

$ npm create cloudflare

GraphQLはテンプレートにないので、Hello Worldを出力するだけの真っ白なテンプレートを使う。

必要なライブラリの設定

とりあえずDBは無視して、GraphQLが動くところまで行く。 graphql-yoga のドキュメントにインストール手順があるので入れていく。

$ npm install graphql graphql-yoga @pothos/core

GraphQLスキーマの作成

Pothos でサンプルを以下のように作る

schema.ts
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サーバを立ち上げる

worker.ts
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を叩けば動く

chimamechimame

DBとつなぎ込み

DBスキーマの作成

必要なライブラリのインストール

$ npm install -D prisma prisma-kysely @types/pg
$ npm install kysely pg
prisma/schema.prisma
// 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に接続するクライアントを作成する

src/context.ts
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
src/builder.ts
import SchemaBuilder from '@pothos/core'
import SimpleObjectsPlugin from '@pothos/plugin-simple-objects'

export const builder = new SchemaBuilder({
  plugins: [SimpleObjectsPlugin],
})
src/schema.ts
- 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()
src/models/index.ts
export * from './User'
src/models/User.ts
import { builder } from '../builder'

export const UserType = builder.simpleObject('User', {
  fields: (t) => ({
    id: t.int(),
    name: t.string({ nullable: true }),
    email: t.string(),
  }),
})
src/resolvers/index.ts
export * from './User'
src/resolvers/User/index.ts
export * from './query'
src/resolvers/User/query.ts
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 に以下の設定が必要

wrangler.toml
node_compat = true

これで http://localhost:8787/graphql でQueryを投げるとデータが返ってくる

が、ブラウザ上で1回目は問題ないが、2回目がエラーとなる。

chimamechimame

2回目のリクエストが失敗する原因

たぶん、これ
https://developers.cloudflare.com/workers/databases/connect-to-postgres/#connection-pooling--startup

どうやらCloudflare WorkersのTCPはリクエストごとに切れる。Kyselyで使用するドライバーはconnection poolがあるので接続情報としてはオブジェクトが残っている。そこで2回目のリクエストの場合はKyselyはconnection poolから前の接続情報をオブジェクトととして持ってくるが、Cloudflare WorkersではTCPが既に切れてしまっているので接続が失敗して、リクエストが処理できないということ。

なので、若干乱暴だが接続部分を以下のように変える。

src/context.ts
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',
    }),
  }),
})
src/resolvers/User/query.ts
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を使用してコネクション数が問題にならないようにする必要がある。

このスクラップは2023/07/29にクローズされました