Cloudflare Workers へのデプロイ
Guren は @guren/plugin-cloudflare を通じて Cloudflare Workers 上で動作し、データベースには D1 を使います。このガイドでは、まっさらなアカウントからデプロイ完了までを一通り扱います。
Cloudflare Workers へのデプロイ
Guren は @guren/plugin-cloudflare を通じて Cloudflare Workers 上で動作し、データベースには D1 を使います。このガイドでは、まっさらなアカウントからデプロイ完了までを一通り扱います。
Workers は常駐サーバーとは形の違うランタイムです。ファイルシステムがなく、リクエスト間でメモリを共有せず、CPU 時間はミリ秒単位で区切られています。このガイドの大部分は、その違いがアプリの設定のどこに現れるかの説明です。
インストール
bunx guren plugin @guren/plugin-cloudflare
bun add @guren/plugin-cloudflare
プラグインは cloudflare:build コマンドを登録し、初回ビルド時に wrangler.jsonc を生成します。
ビルドとデプロイ
bunx guren cloudflare:build
bunx wrangler deploy
cloudflare:build はアプリの build スクリプトを実行したあと、ワーカーのエントリポイント・静的アセット・平坦化したマイグレーションを含む .cloudflare/ ディレクトリを組み立てます。古いワーカーをデプロイしてしまわないよう、両方を 1 つのスクリプトにまとめておきましょう。
{
"scripts": {
"cloudflare:build": "bun run build && bunx guren cloudflare:build --skip-app-build",
"deploy:cloudflare": "bun run cloudflare:build && bunx wrangler deploy"
}
}
rule
.cloudflare/ は生成物です。.gitignore に追加し、デプロイのたびに作り直してください。他のどこからも参照されないため、中身が古くなっていても気づかないまま古いコードをデプロイすることになります。
アプリのビルドに先立って、guren doctor と同じデプロイランタイムチェックが走ります。セッションや OAuth state がプロセスメモリに置かれている、Bun でしか読めないパスワードハッシャーを選んでいる、プロバイダをファイルシステムから探索している、のいずれかに当たると警告を出します(ビルド自体は止まりません)。どれもローカルでは動き、Workers では壊れるものです。警告は Vite の出力の後ろではなく、まだ目を通している位置に出ます。
データベース(D1)
データベースを作成し、その ID を wrangler.jsonc に記録します。
bunx wrangler d1 create my-app
{
"d1_databases": [
{
"binding": "DB",
"database_name": "my-app",
"database_id": "<wrangler が出力した ID>",
"migrations_dir": ".cloudflare/d1-migrations"
}
]
}
ドライバはランタイムによって config/database.ts で切り替えます。D1 は SQLite 互換なので、スキーマは SQLite のダイアレクトで書き、開発時はローカルの SQLite ファイルを使います。
// config/database.ts
import { createD1Database, createSqliteDatabase, defineDatabaseConfig } from '@guren/core'
import { getWorkersEnv, isWorkersRuntime } from '@guren/plugin-cloudflare/env'
import env from './env.js'
interface WorkersEnv {
DB: unknown
}
const database = isWorkersRuntime()
? createD1Database({
binding: () => getWorkersEnv<WorkersEnv>().DB,
migrationsFolder: new URL('../db/migrations', import.meta.url),
})
: createSqliteDatabase({
migrationsFolder: new URL('../db/migrations', import.meta.url),
seedersFolder: new URL('../db/seeders', import.meta.url),
// `context` はアプリの検証済み環境変数。`guren db:*` はアプリの外で動くので、スキーマを自分で解析する
filename: (context) => (context?.env ?? env.parse(undefined, { mode: 'report' }).values).SQLITE_DATABASE_PATH,
})
export const { getDatabase, migrateDatabase, closeDatabase, configureOrm, seedDatabase } = database
export default defineDatabaseConfig(database, { seedOnBoot: process.env.NODE_ENV !== 'production' })
config/env.ts に SQLITE_DATABASE_PATH: Env.string().default('./data/guren.db') を宣言し、database を createApp({ config }) に加えてください。リゾルバの詳細は 設定ガイド にあります。getWorkersEnv と isWorkersRuntime は @guren/plugin-cloudflare/env から import します。パッケージのルートにはビルド用のツールも含まれていて、ワーカーのバンドルには不要だからです。
バインディングは値ではなく、解決用の関数として渡します。バインディングが存在するのはリクエストが届いてからなので、読み取りを遅らせる必要があります。
マイグレーションの適用
マイグレーションはアプリの外側で適用します。Workers 上でアプリが自分でマイグレートすることはありません。
bunx guren cloudflare:build # .cloudflare/d1-migrations を再生成
bunx wrangler d1 migrations apply my-app --remote
rule
先にビルドしてください。migrations_dir は生成ディレクトリの中を指しているので、ビルド前は空です。空のフォルダを見つけた wrangler は「適用するマイグレーションはありません」とエラーではなく正常終了で報告するため、ビルド前に適用すると失敗が成功のように見えます。
ワーカーが自分でシードを実行することもありません。defineDatabaseConfig がシードするのは、seedOnBoot が true で、かつデータベースがマイグレーションありと報告したときだけです。cloudflare:build は NODE_ENV を production に固定しますし、D1 のハンドルは探すファイルシステムを持たないので、マイグレーションなしと報告します。ローカルの bun run dev は SQLite 側を使い、マイグレーションがあればブート時にシードします。
セッションと OAuth state はデータベースに保存する
これは好みの問題ではありません。リクエストごとに別の isolate へ届くことがあり、isolate 同士はデータベース以外に何も共有しないためです。メモリ実装のままでもローカルでは動いているように見えますが、本番ではセッションが毎回消えます。
// config/session.ts
import { defineSessionConfig } from '@guren/core'
import { sessions } from '../db/schema.js'
export default defineSessionConfig((env) => ({
default: env.SESSION_DRIVER,
stores: {
database: { driver: 'database', table: sessions },
},
}))
config/env.ts には SESSION_DRIVER: Env.string().default('database') を宣言します。デプロイ先で何も設定しなくても、データベースのストアが選ばれます。
OAuth も同様です。認可へのリダイレクトと、そこから戻ってくるコールバックは別の isolate に届くのが普通なので、両者を結びつける state は共有できる場所に置く必要があります。
// config/oauth.ts
import { DatabaseOAuthStateStore, defineOAuthConfig } from '@guren/core'
import { oauthStates } from '../db/schema.js'
export default defineOAuthConfig(() => ({
// `providers` は OAuth ガイドのとおり
stateStore: new DatabaseOAuthStateStore(oauthStates),
}))
2つの定義は、それを使うセッションミドルウェアの設定と一緒に createApp() に並べます:
// src/app.ts
import { createApp } from '@guren/core'
import database from '../config/database.js'
import env from '../config/env.js'
import oauth from '../config/oauth.js'
import session from '../config/session.js'
import { registerWebRoutes } from '../routes/web.js'
const app = createApp({
env,
config: [database, session, oauth],
auth: {
autoSession: true,
sessionOptions: { cookieSecure: true },
},
routes: registerWebRoutes,
})
セッションと OAuth state のストア をサービスプロバイダで設定しているアプリもそのまま動きます。サービスプロバイダを使うアプリ を参照してください。
どちらのストアもテーブルを必要とします。sessions は bunx guren add session が生成し、oauth_states のカラムは Stateストレージ に載っています。bunx guren make:auth --oauth は両方のテーブルを生成し、両方のストアを配線します。
ストレージ(R2)
Workers にはファイルシステムが無いため、local ストレージドライバは動きません。代わりに R2Driver を使うと、バケットバインディング経由の Cloudflare R2 バケットを StorageManager の同じ API の下で扱えます。用意する資格情報は無く、AWS SDK もバンドルに入りません。
バケットを作ってバインドします:
bunx wrangler r2 bucket create my-app-media
// wrangler.jsonc
"r2_buckets": [
{ "binding": "MEDIA", "bucket_name": "my-app-media" }
]
次に、config/storage.ts で media をデフォルトのディスクにします:
// config/storage.ts
import { defineStorageConfig } from '@guren/core'
export default defineStorageConfig(() => ({ default: 'media' }))
ストレージの設定で指定できるドライバは組み込みの local・s3・memory だけです。ディスク自体は、プロバイダの register() からバインド済みのマネージャに登録します。Workers 上では R2、それ以外ではローカルファイルシステムを使います。config/database.ts が D1 に使っているのと同じランタイム判定です:
// app/Providers/MediaDiskProvider.ts
import { ServiceProvider, LocalStorageDriver } from '@guren/core'
import { R2Driver } from '@guren/plugin-cloudflare'
import { getWorkersEnv, isWorkersRuntime } from '@guren/plugin-cloudflare/env'
interface Env {
MEDIA: unknown
}
export default class MediaDiskProvider extends ServiceProvider {
register(): void {
this.container.make('storage').registerDisk('media', () =>
isWorkersRuntime()
? new R2Driver({
binding: () => getWorkersEnv<Env>().MEDIA,
publicUrl: 'https://media.example.com',
})
: new LocalStorageDriver({ root: './storage/app/public', url: '/storage' }),
)
}
}
storage を createApp({ config }) に、MediaDiskProvider を providers に加えてください。
binding は値ではなくリゾルバです。バインディングは最初のリクエストと共に届くので、D1 バインディングと同じく読み取りを遅らせる必要があります。あとはストレージガイドの storage.disk('media').put(...) / get(...) / files(...) がそのまま動きます。bun run dev はディスクに、wrangler dev と本番は R2 に書き込みます。
R2 は S3 とは違うため、次の 3 点は挙動が異なります:
url()にはpublicUrlが必須です。 ダッシュボードでバケットにカスタムドメインを割り当て(r2.dev サブドメインはレート制限付きで開発向けです)、それをpublicUrlに渡してください。R2 には導出できる公開 URL が無いため、未設定だとurl()は例外を投げます。temporaryUrl()には S3 資格情報が必要です。 バインディングは URL に署名できません。presign: { accountId, bucket, accessKeyId, secretAccessKey }(R2 API トークン)を渡すか、非公開ファイルはアプリの認証付きルート越しに配信してください。presignが無い場合、temporaryUrl()はその案内付きで例外を投げます。- 可視性はオブジェクト単位ではなくバケット単位です。 バケットは公開(カスタムドメイン / r2.dev)か非公開かのどちらかで、オブジェクトごとの ACL はありません。ドライバはバケットの
visibility(publicUrlがあれば既定'public')を報告し、put({ visibility })やsetVisibility()で逆の値を求められた場合は例外を投げ、対応できたように見せかけることはしません。
putFile() も例外を投げます。Workers には読み取れるローカルファイルがありません。自分でバイト列を読み(await file.arrayBuffer())、put() を呼んでください。一度きりの一括投入なら、手元から bunx wrangler r2 object put my-app-media/<key> --file <path> の方が簡単です。
Bun プロセス(スクリプトや Workers 以外のデプロイ)から同じバケットに到達するには、代わりに S3 互換エンドポイントと S3 ドライバを使います。endpoint: 'https://<ACCOUNT_ID>.r2.cloudflarestorage.com'、region: 'auto'、R2 API トークンの組み合わせで、ストレージガイドを参照してください。
Workers でのアタッチメント
アタッチメントレイヤーは Workers でも動きます。分担が 1 つだけ変わります: Workers には画像デコーダがないため、画像処理はリクエストパスではなくキューワーカーで行います。
- 同期ゲートは Worker 内でそのまま走ります。 バイト数超過(413)、
maxPixelsを超えるヘッダ寸法(422)、HEIC シグネチャ(415)、image: 'require'コレクションへの非画像(422)は、いずれもリクエスト内で拒否されます。純粋な JavaScript で、デコーダは不要です。 queued: trueでアタッチします。 Worker はオリジナルを保存し、宣言済みバリアントをpendingとして記録し、Redis ベースのキュー(RedisQueueDriver。キューガイドが Workers に求めるストアと同じ)へGenerateVariantsJobをディスパッチします。ジョブが完了するまで、バリアント URL はオリジナルへフォールバックします。- ワーカーは Bun で動かします。 別プロセスの Bun(
Bun.Imageあり)がキューを処理します: 先送りしたフルデコード、オプトイン済みコレクションの HEIC 変換、バリアント生成を行います。configureAttachments()がジョブを登録するため、アプリの config を起動するワーカーなら追加配線なしで処理できます。 - private アタッチメントは署名配信ルートによりバインディングだけで動きます。
configureAttachments()でディスクを private と宣言し、ルートを有効にしてください:disks: { media: 'private' }, delivery: {}(未宣言のディスクは public 扱いで、publicUrl付きのバケットもデフォルトで public です)。加えてルート登録関数でregisterAttachmentRoutes(router)をマウントすると、private アタッチメントはパス相対の署名付き URL になり、ルートがget().bodyを Worker 経由でストリーム配信します。presign資格情報は不要です。presignを設定した場合はドライバがcapabilities.presignedGetを宣言し、同じルートが短寿命の presigned URL への 302 リダイレクトに昇格します。R2 固有の注意: R2 の S3 API はresponse-content-*ヘッダオーバーライドを無視するため、リダイレクトはオブジェクトの保存済みメタデータで配信されます。presign 対応の R2 ディスクでContent-Disposition: attachmentを強制したいアプリはserve: 'proxy'を使ってください。public のアタッチメントは従来どおりバケットのカスタムドメインから配信され、Worker の CPU を使いません。
同期ゲートが捕まえられないのは、ヘッダで嘘をつくバイト列だけです。これは受理後にワーカーが検出します: image: 'require' コレクションではジョブがアタッチメントをパージし、それ以外では不透明ファイルとして残ります。
シークレット
APP_KEY は必須です。セッションと CSRF の署名に使われ、これが無いとワーカーは起動時に例外を投げて、リクエストを 1 件も処理しないまま落ちます。
bun -e "console.log('base64:'+Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64'))" | bunx wrangler secret put APP_KEY
アプリが読む他の値(OAuth の認証情報、API キーなど)も同じ方法で設定し、それぞれ config/env.ts で宣言します。vars とシークレットはワーカーのエントリポイントの env に届き、process.env に届くとは限りません。@guren/plugin-cloudflare がアプリのブート前にこの env をバインドし、スキーマはそこから値を読むので、アプリのコードはローカルと同じく検証済みの値を読めます(設定)。wrangler.jsonc の vars に書いてよいのは、秘密でない値だけです。
無料プランの制限
アプリの設計に影響する制限が 2 つあります。
| 制限 | 値 | 意味 |
|---|---|---|
| ワーカーサイズ | 非圧縮 64 MiB(全プラン共通) | 生成コンテンツはバンドルではなく Static Assets、KV、R2 に置く |
| リクエストあたり CPU | 10 ミリ秒 | 重い処理はビルド時か保存時に寄せる |
Cloudflare は 2026-09-04 に圧縮後サイズの上限(無料 3 MB、有料 10 MB)を撤廃し、現在は非圧縮のバンドルサイズだけを検査します。自分のアプリのサイズは bunx guren cloudflare:size(または cloudflare:build --report-size)で測れます。wrangler のドライランを実行し、パッケージ単位で大きい順に内訳を表示するので、気づかないうちに膨らんだバンドルの原因がそこに出ます。この数字が示さない制限が 2 つあります。起動時間(1 秒、wrangler check startup)とメモリ(isolate あたり 128 MB)で、どちらも大きなデータをバンドルすると消費します。
この CPU 制限により、パスワードハッシュは事実上不可能です。意図的に遅くしてある処理を 10 ミリ秒に収めることはできません。無料プランでは、パスワードではなく OAuth による認証を選んでください。フローは認証ガイドにあります。
同じ制約は「リクエスト時の仕事を減らす」設計を後押しします。Markdown の変換は読み取り時ではなく保存時に行い、静的なコンテンツはビルド時にレンダリングしておきます。それでも処理時間がどうしても足りない場合は、有料プランで CPU 制限が緩和されます。
可観測性
Workers は既定ではログを保持しません。そのままでは本番で問題が起きたときの追跡が非常に難しくなります。
{
"observability": {
"enabled": true
}
}
再現しながらリアルタイムで見たい場合は bunx wrangler tail と併用してください。
ローカル開発
wrangler dev はローカルの D1 を相手に実際のランタイムを動かします。Bun の開発サーバーでは検出できないランタイム差異を拾えるため、デプロイ前に一度通しておく価値があります。
bunx wrangler d1 migrations apply my-app --local
bunx wrangler dev
ローカル用のシークレットは .dev.vars に置きます(.gitignore への追加も忘れずに)。
APP_KEY=base64:...
日々の開発では通常の bun run dev の方が速く回せます。wrangler dev は、デプロイ直前や、本番でだけ挙動が違うときに使ってください。
静的アセット
public/ 配下はすべて .cloudflare/assets/ にステージングされ、Workers Static Assets が配信します。これはワーカーが動くより前に応答するため、ビルドが次の 2 点を処理します。
- ステージングしたファイルと同じ場所に
_headersを生成し、ブラウザがドキュメントとして描画する形式 (.html、.htm、.svg、.xhtml、.xml) にContent-Disposition: attachmentとX-Content-Type-Options: nosniffを付けます。ローカルでフレームワークがpublic/に適用しているのと同じ方針ですが、ここではワーカーに適用の機会がありません。画像・スクリプト・スタイルシート・フォントは対象外です。public/に独自の_headersを置いている場合はそのまま残り、生成されたルールがその前に挿入されます。プラットフォームが解釈するルールは最大 100 件で、超過分はエラーにならず読み飛ばされるため、合算して 100 件を超える場合はビルドが警告します。また、照合は大文字と小文字を区別します。グロブでは回避できません (1 ルールにつきスプラットは 1 個まで)。そのためLogo.SVGのようなファイルには個別のルールを生成します。拡張子を小文字で揃えているアプリでは追加のルールは出ません。 assetsバインディングに"html_handling": "none"を設定し、ステージングされたpage.htmlを/page.htmlだけで配信します。プラットフォーム既定では/pageでも応答するため、同名のルートを覆い隠すうえ、_headersのルールが照合するパスからファイルが外れてしまいます。ステージング済みファイルが応答しないリクエストはワーカーへ流れ、そこがページの出どころになります。
public/ から拡張子なし URL で HTML を配信したい場合は、wrangler.jsonc で "html_handling" を自分で指定してください。値が書かれていればビルドはそれを尊重します。その分 .html のルールは弱くなります。
翻訳
Workers には lang/ を読むファイルシステムがないため、ビルドが lang/<locale>/*.json をすべてワーカーに埋め込み、createApp({ i18n }) はそこから翻訳を返します。JSON ファイルはいつもどおり編集し、再ビルドしてください。i18n の path オプションを指定したアプリは対象外です。サーバーレスとバンドルカタログを参照してください。
スケジュールタスク
生成されるワーカーは fetch と並んで scheduled ハンドラを export します。そのため Cloudflare の cron トリガーが、アプリが createScheduler() で登録したタスクを実行します。Workers には常駐プロセスがないため、scheduler.start() は動きません。時間を進める役割はトリガーが担います。
用意するものは 2 つです。1 つめは、Bun サーバーのときとまったく同じ、タスクと scheduler をバインドするプロバイダです。タスクを app/Console/Kernel.ts に宣言しておくと、guren schedule:list と guren schedule:run からも見えるようになります。条件はこのファイルに置くことだけで、書き方はこれらのコマンドが受け付けるどちらでもかまいません(CLIから見えるようにする)。
// app/Console/Kernel.ts
import { Schedule, defaultContainer, type SessionManager } from '@guren/core'
export function scheduleTasksKernel(): Schedule {
const schedule = new Schedule()
// 解決はカーネルの構築時ではなくタスクの実行時です。`session` を束縛するのは
// app.boot() で、cron のエントリポイントはそれを先に await します。
schedule
.call(() => defaultContainer().make<SessionManager>('session').pruneExpired())
.hourly()
.name('sessions:prune')
return schedule
}
// app/Providers/SchedulingProvider.ts
import { ServiceProvider, createScheduler } from '@guren/core'
import { scheduleTasksKernel } from '../Console/Kernel.js'
export default class SchedulingProvider extends ServiceProvider {
register(): void {
this.container.singleton('scheduler', () => {
// logger を渡さないと、例外を投げたタスクは捕捉されて捨てられます (後述)。
const scheduler = createScheduler({ logger: console.log })
for (const task of scheduleTasksKernel().buildTasks()) {
scheduler.addTask(task)
}
return scheduler
})
}
}
2 つめは wrangler.jsonc のトリガーそのものです。ビルドはこれを生成しません: トリガーはタスクの有無にかかわらず起動し、課金されるためです。
{
"triggers": { "crons": ["* * * * *"] }
}
起動のたびに、その分に該当するタスクだけが実行されます。したがってトリガーが最も細かいタスクより粗いと、そのタスクは一度も動きません。["0 * * * *"] のトリガーと .dailyAt('03:30') のタスクは噛み合いません。スケジュールする最小粒度にトリガーを合わせるか、トリガーの境界だけにスケジュールしてください。
Workers 固有の制約は次のとおりです。
schedule.command()は動きません。node:child_process経由でシェルに委ねますが、Workers には実行するサブプロセスがありません。ビルド時ではなく、タスクの実行時に失敗します。schedule.call()またはschedule.job()を使い、処理を直接呼んでください。上の例がsessions:pruneをインプロセスで書いた形です。schedulerバインディングが見つからないトリガーは例外を投げます。 何も掃除していないのに成功として報告することはありません。エラーメッセージが対処法を示し、他のワーカー例外と同じくwrangler tailに出ます。- タスク自身の例外は投げません。
runDueTasks()はタスクごとに捕捉し、scheduler のloggerに報告しますが、その既定値は何もしない関数です。そのため D1 のエラーで sweep が失敗しても成功として報告され、wrangler tailには何も出ません。scheduler の生成時にloggerを渡してください。 preventOverlapping()は起動をまたいで効きません。 タスク上のメモリ内フラグで、起動ごとに新しい isolate になりうるためです。トリガー間隔より遅いタスクは自分自身と重なります。タスクを間隔より短く保つか、isolate より長生きする場所にガードを置いてください。D1 の行か、インスタンスごとの永続 state と alarm ベースのスケジュールを持つ Durable Object です(永続エージェント)。cron トリガーが向いているのは、identity を持たないアプリ全体の sweep です。重複してはいけないジョブは identity を持つので、それを表すには Durable Object を使います。runOnOneServer()には isolate 間で共有するSchedulerLockが必要です。 既定のMemorySchedulerLockは 1 つの isolate の中だけに存在するため、ここでは何も守れません。スケジューラは警告を出したうえで、起動のたびにタスクを実行します。Workers に Redis は無く、KV には条件付き書き込みが無いので、SchedulerLockは一意キーへの D1 の INSERT か Durable Object の上に実装し、createScheduler({ lock })に渡してください。
--mcp-oauth のワーカーはもう 1 つ掃除します。タスクを実行する前に、前回の掃除から 1 時間が経過したかを毎回確認し、経過していれば OAuth プロバイダの purgeExpiredData を呼びます。クライアントが削除されて孤立した grant は expiry を持たないため KV の TTL では消えず、これを取り除くのはこの掃除だけです。1 回の掃除で確認するのは grant が最大 15 件、続く 2 回目の呼び出しで token が最大 15 件です。分けているのは、プロバイダが grant の走査で予算を使い切った時点で戻ってしまい、その後ろの token に一度も到達しないためです。どちらもつねにキー空間の先頭から始まります: プロバイダのカーソルは呼び出しをまたいで残らないため、その範囲より後ろのレコードには到達せず、打ち切られた場合は wrangler tail にその旨が出ます。この件数が小さいのは、Free プランでは 1 起動あたりのサブリクエストが 50 に制限され、KV の読み取りもそこに数えられるためです。しかも同じ予算をアプリ自身のタスクと共有します。ワーカー側からはどちらのプランかを判別できません。掃除が失敗してもログに出るだけで、次の間隔まで待つ扱いになり、アプリのタスクは止まりません。
掃除にも他と同じく triggers.crons のエントリが必要です。掃除のためだけにトリガーを足す場合も scheduler のバインドは必要で、無ければ毎回の起動が掃除のあとで上記の例外を投げます。
永続エージェント
Workers は、アプリケーション自身がホストするエージェントの基盤でもあります。Durable Object が、長命なエージェントに永続的な identity・永続的な state・alarm に支えられたスケジュールを与えます。@guren/plugin-agents はそれらを、ルートがすでに宣言しているエージェントツールの背後に置きます。
アプリに config/agents.ts があると、cloudflare:build は 3 つのことを追加で行います。登録済みクラスごとの名前付き export を生成ワーカーに追記すること、コミット済みの wrangler.jsonc がそれぞれを SQLite バックの Durable Object としてホストしているかを検証すること(不足していれば、バインディングなしでデプロイせず、追記すべき JSON をそのまま出力して失敗します)、そして /agents/* をレジストリ自身の authorizer の背後にすべて拒否でマウントすることです。durable_objects と migrations のエントリは手書きせず、ビルドを実行してその出力を貼り付けてください。
デプロイしたエージェントの無料プランでの実測値、1回の sweep が収まるべき D1 クエリ予算、そしてエージェントの書き方全般は永続エージェントにあります。
既存アプリの更新
wrangler.jsonc は初回のみ生成され、その後は上書きされません。そのため、プラグイン更新前に作られたアプリは元の設定を保ち続けます。古い設定を検出すると、ビルドが不足している項目を具体的に出力するので、それを追記して再ビルドしてください。
唯一出力しないのが triggers.crons です。アプリがスケジュールタスクを登録しているかどうかは、スケジュールカーネルを見つけて読み込めたビルドにしか分かりません。別の書き方で宣言されたタスクは「無い」と読めてしまうため、ビルドは推測せずに黙ります。タスクを持っていて Workers でも動かしたい場合は、トリガーを手で追記してください(スケジュールタスク)。
デプロイ後
- Cloudflare のダッシュボードでワーカーに独自ドメインを割り当て、OAuth のコールバック URL も併せて更新します。
- 監視とインシデント対応については本番運用ランブックを参照してください。