このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
ソーシャルメディアユーザーは、プラットフォームがシャットダウンしたり、ピボットしたりするたびにIDとデータを失うのにうんざりしています。ATProtoのエコシステム(Authenticated Transfer プロトコルの略)では、ユーザーが自分のデータとIDを所有します。彼らが公開するものはすべて、暗号署名されたグローバルなソーシャルWebの一部となります。Blueskyは最初の大きな例ですが、分散型ソーシャルネットワークの新しい波は始まったばかりです。この記事では、Cloudflareの開発者用プラットフォーム上で、完全なサーバーレスのATProtoアプリケーションを構築してデプロイする方法を紹介します。
なぜサーバーレスなのか?VMの管理、データベースのスケーリング、CIパイプラインの維持、可用性ゾーン全体へのデータの配信、DDoS攻撃に対するAPIの保護などのオーバーヘッドが実際の構築から目をそらさせます。
そこでCloudflareの出番です。当社のDeveloper Platformを活用して、グローバルネットワーク上で実行されるアプリケーションを構築できます。Workersは数ミリ秒でグローバルにコードをデプロイし、KVは高速でグローバルに分散されたキャッシングを提供し、D1は分散型リレーショナルデータベースを提供し、Durable ObjectsはWebSocketsを管理し、リアルタイムを処理します。調整する必要があります。何より素晴らしいのは、サーバーレスATProtoアプリケーションの構築に必要なものはすべて無料枠で利用できるため、一切コストをかけずに始められることです。コードは、このGitHub repoでご覧いただけます。
ATProtoエコシステム:簡単にご紹介します
まず、ATProtoエコシステムにおけるデータの流れの概念的な概要から始めましょう:
ユーザーはアプリを操作し、アプリが個人リポジトリに更新を書き込みます。これらの更新が変更イベントをトリガーし、リレーに公開され、グローバルイベントストリームを通じて配信されます。元のアップデートを公開していないアプリでも、こうしたイベントに購読することができます。ATProtoでは、リポジトリ、リレー、アプリはすべて独立したコンポーネントであり、異なるオペレーターによって実行され得る(そして実行される)からです。
ID
ユーザーIDは、アカウントで始まり、alice.example.com
のような人間が読める名前です。プロトコルがDNSを活用して、誰がどのアカウントを所有しているかというグローバルな可視性を提供できる、各ハンドリングは有効なドメイン名でなければなりません。ユーザーのパーソナルデータサーバー(PDS)の場所を含む、ユーザーの分散型識別子(DID)にマップを処理します。
認証
ユーザーのPDSは、キーとリポジトリを管理します。認証を処理し、リポジトリを通じてデータの権威あるビューを提供します。
さらに詳しく知りたい方は、こちらの分散システムエンジニア向けのATProtoの記事をご覧ください。
ここでの違いは、そして見落とされやすい点は、このスタックのどの部分も単一のサービスの信頼に依存していることはほとんどないことです。DID解決は検証可能。PDSはユーザーが選択できます。クライアントアプリは単なるインターフェイスです。
データを公開したり取得したりする際には、署名と自己検証が行われます。つまり、他のアプリは、許可を求めることなく、また当社のバックエンドを信頼することなく、その上にすべての他のアプリを使用したり、その上に構築したりすることができます。
私たちのアプリケーションは、
ATProtoチームが構築した小さいながらも完全なデモアプリ、Statusphereを使って作業します。これは可能な限りシンプルなソーシャルメディアアプリで、ユーザーは単一絵文字のステータス更新を投稿します。非常に最小限であるため、Statusphereは、分散型ATProtoアプリがどのように機能し、Cloudflareのサーバーレススタック上で動作するように適応させるかについて、最適な出発点となります。
Statussphereスキーマ
ATProtoでは、すべてのリポジトリデータがJSON-Schemaに似た共有スキーマ言語であるLexiconを使用して型付けされます。Statusphereには、ATProtoチームが定義したxyz.statusphere.status
レコードを使用します。
{
"type": "record",
"key": "tid", # timestamp-based id
"record": {
"type": "object",
"required": ["status", "createdAt"],
"properties": {
"status": { "type": "string", "maxGraphemes": 1 },
"createdAt": { "type": "string", "format": "datetime" }
}
}
}
辞書は厳密に型付けされているため、アプリ間の相互運用性が容易です。
構築の仕組み
このセクションでは、認証からリポジトリの読み込みおよび書き込み、リアルタイム更新まで、Statusphere内のデータの流れをたどり、サーバーレスインフラストラクチャ上でライブイベントストリームの処理方法を紹介します。
1. 言語の選択
ATProtoのコアライブラリはTypeScriptで記述されており、Cloudflare WorkersはFirst-classのTypeScriptのサポートを提供します。Cloudflare Workers上にATProtoサービスを構築する際の自然な出発点となります。
ただし、ATProto TypeScriptライブラリは、バックエンドまたはブラウザのコンテキストを想定しています。Cloudflare WorkersはサーバーレスコンテキストでNode.js APIの使用をサポートしていますが、ATProtoライブラリの使用は、「エラー」リダイレクト処理モードの使用は、エッジランタイムと互換性がありません。
Cloudflareは、WASM クロスコンピレーションを介してWorkersのRustもサポートしているので、次にそれを試してみました。ATProto Rust クレートとコード生成ツールは、Rustの型システムを大いに活用し、ツールを構築しますが、まだ活発に開発中です。それでも、RustのWASMエコシステムは堅固であるので、Statusphereの既存のRust実装を適応させることで、すぐに作業可能なプロトタイプを動作させることができました。(もともとBailey Townsendが書いたものです)。コードは、このGitHub repoでご覧いただけます。
Cloudflare Workers上にATProtoアプリを構築しているなら、TypeScriptライブラリに貢献して、サーバーレスランタイムのサポートを強化することをお勧めします。このアプリのTypeScriptバージョンは、素晴らしい次のステップになります。構築にご興味のある方は、Cloudflare Developer Discordサーバー経由でご連絡ください。
2. 追随する
この「Deploy to Cloudflare(Cloudflareにデプロイ)」ボタンを使用して、リポジトリを複製し、独自のKVおよびD1インスタンスとCIパイプラインを設定します。
このリンクの手順に従い、デフォルト値を使用するか、カスタム名を選択すると、独自のStatusphere Workerを構築し、デプロイします。
注:このプロジェクトには、パブリックイベントストリームから読み取られるスケジュールコンポーネントが含まれています。リソースを節約するために、実験が終わったら削除するべきかもしれません。
3. ユーザーのハンドリングを解決する
ユーザーのデータとやりとりするには、まず、_atprotoサブドメインに登録されているレコードを使用して、DIDのハンドリングを解決します。例えば、私の処理はinanna.recursion.wtf
で、そのため、私のDIDレコードは _atproto.inanna.recursion.wtf
に保存されます。そのレコードの値は、d:plc:p2sm7vlwgcbbdjpfy6qajd4g
です。
次に、DIDを対応するDIDドキュメントに解決します。DIDドキュメントには、ユーザーの個人データサーバーの場所を含むIDメタデータが含まれます。DID方法に応じて、この解決はDNS(dd:web識別子の場合)を介して直接処理され、より多くの場合、dod:plc識別子の場合は資格情報の公開台帳を介して処理されます。
これらの値は頻繁に変更されないため、Cloudflare KVを使用してキャッシュします。このような場合には、更新頻度は低いものの、低遅延でグローバルに利用できるようにする必要があるKey-Valueマッピングがある場合に最適です。
DIDドキュメントから、ユーザーの個人データサーバーの場所を抽出します。私の場合は、bsky.social
で、独自のPDSをセルフホストしたり、別のプロバイダーを利用したりすることもあります。
OAuthフローの詳細についてはここでは重要ではありません。私が実装に使用したコードを読むことも、OAuthの仕様を掘り下げることもできます。しかし、簡単に言うと、ユーザーはPDSを介してサインインし、署名鍵を使用して、アプリに代わって行動する許可を与えます。
タワーセッションを使ってセキュアなセッションCookieにセッションデータを保持します。つまり、クライアント側では不透明なセッションIDのみが保存され、すべてのセッション/認証状態データはCloudflare KVに保存されます。このユースケースにも、自然に適合したと言えます。
4. ステータスとプロファイルデータの取得
セッションCookieに保存されたDIDを使用して、ユーザーのOAuthセッションを復元し、認証されたエージェントをスピンアップします。
let agent = state.oauth.restore_session(&did).await?;
エージェントの準備が完了すると、ユーザーの最新のStatusphe投稿とBlueskyプロファイルを取得します。
let current_status = agent.current_status().await?;
let profile = agent.bsky_profile().await?;
従業員のステータスとプロフィール情報が手元にあるので、ホームページをレンダリングできます:
Ok(HomeTemplate {
status_options: &STATUS_OPTIONS,
profile: Some(Profile {
did: did.to_string(),
display_name: Some(username),
}),
my_status: current_status,
})
5. アップデートの公開
ユーザーが新しいe品ステータスを投稿すると、個人リポジトリに新しいレコードを作成します。データの取得に使用したのと同じ認証済みエージェントを使用します。今回は、読み取りの代わりに、レコードの作成操作を実行します。
let uri = agent.create_status(form.status.clone()).await?.uri;
この操作は、新しいレコードの正規の識別子であるURIを返します。
そして、ステータスアップデートをD1に書き、すぐにUIに反映できるようにします。
6. Durable Objectsを使用して更新を送信
アクティブなホームページは全て、Durable ObjectへのWebSocket接続を維持し、Durable Objectは軽量のリアルタイムメッセージブローカーとして機能します。アイドル状態では、Durable Objectはハイバネーションし、WebSocket 接続を維持しながらリソースを節約します。Durable Objectにメッセージを送信して、それを起動し、新しい更新をブロードキャストします。
state.durable_object.broadcast(status).await?;
その後、Durable Objectが接続されているすべてのホームページに新しい更新をブロードキャストします。
for ws in self.state.get_websockets() {
ws.send(&status);
}
その後、すべてのライブWebSocketを反復処理して、更新を送信します。
実際の実用的注意点:Durable Objectsはインスタンス間でシャーディングされた場合にパフォーマンスが向上します。 簡単にするために、すべてが1つのDurable Objectを介してすべてを実行するケースを説明しました。
それ以上の拡張のために、次のステップは、ロケーションヒントを用いて、サポートされているロケーションごとに複数のDurableObjectインスタンスを使用することで、世界中のユーザーの遅延を最小限に抑え、単一のロケーションに多数の同時ユーザーが発生した場合のボトルネックを回避することになります。最初はこのパターンの実装を検討しましたが、ATProtoの開発者がアプリのテンプレートとして使用できるような簡潔な「hello world」スタイルの例を作成するという私の目標と競合していました。
7.ライブ変化の変化に耳を傾ける
課題:リアルタイムフィードとサーバーレス
自社のアプリ内で更新を公開するのは簡単ですが、ATProtoエコシステムでは、他のアプリケーションがユーザー向けにステータス更新を公開することが可能です。Statussphereを完全に統合したい場合は、これらのイベントも選択する必要があります。
ライブイベント更新のリッスンには、ATProto JetstreamサービスへのWebSocket接続が永続的に必要です。従来のサーバーベースのアプリは、WebSocket クライアントソケットを無期限に開いたままにすることができますが、サーバーレスプラットフォームはできません。Workersは永遠に実行できません。
ライブサーバーを実行せずに「リッスン」する方法が必要です。
ソリューション:Cloudflare Worker Cron Triggers
これを解決するために、私たちはリッスンロジックをCron Triggerに移動しました。ライブソケットを開いておくのではなく、この機能を使用して、定期的なジョブを使用して、小さなバッチで更新を読み取ります。
スケジュールされたworker呼び出しが開始されると、永続ストレージから最後に確認されたカーソルがロードされます。次に、接続して、Jetstream(ATProtoレポジトリイベントのストリーミングサービス)に接続します。xyz.statusphere.statusコレクションでフィルタリングされ、最後に確認されたカーソルが始まります。
let ws = WebSocket::connect("wss://jetstream1.us-east.bsky.network/subscribe?wantedCollections=xyz.statusphere.status&cursor={cursor}").await?;
カーソル(マイクロ秒のタイムスタンプ)をDurable Objectの永続ストレージに保存するため、オブジェクトが再起動しても、再開場所が正確に把握できます。開始時間より新しいイベントを処理するとすぐに、WebSocket 接続を閉じ、Durable Objectをスリープ状態に戻します。
トレードオフ:更新は最大で1分も遅れる可能性がありますが、システムは完全にサーバーレスのままです。これは、インフラストラクチャの複雑さを最小限に抑えることが、完璧なリアルタイム配信を実現することよりも重要な、初期段階のアプリやプロトタイプに最適です。
オプションでアップグレード:リアルタイムイベントリスナー
リアルタイム更新が必要で、サーバーレスモデルを少し変えたいという場合は、JetstreamへのライブWebSocket接続を維持する軽量のリスナープロセスをデプロイできます。
このプロセスは、1分に1回ポーリングする代わりに、xyz.statusphere.statusコレクションの新しいイベントをリッスンし、到着するとすぐにCloudflare Workerに更新をプッシュします。このリスナープロセスのスケッチはこちらと、それからの更新を処理するエンドポイントはこちらで確認できます。
結果は、依然として従来型のサーバーではありません。
Webへの公開禁止
開いているHTTPポートなし
永続的データベースなし
単一目的のステートレスリスナーであり、アプリが大きくなり、より深刻なインフラストラクチャが必要になるまで、ホームサーバーで実行するのに十分なシンプルなものです。
後で、Cloudflare Queuesのようなツールを使用して、この設計をよりスケーラブルなものに置き換え、バッチ処理や再試行を提供できますが、小規模から中規模のアプリケーションでは、この軽量なリスナーが簡単で効果的なアップグレードです。
将来を見据える
現在、Durable Objectsは長期間のWebSocketサーバー接続を保持しながらハイバネーションすることができますが、長期間のWebSocketクライアント接続を保持する場合はハイバネーションをサポートしません(Jetstreamリスナーのように)。そのため、Statusphereでは、ネットワークと同期を維持するために、Cron Triggerを介したWorker呼び出しや軽量の外部リスナーを使用して回避策を講じています。
Durable Objectsに将来的な改善(アクティブなWebSocketクライアントのハイバネーションのサポートを追加するなど)を行うことで、こうした回避策の必要性を完全に排除する可能性があります。
独自のATProtoアプリを構築する
これは、サーバーゼロで運用コストを最小限に抑えながら、Cloudflareで完全に実行されるフル機能のatprotoアプリです。Workersはほとんどのユーザーから50ミリ秒以内でコードを実行し、KVとD1はデータの可用性を維持し、Durable ObjectsはWebSocketのファンアウトとライブ調整を処理します。
Cloudflareにデプロイボタンを使用して、リポジトリを複製し、サーバーレス環境を設定します。そして、あなたが構築したものを私たちに見せてください。当社のDiscordにリンクをドロップするか、Blueskyで@cloudflare.socialまたはXで@CloudflareDevのタグを付けてください。ぜひご覧ください。