🐶

APIサーバーをCloudflare WorkersとCloud Functionsから Fastify (Cloud Run)に移行した話

に公開

こんにちは。かる(karuhi)です。
今回は、以前の記事でも少し触れた「Cloudflare Workers と Cloud Functions の併用構成」をやめて、Fastify + Cloud Run に一本化した話をまとめます。

以前の記事
https://zenn.dev/karuhi/articles/a904048bb9812d

はじめに

もともとは「少しでも安価かつシンプルにサービスを動かせないか?」という想いからユースケースに合わせて Cloudflare WorkersCloud Functions などを活用していました。しかし、「ローカルで開発・デバッグしたい」「ライブラリや型定義を一元管理したい」といった点で、構成が複雑化してしまったのが悩みでした。

そこで、ある程度枯れていて安定している Fastify でエンドポイントをまとめて実装し、Cloud Run 上で動かす構成をプロジェクト「Prometheus」[1]として進めました。移行後は約9ヶ月ほど運用していますが、いろいろとメリットがあったので、紹介したいと思います。

移行前の構成と課題

Workers と Functions を併用していた理由

以前は「手軽にサーバーレスを試してみたい」という理由で、Cloudflare Workers を複数用意し、そこに複数の API をデプロイしていました。ただ、Workers 上で動かない依存ライブラリ(サイズが大きいものなど)がある場合は、やむを得ず Google Cloud Functions に配置していました。

その結果:

  • ソース管理がプラットフォームごとにバラバラ
  • Cloudflare Workers と Functions のそれぞれで JavaScript と TypeScript が混在
  • KV や D1 などの Cloudflare 独自ストレージを使い始めるとさらに複雑化
  • ローカル開発・デバッグがやりづらい(ほぼGUIでやっていた...)

などの問題に直面しました。APIのエンドポイントが増えるにつれFunctionsやWorkersが増え、スケールに対する見通しも立ちにくくなり、保守コストも下げたい気持ちがどんどん高まっていました。[2]
「旧構成(Workers + Functions)」の構成図

どうして Fastify + Cloud Run にしたのか

選定理由:Fastify

  • 枯れている:すでにある程度実績があり、プラグインも充実している。
  • TypeScript での型管理がしやすい:自前で型定義をしっかり行いたかった。
  • 高速 & シンプル:パフォーマンスも良く、シンプルに設計できる。

個人開発やスピード重視のプロダクトで、「最低限のメンテだけで済ませたい」と考えると、Express よりモダンかつ軽量な Fastify が魅力的でした。

選定理由:Cloud Run

  • すでに Cloud Run でNext.jsアプリを運用しており、知見があった
  • コスト感・スケーリングの仕組みが把握しやすい
  • Dockerfile ベースのデプロイなので、ローカルと本番の環境差分を抑えられる

Cloud Run は、ある程度のコンテナ化の知識があれば簡単にデプロイできるのが強みです。Workers や Functions のように「クラウドプラットフォームに合わせてコードを書く」必要が薄くなり、ベンダーロックインのリスクも低めなのが嬉しいポイントでした。

新しいシステム構成の概要

技術スタック

  • Fastify(TypeScript)
  • Upstash Redis(キャッシュ/セッション管理)
  • TiDB(D1 から移行した DB)
  • Prisma(ORM として採用)
  • ESLint & Prettier(開発効率向上/コーディング規約統一)
  • nodemon(ローカル起動時のホットリロード)
  • typebox(API スキーマの型定義)
  • swagger(API ドキュメント自動生成)

新構成「Fastify + Cloud Run + Upstash Redis + TiDB」の構成図

移行プロセス

1. エンドポイントの洗い出し

まず、既存の Workers & Functions に散らばっているエンドポイントを整理しました。API 仕様の統一を目指して、typebox + Swagger でスキーマを定義しました。

import { Type, Static } from ""@fastify/type-provider-typebox"x";
import type { FastifyInstance } from "fastify/types/instance";

const HelloSchema = {
  querystring: Type.Object({
    name: Type.String(),
  }),
  response: {
    200: Type.Object({
      message: Type.String(),
    }),
  },
};

export default async function routes(fastify: FastifyInstance) {
  fastify.get(
    "/",
    {
      schema: {
        ...TestSchema,
        description: "テストスキーマ",
        tags: ["Test"],
        summary: "テストなレスポンスを返す",
      },
    },
    async (request: FastifyRequestTypebox<typeof TestSchema>, reply: FastifyReplyTypebox<typeof TestSchema>) => {
      const { name } = request.query;
      return { message: `Hello, ${name}!` };
    },
  );
}

