だれでもAIメーカーの技術スタックとか
だれでもAIメーカーというWebサービスを作りました。このスクラップでサービスの技術的な話をまとめておきます。
主な使用サービス/ライブラリは以下です。
- Next.js …アプリケーションのフレームワーク
- Vercel …デプロイ先
- PlanetScale …サーバーレスDB(MySQL)。ORMにはPrismaを使用
- Upstash …サーバーレスでRedisを使えるやつ
- Cloudflare R2 …画像のアップロード先
- Open AI API
ここに落ち着くまでに紆余曲折あったので、少し詳しく説明しておきます。
Next.js on Vercel
利用しているフレームワークはNext.jsです。クライアントからのデータの取得・更新リクエストはAPI Routesから受け付けるようにしています。
アプリケーションのデプロイ先はVercelにしました。最初はNext.js on Cloudflare Workersをやろうとしたのですが、辛い部分が多くて断念しました。
余談)なぜNext.jsをCloudflare Workersで動かしたかったか
低コストで運用でき、大量のアクセスが来ても低コストでスケールできるためです。以前Cloudflare WorkersでSSRができると何が嬉しいかに書いたのと同じ理由になります。
余談)Next.js on Cloudflare Workersの何が辛かったか
Cloudflareのドキュメントに載っているcloudflare/next-on-pages
を使ったやり方を試したのですが、現状だとバグっぽい挙動が多く、ワークアラウンドのコードがどんどん増えてしまいました。
ワークアラウンドの例
当時のREADMEに書かれていた内容をペタリ
🚨
public/_routes.json
にて静的なページや静的なファイルへのパスをexclude
で指定する必要あり
- Cloudflare Pages on Next.jsしたときに、2023/03/27時点ではpublicディレクトリ内に
_routes.json
を作成しないとpublic/images/*
などの静的ファイルに対してもfunctionsが走ってしまう/apps/new
と/apps/[id]
というページがあったとき、/apps/new
の方は静的ファイルとしてアップロードされる。しかし/apps/[id]
のfunctionsが走ってしまうため、/apps/new
にアクセスしても/apps/[id]
のSSRが実行されてしまう
PlanetScale
Next.js on Cloudflare Workersを試している段階でDBはPlanetScaleにすることを決めました。
PlanetScaleは低コストから利用でき、開発者体験が良く、本当に素晴らしいサービスです。
Cloudflare WorkersからPlanetScaleに接続する場合、基本的に@planetscale/databaseというfetch APIをベースに動く専用のパッケージを使うことになります。これと合わせてORMを使いたいところなのですが、Prismaは現状@planetscale/database
に対応していません。
drizzleという新星のORMを導入してみてその使い勝手の良さにびっくりしたものの、ふと「サービス開発より技術選定が楽しくなってしまってる」「このままのペースだと1ヶ月どころか半年かかる」と気づき、Next.js on Vercel + PlanetScale + Prismaという構成に切り替えることにしました。
※ VercelのEdgeランタイムからDBへのアクセスが必要になるケースでは、Prismaではなく@planetscale/database
を利用しています。
Upstash
個人的に最近気に入ってるのがUpstashというサーバーレスでRedisが使えるサービスです。
1人のユーザーがOpenAIのAPIを無限にリクエスト出来ないようにレートリミットを導入する必要があったため、Upstashを使うことにしました。
ちなみにUpstashは東京リージョンにも対応しており、VercelのServerless Functionsのリージョンと合わせておけば低レイテンシでアクセスできます。HTTPベースなのでEdgeランタイムでも使用することもできます。
ちょうど最近Vercel KVというサービスがリリースされましたが、これも裏側ではUpstashを使っているようですね。
Cloudflare R2
アバター画像のアップロード先にCloudflare R2を使っています。S3やCloudStorageと比べて圧倒的に安く、十分に使いやすいため、新規プロジェクトではR2を率先して使うようになりました。
実装周りの話し
OG画像を動的に生成する
主にSNS経由で流入してもらうネタ系のサービスであるため、「AIサービスのトップページ」と「AIサービスの結果ページ」では動的にOG画像を生成するようにしました。
VercelでOG画像を動的生成する場合、@vercel/ogというパッケージを使えば、レンダリングする画像をJSX記法で書くことができます。
@vercel/og
はEdgeランタイムにのみ対応しており、Next.jsのAPI Routesから使うには以下のような書き方をすることになります。
import { ImageResponse } from '@vercel/og'
// ランタイムをedgeに
export const config = {
runtime: 'edge',
};
export default async function ogImagehandler() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
fontSize: 128,
}}
>
Hello!
</div>
)
)
}
ここで問題になるのは日本語フォントの読み込みです。
この記事にも書かれているようにVercelのEdge Functionsには以下のようなサイズの制限があります。
- Hobbyプラン: 1MB
- Proプラン: 2MB
今回はVercelのプランは「Pro」であったため、2MBの制約が適用されるわけですが、日本語のフォント 1ウェイト分をそのままimportするとどうしても2MBを超えてしまいました。
そこでサブセットフォントメーカーを使って出番の少なそうな文字列を取り除くことにしました。
同じことをやっている記事を見るとだいたい「JIS第2水準漢字」をほぼばっさり捨てていたのですが、それだと豆腐(□
)になってしまう文字が頻出してしまったので、目視で「これ見たことねぇな」と思った漢字をひたすら取り除いていく作業を行いました。
作業を続けていくうちに過去に経験したことがないほどにゲシュタルト崩壊を起こし、どの漢字を見ても見たことがあるのかないのか分からなくなってしまったので、WOFF変換後のサイズが1.1MBくらいになった時点で作業を切り上げました。
(ちなみにChatGPTに使用頻度の少ない漢字を取り除く作業をお願いしてみたりもしたのですが、全くダメでした)
匿名認証とレートリミット
サービスを気軽に使ってもらえるように未ログインでも使用できる仕様にしたこともあり、悪用を防ぐために匿名認証とレートリミットを導入しています。これについては別の記事でまとめています。
ちなみに匿名認証に関するエンドポイントはアクセスが比較的多くなりそうだったので、EdgeランタイムのAPI Routesを使っています。
Serverless Functionsの使用を節約する
VercelのServerless Functionsの同時実行数はProプランだと1,000までとなっています。バースト等によりこの制限を超えてしまうとサービスが全く動かなくなってしまうため、可能な限りEdge API RoutesやSSG、ISRを使うようにしています。
The maximum number of concurrent executions for Serverless Functions is 1000 by default.
Serverless Function Concurrency