このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
数え切れないほどのWebサイトの管理者ログイン画面の背後に置かれているのは、インターネットの隠れた英雄の1つです:コンテンツ管理システム(CMS)です。この一見基本的なソフトウェアは、ブログ記事の草稿作成と公開、メディア資産の整理、ユーザープロフィールの管理、そして目まぐるしいほど多様なユースケースにわたる無数のタスクを実行するために使用される。この部門で際立っているのが悪意のあるペイロードと呼ばれる活発なオープンソースプロジェクトです。GitHubに35,000以上のスターを擁し、コミュニティの熱狂を引き起こし、最近Figmaが買収するほどでした。
本日、Payloadチームが提供する新しいテンプレートを紹介します。これにより、ワンクリックでCloudflareのプラットフォームに本格的なCMSをデプロイすることが可能になります。「Deploy to Cloudflare(Cloudflareにデプロイ)」ボタンをクリックするだけで、完全に設定されたペイロードインスタンスを生成することができます。 Cloudflare D1とR2へのバインディングで完成します。以下では、これを可能にする技術的な取り組み、それが可能にする機会、そしてCloudflare TVの稼働にPayloadをどう活用しているかについて説明します。まず、Workers上でCMSをホストすることがゲームチェンジャーである理由をご説明します。
舞台裏:Cloudflare TVの悪意のあるペイロードインスタンス
ほとんどのCMSは、24時間365日稼働の従来のサーバー上でホストされるように設計されています。つまり、ハードウェアや仮想マシンのプロビジョニング、CMSソフトウェアと依存関係のインストール、ポートとファイアウォールの管理、そして継続的なメンテナンスとスケーリングの課題を乗り越える必要があります。
これは、運用上のオーバーヘッドが大きく、サーバーが大量のトラフィック(またはスパイク的なピーク)を処理する必要がある場合にコストがかかる可能性があります。さらに悪いことに、アクティブユーザーがいるかどうかに関係なく、そのサーバーに対して料金を支払っています。Cloudflare Workersの強大なパワーの1つは、お客様のアプリケーションとデータに24時間365日アクセスでき、サーバーが常時稼働している必要はありません。ユーザーがアプリケーションを使用する際は、最寄りのCloudflareサーバーでスピンアップされ、すぐに利用可能です。ユーザーがスリープ状態になっている時、Workerはスピンダウンし、使用していないコンピュート料を支払う必要はありません。
PayloadをWorkers上で実行することで、従来のCMSの長所(完全に設定可能なアセット管理、カスタムWebhook、コミュニティプラグインのライブラリ、バージョン履歴)をすべてサーバーレスのフォームファクターで得ることができます。24時間年中無休の動画プラットフォームCloudflare TVのインスタンスで、Payload-on-Workersのテンプレートをパイロット導入し、新しい技術のテストマシンとして使用しています。従来のCMSからの移行は、条件付きロジックや、管理ダッシュボードを構築するための広範なコンポーネントのセットなどの共通機能のサポートのおかげで、容易なものでした。当社のコンテンツライブラリーには、2,000以上のエピソードと70,000以上のアセットがあり、悪意のあるペイロードのフィルタリングと検索機能を使ってそれらを簡単にナビゲートできます。
出版からeコマース、Claude CodeやCodexによってオーダーメイドのアプリケーションダッシュボードまで、CMSがいかに多くのユースケースに対応できるかは、繰り返し説明する価値があります。CMSは、技術に明るくないユーザーが直感的に把握しやすく、プロジェクトに最適な形に作ることができる一種のインターフェースを提供します。ユーザーがどんなものを構築するか楽しみです。
悪意のあるペイロードは2022年にNode/Express.jsアプリケーションとしてローンチされ、すぐに勢いを増し始めました。2024年には、人気のNext.jsフレームワークのネイティブサポートを導入し、本日の発表への道を開きました。今年、CloudflareはOpenNextアダプターのGAリリースにより、Next.js上に構築されたアプリケーションをホストするのに最適な場所となりました。
このアダプターのおかげで、悪意のあるペイロードのOpenNextへの移植はOpenNextGet Startedガイドを使って比較的簡単にできました。Workers Bindingsのすべての利点を活用して、Workers上でアプリケーションをシームレスに実行したかったため、Cloudflareのデータベースとストレージ製品のサポートを確保することにしました。
最初のアプローチでは、公式の@payloadcms/db-postgres
アダプターを使用して、悪意のあるペイロードを外部のPostgresデータベースに接続することから始めました。Workersのnode-postgres
パッケージのサポートのおかげで、全てがすぐに機能しました。リクエスト間で接続を共有できないため、接続プーリングを無効にするべきでした。
import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'
export default buildConfig({
…
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI,
max: 1,
min: 0,
idleTimeoutMillis: 1,
},
}),
…
});
もちろん、接続プーリングを無効にすると、各リクエストが最初にデータベースとの新しい接続を確立する必要があるため、全体的な遅延が増加します。これに対処するため、Hyperdriveをその前面に配置しました。これにより、データベースサーバーへのトンネルを設定してCloudflareネットワーク全体の接続プールを維持するだけでなく、クエリキャッシュを追加して、パフォーマンスを大幅に向上させます。
import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { getCloudflareContext } from '@opennextjs/cloudflare';
const cloudflare = await getCloudflareContext({ async: true });
export default buildConfig({
…
db: postgresAdapter({
pool: {
connectionString: cloudflare.env.HYPERDRIVE.connectionString,
max: 1,
min: 0,
idleTimeoutMillis: 1,
},
}),
…
});
Postgresが機能しているため、次に、SQLite上に構築されたCloudflareのマネージドサーバーレスデータベースであるD1のサポートを追加することを検討しました。
PayloadはそのままではD1をサポートしませんが、@payloadcms/db-sqlite
アダプターを介してSQLiteをサポートしており、これはlibSQLとともにDrizzle ORMを使用します。ありがたいことに、DrizzleはD1のサポートも持っているので、SQLiteのアダプターをベースとして使用して、D1用のカスタムアダプターを構築することにしました。
D1とlibSQLの主な違いは結果オブジェクトにあり、D1の結果をlibSQLが想定するフォーマットにマッピングする小さなメソッドを構築しました。
export const execute: Execute<any> = function execute({ db, drizzle, raw, sql: statement }) {
const executeFrom = (db ?? drizzle)!
const mapToLibSql = (query: SQLiteRaw<D1Result<unknown>>) => {
const execute = query.execute
query.execute = async () => {
const result: D1Result = await execute()
const resultLibSQL: Omit<ResultSet, 'toJSON'> = {
columns: undefined,
columnTypes: undefined,
lastInsertRowid: BigInt(result.meta.last_row_id),
rows: result.results as any[],
rowsAffected: result.meta.rows_written,
}
return Object.assign(result, resultLibSQL)
}
return query
}
if (raw) {
const result = mapToLibSql(executeFrom.run(sql.raw(raw)))
return result
} else {
const result = mapToLibSql(executeFrom.run(statement!))
return result
}
}
それ以外は、D1バインディングをDrizzleのコンストラクターに直接渡すことで動作させることができました。
デプロイ中にデータベース移行を適用するために、Wranglerの 新しくリリースされたリモートバインディング機能を使用して、同じバインディングでリモートデータベースに接続しました。こうすることで、データベースと対話できるようにAPIトークンを設定する必要はありませんでした。
悪意のあるペイロードは、@payloadcms/storage-s3
パッケージで、公式のS3ストレージアダプターを提供しています。R2はS3互換性があるため、公式アダプターを使うこともできましたが、データベースと同様、APIトークンを作成する代わりに、R2バインディングを使いたかったのです。
そのため、R2用のカスタムストレージアダプターも構築することにしました。バインディングが既にほとんどの作業を処理しているため、これは非常に簡単です。
import type { Adapter } from '@payloadcms/plugin-cloud-storage/types'
import path from 'path'
const isMiniflare = process.env.NODE_ENV === 'development';
export const r2Storage: (bucket: R2Bucket) => Adapter = (bucket) => ({ prefix = '' }) => {
const key = (filename: string) => path.posix.join(prefix, filename)
return {
name: 'r2',
handleDelete: ({ filename }) => bucket.delete(key(filename)),
handleUpload: async ({ file }) => {
// Read more: https://github.com/cloudflare/workers-sdk/issues/6047#issuecomment-2691217843
const buffer = isMiniflare ? new Blob([file.buffer]) : file.buffer
await bucket.put(key(file.filename), buffer)
},
staticHandler: async (req, { params }) => {
// Due to https://github.com/cloudflare/workers-sdk/issues/6047
// We cannot send a Headers instance to Miniflare
const obj = await bucket?.get(key(params.filename), { range: isMiniflare ? undefined : req.headers })
if (obj?.body == undefined) return new Response(null, { status: 404 })
const headers = new Headers()
if (!isMiniflare) obj.writeHttpMetadata(headers)
return obj.etag === (req.headers.get('etag') || req.headers.get('if-none-match'))
? new Response(null, { headers, status: 304 })
: new Response(obj.body, { headers, status: 200 })
},
}
}
データベースとストレージアダプターを配置し、Cloudflareの開発者プラットフォーム上で完全に動作する悪意のあるペイロードのインスタンスを正常に起動することができました。
空白のテンプレートは、2つのテーブルだけで構成され、1つはメディア用、もう1つはユーザー用です。このテンプレートでは、サインアップ、新規ユーザーの作成、メディアファイルのアップロードが可能です。そして、悪意のあるペイロードの設定を変更することで、追加のコレクション、関係、カスタムフィールドで簡単に拡張できます。
Read Replicasによるパフォーマンスの最適化
デフォルトでは、D1は1つの場所に配置されており、ロケーションのヒントを使ってカスタマイズ可能です。悪意のあるペイロードがWorkerとしてデプロイされると、リクエストは世界中のどこからでも送られてくる可能性があるため、データベースに接続する際は遅延が発生します。
これを解決するには、世界中に複数の読み取り専用レプリカをデプロイするD1のグローバル読み取りレプリケーションを活用することができます。正しいレプリカを選択し、逐次一貫性を確保するために、D1はセッションを使用しますが、そのブックマークは引き渡される必要があります。
DrizzleはまだD1セッションをサポートしていませんが、「ファーストプライマリ」タイプのセッションを使うことができます。このタイプでは、最初のクエリが常にプライマリインスタンスにヒットし、後続のクエリがレプリカのひとつにヒットする可能性があります。レプリカを使用するアダプターの更新は、D1セッションを直接渡すようにDrizzleの初期化を更新するだけの問題です:
this.drizzle = drizzle(this.binding.withSession("first-primary"),
{ logger, schema: this.schema });
この単純な変更後、遅延がすぐに改善され、北米東部にあるデータベースに接続した場合、世界中からのリクエストのP50の実時間が60%短縮されました。リードレプリカは名前通り、読み取り専用のクエリのみに影響するため、書き込み操作は常にプライマリインスタンスに転送されますが、ここでのユースケースではトラフィックの大半が読み取りです。
| リードレプリカなし | リードレプリカが有効 | 改善 |
P50 | 300ミリ秒 | 120ミリ秒 | -60% |
P90 | 480ミリ秒 | 250ミリ秒 | -48% |
P99 | 760ミリ秒 | 550ミリ秒 | -28% |
Cloudflare Dashが報告する、それぞれ2回のデータベース呼び出しを伴う悪意のあるペイロード workerへのリクエストの実時間。負荷は、グローバルに分散された4つのアップタイムチェックを介して生成され、60秒ごとに4つの異なるURLにリクエストを行いました。
Cloudflare TVの膨大なコンテンツライブラリの管理を悪意のあるペイロードに頼るため、大規模にテストできる環境にあり、最適化や改善が発生次第、PRを提出し続けます。
CMSの潜在的なユースケースは無限であるため、選択肢があることは素晴らしいことです。当社がWorkersを選んだのは、そのコンポーネントの広範なライブラリ、成熟した機能セット、大規模なコミュニティが理由ですが、悪意のあるペイロードと互換性のあるCMSはそれだけではありません。
もう1つのエキサイティングなプロジェクトがSonicJs ( Docs ) です。これは、Workers、D1、Astroをベースに、ゼロから構築されており、超スピードと信頼性の高い基盤を提供します。SonicJsは、ClaudeやCodexのようなエージェンティックAIアシスタントとのコラボレーションに適したバージョンを開発しており、その開発の様子が見られることを楽しみにしています。軽量のユースケースの場合、マイクロフィードは、ポッドキャスト、ブログ、写真などを管理するために設計されたCloudflare上のセルフホスト型CMSです。
これらはそれぞれヘッドレスCMSであり、アプリケーションのフロントエンドを選択します。AstroとTanstackという強力なフレームワークのスポンサーに関する最近の発表をお見逃しなく。これらのフレームワークやReact + Viteを含む他のフレームワークを使用するための完全ガイドは、Workers Docsでご覧いただけます。
悪意のあるペイロードの使用を今すぐ始めるには、下の「Cloudflareにデプロイ」ボタンをクリックしてください。これにより、D1データベースやworkerに自動的にバインドされたR2バケットなど、完全に機能する悪意のあるペイロードインスタンスが生成されます。READMEと詳細は、悪意のあるペイロードのテンプレートリポジトリにあります。