※上記は例です。

これをSwaggerにも反映できるので、ドキュメントとコードの差分が出にくいのが最高でした。
Web上で動作確認を行うこともできます。

2. アプリケーション実装

Fastify はプラグインの仕組みが充実しているので、認証・バリデーション・キャッシュ戦略などを追加するときもスムーズでした。以下のプラグインが特に便利でした。

  • @fastify/cors
  • @fastify/helmet
  • @fastify/redis
  • @fastify/swagger-ui
  • @fastify/type-provider-typebox

3. Dockerfile 作成 & Cloud Run デプロイ

Cloud Run にデプロイするには、Node.js が動く Docker イメージを作成するだけ。
既にクライアントアプリで Docker 化の経験があったので、そこから流用しました。

FROM node:20.18.1-alpine

WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi

COPY . .

RUN pnpm build

CMD ["pnpm", "start"]

※上記は一例です。

あとは gcloud run deploy コマンドなどで Cloud Run にデプロイすれば完了。
段階的リリースを行うために、Workers/Functions のエンドポイントをクライアント側で順次切り替えながら移行しました。

運用してみて感じたメリット/デメリット

メリット

  1. 保守コストの削減
    • ワンソースでエンドポイントを管理できるので、ソースが散らばらない。
    • TypeScript の型定義や Swagger により、バグや仕様ズレを最小限に抑えられる。
  2. パフォーマンス・スケーリングの安定感
    • Cloud Run は「必要に応じて自動スケール」してくれるので、急なアクセス増にも対応できる安心感。
    • 重い処理(レコメンドや NLP)も意外と動かせる。[3]
  3. ローカル開発がしやすい
    • 「ローカルで快適にデバッグ」→「そのまま本番へ」がスムーズ。

デメリット

  • いまのところ大きなデメリットは感じていません。
  • 少しGoogleCloudやDockerまわりの知識が必須になるので、Workers や Functions のように「コードだけ書けば動く」とはいかない。
  • FastifyとTypeboxを活用するには、若干の学習コストとTypeScriptの前提知識が必要かもしれない(構成によっては型補完させるために工夫が必要)
  • ブログの方で問題点としてあげていたコールドスタートに関しては、ほぼ常にリクエストがあるからか、それほど問題になっていません。

9ヶ月間運用して思うこと

移行してから約9ヶ月が経ちますが、特に大きなトラブルはなく、メンテナンス不要レベルで安定動作しています。もちろんモニタリングやログは Cloud Logging で見ていますが、これといった障害もなく、とても快適です。

  • ログ管理:Cloud Run → Cloud Logging で確認可能
  • CI/CD:GitHub Actions と Cloud Build で自動デプロイ
  • 監視ツール:Sentry や Datadog の導入もしたい(まだ手が回っていませんが...)

まとめ

  • 「Workers + Functions」から「Fastify + Cloud Run」への一本化は、想像以上にメリットが大きかった。
  • スケーリングやコスト、そして開発効率を考えると、TypeScript と Docker を軸にしておくのはTSをメインに書いている私個人的にも安心感がある。
  • これから個人開発やスモールスタートのプロジェクトで迷っている方にも、Fastify + Cloud Run はおすすめできる選択肢。

もし同じような構成で悩んでいたり、「Workers と Functions を併用しているけど管理が大変…」という方がいらっしゃれば、ぜひ試してみてください!

おわりに

今回の移行話が、同じような課題を抱えている方の参考になれば幸いです。「スピード重視だけど、長期的に保守コストを抑えたい」という方には特におすすめできる構成かなと思っています。

最後まで読んでいただきありがとうございました!
もし何か気になる点や意見がありましたら、ぜひコメントなどで教えてください!
また、Likeも頂けると励みになります💚

脚注
  1. プロメテウスはギリシャ神話の中で、神々に逆らって人類に火を与えた英雄らしいです。かっこ良さそうな名前をServiceの命名に使いたかったので、これにしました。特に意図はないですw ↩︎

  2. 最初からちゃんとWorkers上とかで真面目に構成考えてやっておけばよかったという話ではある、Honoとか便利でアツいし...。 ↩︎

  3. 久々にtokenizerって聞いたし、ベクトルの内積計算とか楽しかった...(今は素人が作ったのもあって精度低いので、SaaSに任せてます) ↩︎

Discussion