Closed11

だれでもAIメーカーの技術スタックとか

catnosecatnose

主な使用サービス/ライブラリは以下です。

  • Next.js …アプリケーションのフレームワーク
  • Vercel …デプロイ先
  • PlanetScale …サーバーレスDB(MySQL)。ORMにはPrismaを使用
  • Upstash …サーバーレスでRedisを使えるやつ
  • Cloudflare R2 …画像のアップロード先
  • Open AI API

ここに落ち着くまでに紆余曲折あったので、少し詳しく説明しておきます。

catnosecatnose

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が実行されてしまう
catnosecatnose

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を利用しています。

catnosecatnose

Upstash

個人的に最近気に入ってるのがUpstashというサーバーレスでRedisが使えるサービスです。

https://upstash.com

1人のユーザーがOpenAIのAPIを無限にリクエスト出来ないようにレートリミットを導入する必要があったため、Upstashを使うことにしました。

ちなみにUpstashは東京リージョンにも対応しており、VercelのServerless Functionsのリージョンと合わせておけば低レイテンシでアクセスできます。HTTPベースなのでEdgeランタイムでも使用することもできます。

ちょうど最近Vercel KVというサービスがリリースされましたが、これも裏側ではUpstashを使っているようですね。

catnosecatnose

Cloudflare R2

アバター画像のアップロード先にCloudflare R2を使っています。S3やCloudStorageと比べて圧倒的に安く、十分に使いやすいため、新規プロジェクトではR2を率先して使うようになりました。

catnosecatnose

実装周りの話し

OG画像を動的に生成する

主にSNS経由で流入してもらうネタ系のサービスであるため、「AIサービスのトップページ」と「AIサービスの結果ページ」では動的にOG画像を生成するようにしました。

動的生成のOG画像の例

VercelでOG画像を動的生成する場合、@vercel/ogというパッケージを使えば、レンダリングする画像をJSX記法で書くことができます。

@vercel/ogはEdgeランタイムにのみ対応しており、Next.jsのAPI Routesから使うには以下のような書き方をすることになります。

/api/og-image/[id].tsx
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>
    )
  )
}

ここで問題になるのは日本語フォントの読み込みです。

https://zenn.dev/hiromu617/articles/c03fef6f4d6c6e

この記事にも書かれているようにVercelのEdge Functionsには以下のようなサイズの制限があります。

  • Hobbyプラン: 1MB
  • Proプラン: 2MB

今回はVercelのプランは「Pro」であったため、2MBの制約が適用されるわけですが、日本語のフォント 1ウェイト分をそのままimportするとどうしても2MBを超えてしまいました。

そこでサブセットフォントメーカーを使って出番の少なそうな文字列を取り除くことにしました。
同じことをやっている記事を見るとだいたい「JIS第2水準漢字」をほぼばっさり捨てていたのですが、それだと豆腐()になってしまう文字が頻出してしまったので、目視で「これ見たことねぇな」と思った漢字をひたすら取り除いていく作業を行いました。

作業を続けていくうちに過去に経験したことがないほどにゲシュタルト崩壊を起こし、どの漢字を見ても見たことがあるのかないのか分からなくなってしまったので、WOFF変換後のサイズが1.1MBくらいになった時点で作業を切り上げました。

(ちなみにChatGPTに使用頻度の少ない漢字を取り除く作業をお願いしてみたりもしたのですが、全くダメでした)

catnosecatnose

返信が遅くなってすみません、完全に見落としてました…!
外部から読み込む方法はレイテンシが大きくなりそうだと敬遠していたのですが、たしかにtext=で必要なフォントだけを読み込むようにすればある程度解消できそうですね。
今度実際に試してみようと思います。デモのリポジトリまで用意していただき、本当にありがとうございました。

catnosecatnose

匿名認証とレートリミット

サービスを気軽に使ってもらえるように未ログインでも使用できる仕様にしたこともあり、悪用を防ぐために匿名認証とレートリミットを導入しています。これについては別の記事でまとめています。

https://zenn.dev/catnose99/articles/9183c86d3558e5

ちなみに匿名認証に関するエンドポイントはアクセスが比較的多くなりそうだったので、EdgeランタイムのAPI Routesを使っています。

catnosecatnose

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

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