こんにちは。Cloudflare と Ruby が好きな者です。
タイトルの通り、Cloudflare Workers 上で動作するアプリを Ruby で書きたくて試行錯誤した結果、ほとんど自分にしか需要のなさそうな俺得フレームワークを Ruby WASM で作りましたのでそのことを書きます。
Cloudflare Workersとは
同サービスに詳しい人には冗長ですが一応。
Cloudflare Workers は CDN(コンテンツ・デリバリー・ネットワーク)で有名な Cloudflare が提供するサービスです。
Cloudflare Workers とその周辺サービスは Cloudflare が提供するエッジコンピューティング環境であり、最大の特徴はアプリケーションを中央サーバーではなく、よりユーザーに近いエッジロケーション側で実行する点にあります。
アプリケーションだけでなく、Queueサービス、生成AI(LLM)、ベクトルDB、そしてリレーショナル・データベースまでもエッジ側で実行しユーザーに対して高速な応答を実現します。
ここでの『エッジ』はIoT端末のことではありません。Cloudflareが世界中の都市に展開している、ネットワーク接続拠点(データセンター)を指します
Cloudflare Workers はコンテナではなく、お互いに分離された無数のV8ランタイム上でJavaScriptを直接実行します(TypeScriptはデプロイ時にツールがJSにコンパイルするらしい)。
従って、最も推奨される開発言語は TypeScriptまたはJavaScriptです(以下の図は公式サイトから引用)
ただ、同時にWASMもサポートしており、Ruby 3.2 から標準サポートされているWASMでも動かせるのではないかと思いチャレンジしてみました。
(文脈と関係ありませんが、 「実行場所:全地球」 かっこいい。痺れる。)
Cloudflare Workers の WASMサポート状況
2025年11月現在、Cloudflare Workers の各言語サポート状況は以下のようになります。
| グループ | 言語 | 実行の仕組み (サーバー側) | 開発サポート (公式) |
|---|---|---|---|
| 1. ネイティブ | JavaScript TypeScript |
V8エンジンが直接コードを実行 |
公式サポート (ドキュメント・テンプレート完備) |
| 2. 準ネイティブ | Python |
Pyodide (Wasm) (Cloudflareがランタイムを提供) |
公式サポート (Wasmビルド不要、テンプレート有) |
| 3. 準ネイティブ |
Rust Go (TinyGo) |
Wasmバイナリ (ユーザー側でビルド) |
準公式サポート (専用ライブラリ・ビルド自動化) |
| 4. 一般Wasm | C++ Kotlin Ruby 等 |
Wasmバイナリ (ユーザー側でビルド) |
限定的 / なし (コミュニティ任せ) |
Python は Cloudflare側でWASMが用意されており、RustやGoは専用のライブラリやツールが提供されていますが、Rubyを含めた他の言語は特別なサポートが無い状況です。
したがって、PythonプロジェクトではPythonのWASMランタイムを含める必要がありませんが、Rubyではプロジェクト中に Ruby WASM を含めた形でデプロイする必要があります。
やったこと
最初はCloudflare Workers上で Rubyのちょっとしたコードを文字列として与えて動作検証するところからスタートし、最終的には以下のようなコードでHTTPリクエストをルーティングして色々できるフレームワークっぽいものを作るに至りました。
TypeScript界隈で人気のあるHonoのようなシンプルで機能的なフレームワークの上でRubyコードを書きたいと思って作りましたが、あくまで趣味で作ったものなので、とても商用サービスの本番環境で使えるようなものではありません。それでもまあ名前は必要かと思って Hibana (火花) と名付けました。
GitHubは以下。
プロジェクトの開始
大前提として Cloudflare にユーザー登録が必要です。(無料プランであればクレジットカード登録不要)
また、npmコマンドが使える環境であること前提です。最近は大体の人が入っていると思いますが、入ってない人はインストールが必要です。
Hibanaプロジェクトの雛形は以下の1コマンドで作成できます。Railsでいうところの rails new ですね。
npm create hibana@latest <project-name>
<project-name>は自身のプロジェクト名で。
例えばプロジェクト名が my-app の場合は以下を実行します。
npm create hibana@latest my-app
開発用サーバーの起動
cd my-app
npm install
npm run dev
これだけで、ローカルで開発用サーバーが起動します。(動かなかったらごめんなさい、コメントで教えてください)
他に8787ポートを使っているプロセスがなければ8787ポートでHTTPサーバーが起動し、 http://localhost:8787 でサンプルページが表示されると思います。
また、 http://localhost:8787/hello にアクセスすると、Hello from Ruby WASM! と表示されます。
実際に Cloudflare Workers にデプロイしたサンプルは以下からアクセスできます。
https://my-app.hiroe-orz17.workers.dev/
デプロイ
事前にプロジェクトルートで npx wrangler login で認証をした上で、
npm run deploy
すると、たったこれだけでCloudflare Workers にデプロイされます。
インスタンスやコンテナの準備は一切必要ないのが Cloudflare Workers のいいところ。
しかもアプリには自動的にホスト名(例: https://my-app.hiroe-orz17.workers.dev/ )が割り当てられますし、必要なら自分で取得したホスト名を使うことも可能です。
新規プロジェクトのファイル構成
生成したプロジェクトの主なファイル構成は以下のようになってます。
app/
app.rb # ルーティングとビジネスロジック 編集するのは主にこれ
helpers/ # ここにRubyファイルを配置すると自動でrequireする
application_helper.rb # グローバルなヘルパー、初期状態は空
models/ # ORMを配置するディレクトリ。初期状態では存在しない
templates/
index.html.erb # テンプレートファイル
layouts/
application.html.erb # 共通レイアウト
public/ # 静的ファイル
index.html
src/ # 起点となるTypeScriptのソースファイル ここのファイルは基本いじらない
index.ts
wrangler.toml # Cloudflare Workers の設定ファイル
Hello World
ルーティングは app/app.rb に Sinatraライクにブロックで定義します。
例えば最もシンプルな Hello World は次のようになります。
get "/hello" do |c|
c.text("Hello Hibana ⚡")
end
サンプル: https://my-app.hiroe-orz17.workers.dev/hello
パラメータの取得
パスの定義中に :パラメータ名 のように定義することでパラメータとして取得できます。
get "/post/:year/:month/:day" do |c|
year = c.params[:year]
month = c.params[:month]
day = c.params[:day]
c.text("#{year}-#{month}-#{day}")
end
サンプル: https://my-app.hiroe-orz17.workers.dev/post/2025/12/01
GETパラメータも
get "/query" do |c|
name = c.query["name"]
age = c.query["age"]
c.text("Name: #{name}, Age: #{age}")
end
サンプル: https://my-app.hiroe-orz17.workers.dev/query?name=Mike&age=20
POSTはこんな感じ。 json_body form_body raw_body が使えます。
post "/echo" do |c|
c.json(
content_type: c.content_type,
params: c.params,
json_body: c.json_body,
form_body: c.form_body,
raw_body: c.raw_body,
)
end
curl -X POST "https://my-app.hiroe-orz17.workers.dev/echo" \
-H "Content-Type: application/json" \
-d '{
"name": "Hanako",
"age": 29,
"likes": ["coffee", "books"]
}'
リレーショナル・データベース(D1)との連携
Cloudflare D1 は、1つのプライマリと世界中の複数リージョンのリードレプリカからなる、Cloudflare ネットワーク上のサーバレスなリレーショナル・データベースです。
D1はCloudflareのエッジサービスの中でも最も野心的な機能の一つと思います。それまでの 「DBと連携するような複雑な処理は中央サーバーで実行し、エッジロケーションはその結果をキャッシュするのみ」 という常識を覆しました。
D1の実態は、一箇所のプライマリ(マスター)と、世界中に分散配置されたリードレプリカで構成される SQLite ベースのデータベースです。
書き込みができる マスターは最初にデータベースが作成された地域のエッジに自動的に配置 され(作成時に指定可能)、 世界中にリードレプリカを分散配置 。リードレプリカへは非同期書き込みすることで、ある程度の一貫性と参照スピードを両立しています。
Hibanaを使うとD1への操作は以下のように行えます。
db = c.env(:DB)
result = db.prepare("SELECT * FROM posts WHERE id = ?").bind(1).first
サンプル: https://my-app.hiroe-orz17.workers.dev/d1
ORMも作ってみた
個人的には生のSQL書くのにそんなに抵抗感はないのですが、ActiveRecordみたいなORMもあったら便利かなと思って薄いORMを実装しました。
こんな感じでモデルを書いて
class Post < Hibana::Record
table_name "posts"
primary_key :id
timestamps true
attribute :title, :string
attribute :views, :integer, default: 0
attribute :status, :string, default: "draft"
belongs_to :user
scope :published, -> { where(status: "published") }
end
DB操作はActiveRecordっぽくこんな感じです。
posts = Post
.published
.where("views >= ?", 1_000)
.order(views: :desc)
.limit(20)
サンプル: https://my-app.hiroe-orz17.workers.dev/orm/sample
生成AIもエッジで実行
2023年9月、Cloudflare はエッジロケーションで生成AIのLLMを実行できる Workers AI を発表しました。
Workers AI:Cloudflareのグローバルネットワーク上でサーバーレスGPUによる推論を実現
Hibanaでも Workers AI をサポートし、エッジロケーションでのAI連携ができるようにしました。
例えば OpenAI が提供している GPT-OSS を使ったサンプルコードは以下のようになります。
get "/ai-demo-gpt-oss" do |c|
ai = c.env(:AI)
prompt = "Cloudflare Workers AIとはなんですか? 日本語でわかりやすく教えて"
model = "@cf/openai/gpt-oss-20b"
result = ai.run(
model: model,
payload: {
input: prompt,
reasoning: {
effort: "low",
summary: "auto"
}
},
)
c.json({prompt: prompt, result: result})
rescue WorkersAI::Error => e
c.json({ error: e.message, details: e.details }, status: 500)
end
このコードを Cloudflare Workers で実行すると、AIの回答が表示されます。
gpt-ossは公開されたけど自宅のPCではGPUパワーが足りなくて試せなかったという方も、Workers AI を使えば簡単に試せます。しかも Ruby で。
サンプル: https://my-app.hiroe-orz17.workers.dev/ai-demo-gpt-oss
テンプレート・レンダリング
ここまで来たらテンプレートのレンダリングも欲しいなと思って作りました。内部ではRuby標準のERBを使わせて頂いています。
app/
app.rb
templates/
index.html.erb
layouts/
application.html.erb
レイアウトファイル
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hibana</title>
</head>
<body>
<%= yield %>
</body>
</html>
ERBファイル
<h1>Hello <%= name %></h1>
<p>Age: <%= age %></p>
テンプレート内で使用する変数は c.render() にハッシュ形式で渡します。
get "/template" do |c|
c.render("index", name: "Hibana", age: 50)
end
サンプル: https://my-app.hiroe-orz17.workers.dev/template
S3互換のR2も使える
Cloudflare にはAWS S3互換のR2というオブジェクトストレージがあります。
特徴は
- データのダウンロード(Cloudflareから外方向)に課金がなく無料
- S3のような厳密なリージョン選択をユーザーに意識させず、データへのアクセスは世界中のエッジロケーションを経由して高速化される
です。
Hibanaでのサンプルコードは次のようになります。
bucket = c.env(:MY_R2)
key = "my-key"
value = "This is R2 sample."
# 保存
bucket.put(key, value)
# 参照
value = bucket.get(key).text
サンプル: https://my-app.hiroe-orz17.workers.dev/r2
その他のサンプル
上記以外にキーバリューストアのKVや Queue, HTTPリクエストを実行する fetch も実装しました。
サンプルコードは github の README.ja.md に書いてます。
工夫したところ
Cloudflare D1などの機能をRuby側で使うための仕組み
Cloudflare Workers のランタイムである V8 で WASM が実行できるのは既知のことなのでそれは比較的簡単にできたのですが、これまでに紹介したリレーショナル・データベースのD1やAI、S3互換のR2などと連携できないと面白くありません。
一方で、これらの機能は Workers ランタイム内では TypeScript/JavaScript からのみ直接利用でき、Ruby WASM からはそのままでは D1 や R2 のバインディングに触れたり、fetch で外部 HTTP リクエストを送ったりはできません(従ってAPIを叩くこともできません)。
この問題に対処するため、
- TypeScript側でD1やR2などを操作する低レベル関数を予め用意しておき
- それをアプリ起動時に Ruby側で定義されている特異クラスの attr_accessor に登録して
- Rubyでメソッドが呼ばれたときは attr_accessor に登録された TypeScript の関数をコールすることで Ruby と TypeScript の連携を実現
これによってD1・KVなど TS/JS からのみ使える周辺機能を Ruby からも使えるようにしました。
ruby-runtime.tsから、主要な部分を抜粋したコードが以下になります。
// ① Ruby WASM の VMをロード
import { DefaultRubyVM } from "@ruby/wasm-wasi/dist/browser"
// ② Ruby WASM のランタイムを変数 vm に格納
const { vm } = await DefaultRubyVM(module, {
consolePrint: true,
env: { RUBYOPT: "--disable-did_you_mean" },
})
// ③ jsライブラリをロード
vm.eval('require "js"')
// ④ Ruby側で定義されている HostBridge 特異クラスを変数 HostBridge に格納
const HostBridge = vm.eval("HostBridge")
// ⑤ Ruby側の HostBridge 特異クラスの attr_accessor に TypeScript側の関数を登録
HostBridge.call("ts_call_binding=", vm.wrap(host.tsCallBinding))
HostBridge.call("ts_run_d1_query=", vm.wrap(host.tsRunD1Query))
HostBridge.call("ts_http_fetch=", vm.wrap(host.tsHttpFetch))
...
Ruby側の HostBridge 特異クラスはこうなってます。
require "json"
module HostBridge
class << self
# ⑥ TypeScript側から関数を登録されるアクセサ
attr_accessor :ts_call_binding,
:ts_run_d1_query,
:ts_http_fetch,
:ts_workers_ai_invoke,
:ts_report_ruby_error,
:ts_html_rewriter_transform,
:ts_durable_object_storage_op,
:ts_durable_object_alarm_op,
:ts_durable_object_stub_fetch,
:ts_queue_message_op,
:ts_queue_batch_op
# ⑦ 他のRubyコードから使われるコールメソッド。binding_nameは wrangler.toml で設定しています
def call(binding_name, method_name, *args)
ensure_call_binding_registered!
ts_call_binding.apply(binding_name.to_s, method_name.to_s, args)
end
# ⑧ Promiseを返すTypeScriptの関数を呼んだ場合は await して同期的に値を返す
def call_async(binding_name, method_name, *args)
result = call(binding_name, method_name, *args)
if result.respond_to?(:await)
result.await
else
result
end
end
def run_d1_query(binding_name, sql, bindings, action)
(...)
end
...
この仕組みができたことで、(D1やWorkers AIなど)TS/JS内でしか使えない機能をRuby内で使えるようになって利便性が大きく向上しました。
Ruby WASM のプロジェクトへの組み込み
工夫というほどでもありませんが...
はじめに書いたように、Pythonと異なり Ruby は Cloudflare Workers では公式サポートがありません。したがってプロジェクトに Ruby WASM を含める必要があります。
ありがたいことに、Ruby WASM は npmのライブラリとして提供して頂いているので、これを使わせてもらいました。
...
"dependencies": {
"@ruby/3.4-wasm-wasi": "2.7.2",
"@ruby/wasm-wasi": "2.7.2"
}
上記のように Hibana の packages/runtime/package.json に書いておくことで、 npm install した際に Ruby WASM が降ってきます。
AIエージェントを使った開発フロー
AIエージェントは主に Codex CLI を使っています。
あくまで個人の感覚ですが、Claude Code に比べてリファクタリングやバグ修正の能力が高いように感じます。
応答が遅いときもありますが、月額20ドルで使えるという点も個人の趣味で使うには良いですね。今は Claude Max を解約して 月額20ドルのChatGPTを契約して Codex CLI を使ってます。
Codexとのやり取りでは、
- まず最初にインターフェイス(例:
db.prepare("SELE ...みたいな構文でSQL文を扱いたい みたいな)を話し合って - だいたい結論が見えたらインターフェイスの仕様をまとめてもらって (内容をチェック)
- それをもとに設計案を作ってもらって (内容をチェック)
- それをもとに実装開始!
- 動作検証
- コードをレビューして問題なさそうならコミットプッシュしてPR作ってもらう
-
Codexとgithubを連携設定してるので、githubのPR生成後、自動的にCodexのレビューが走り出す
- レビューが走り出すと自動的に👀みたいなスタンプがつく。問題無ければ👍に変わる。
- 指摘があったら修正して再度レビュー依頼( プルリクエストのコメントで
@codex reviewてコメントするとレビューが開始される) - Codexが「問題ないよ〜」みたいなこと言ってきたらmainブランチにマージ
という感じで1機能(もしくは1機能を複数に分割したうちの1つ)追加して、その繰り返しでやってます。
実際のレビューの様子は薄いORM #3とかに残ってます。
今後の課題
Ruby WASM のサイズ
Cloudflare Workers でアップロードできる1アプリあたりのサイズには制限があって、ツールによる圧縮後のサイズが無料ユーザーは3MB、有料ユーザー($5/月)は10MBです(公式ドキュメント)。
Ruby WASM を含むHibanaのプロジェクトは初期状態ですでに8.6MB程度あり、これにより
- 無料ユーザーはアプリをデプロイできない(ローカルで試すのは可能)
- 有料ユーザーは10MBまで使えますが、RubyWASMを含むHibanaで8.6MB使ってるので、プロジェクトで使用できるコードのサイズの上限が1.4MB程度に制限される
- サイズが大きいことによる起動速度の遅延
という課題があります。
ただ、それでも Cloudflare Worker でRubyが動いたのは感動的で、RubyおよびRuby WASM のコミッタの皆さんには感謝しかありません。
現状ではこれらの制限でCloudflare Workers 無料ユーザーはRubyWASMアプリ をデプロイする事はできません 。
$5/月というかなり良心的な金額設定なので有料登録して使うのもアリですが、できたら Python みたいにネイティブでRuby WASMをサポートして頂けると、劇的にプロジェクトサイズが小さくなって無料ユーザーでも Ruby on Cloudflare が試せるようになるので Cloudflare様なにとぞ...🙏
これから
Ruby WASM のサイズ縮小の努力をするかも
標準のRuby WASMは標準ライブラリなど多くのものを含んだ状態になっていますが、今回の用途であれば必要ないライブラリが沢山あります。
不要なライブラリを外してビルドすることで少しは軽くなるかもしれませんので、取り組んでみようかどうか考え中です。
でもPythonみたいにCloudflare側でRuby WASM をサポートしてくれるのが一番嬉しんですが、難しいかな?
実際に Hibana を使ってそれなりのアプリを作ってみたい
そもそもの目的はフレームワークを作ることじゃなくて、Cloudflare Workers で Rubyのアプリを書いて動かすことでした。なので、本来の目的であるRubyアプリを書いて動かしたいなと思っています。そうするうちにHibana側にも足りないものとかバグとか色々出てくると思うので、そしたら修正や機能追加をしていこうかなと思います。
あと、自分はもともとIoTのサーバーサイド大好き人間なんで、Cloudflare Workers を使ったセキュアで地球規模にスケールするIoTサーバーをRubyで書くとかロマンがあっていいなあと思ってます。
実はCloudflareはMQTTもサポートしてますので、これは是非 Hibana でもサポートして遊びたい。
記事:プログラマブルなMQTTベースのメッセージング

IoTのバックエンドに DurableObject を使って、現実のモノの状態をリアルタイムに反映するデジタルツインを実現して可視化や制御までやっちゃうのも、本来のWorkersの使い方とは多分違いますけど楽しそうですよねえ。
以上、俺得フレームワークを作った話でした。



