この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
どうも!オペ部西村祐二です。
この記事は「Cloudflare のアドベントカレンダー」の4日目の記事です。
以前、下記のようなブログを書きました。そこからCloudflareのサービスに興味がでてきましたので今回、Cloudflare Workers(Rust)からCloudflare Workers KVを使うチュートリアルをやってみました。
今回、やってみたチュートリアルは下記になります。
Use Workers KV directly from Rust · Cloudflare Workers docs
Cloudflare Workers KVとは
Cloudflareのアプリケーション向けサーバーレス Key-Value ストレージです。
Cloudflare Workers KVは、Cloudflareのグローバルネットワーク内にあるすべてのデータセンターで、セキュアな低レイテンシーKey-Value Storeへのアクセスを提供。
Cloudflare Workers KV|サーバーレスコンピューティング | Cloudflare
やってみる
環境
- MacOS: 10.15.7
-
Rust: v1.58.0-nightly
-
Wrangler: 1.19.5
事前準備
- warnglerをインストール
wranglerというCLIをインストールしてCloudflareの環境を操作できるようにしておいてください。
詳細は下記参考サイトを確認ください。
cloudflare/wrangler: ? wrangle your Cloudflare Workers
Get started guide · Cloudflare Workers docs
雛形プロジェクトを作成
wranglerを使って雛形プロジェクトを作成します。
worker-rsを使用したテンプレートを指定します。
worker-rsはWebAssembly経由でRustで記述したプログラムをCloudflare Workersで動かすためのクレート(ライブラリ)です。
$ wrangler generate workers-kv-from-rust https://github.com/cloudflare/rustwasm-worker-template/
$ cd workers-kv-from-rust
$ git add -A
$ git commit -m 'Initial commit'
KV namespaceを作成
wranglerをつかって「KV_FROM_RUST」を指定し、namespaceを作成します。
プロジェクト名と指定した値を連結した「workers-kv-from-rust-KV_FROM_RUST」というnamespaceが作成されます。
❯ wrangler kv:namespace create "KV_FROM_RUST"
? Creating namespace with title "workers-kv-from-rust-KV_FROM_RUST"
✨ Success!
Add the following to your configuration file:
kv_namespaces = [
{ binding = "KV_FROM_RUST", id = "xxxxxxxxxxxxxxxxxxx" }
また、--preview
をつけるとローカルから実行するときに参照されるKV namespaceを作成してくれます。
❯ wrangler kv:namespace create "KV_FROM_RUST" --preview
? Creating namespace with title "workers-kv-from-rust-KV_FROM_RUST_preview"
✨ Success!
Add the following to your configuration file:
kv_namespaces = [
{ binding = "KV_FROM_RUST", preview_id = "xxxxxxxxxxxxxx" }
]
webコンソールを確認するとちゃんと作成されていることがわかります。
KV namespaceをバインドさせる
KV namespace作成時に生成されたidをwrangler.tomlファイルに追加します。
wrangler.toml
.
.
.
kv_namespaces = [
{ binding = "KV_FROM_RUST", preview_id = "xxxxxxxxxxxxx", id = "xxxxxxxxxx" }
]
.
.
.
KV namespaceにアクセスするための下処理追加
JS側では変数KV_FROM_RUSTとして、作成したKV namespaceにアクセスします。Rustの名前空間から読み取りまたは書き込みを行うには、オブジェクト全体をRustハンドラー関数に渡す必要があります。
$ mkdir worker
$ cd worker
$ touch worker.js
worker/worker.js
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
const { handle } = wasm_bindgen;
const instance = wasm_bindgen(wasm);
/**
* Fetch and log a request
* @param {Request} request
*/
async function handleRequest(request) {
await instance;
return await handle(KV_FROM_RUST, request);
}
このチュートリアルでは、Rust側で可能な限り多くの処理を実行し、リクエストを直接wasmハンドラーに渡して、wasmハンドラーが応答を作成して返します。
リクエストとレスポンスの処理をJavaScript側で維持するテンプレートとは異なることに注意してください。
Rust側でなるべく処理を寄せるために、web-sysをRust依存関係の1つとして宣言し、Request、Response、およびResponseInitを明示的に有効にします。(UrlおよびUrlSearchParams機能はこのチュートリアルの後半で使用されます)。
.
.
.
[dependencies.web-sys]
version = "0.3"
features = [
'Request',
'Response',
'ResponseInit',
'Url',
'UrlSearchParams',
]
下記のように記述することでRustのRequest、Responseを使用して、常に200OKステータスで応答するシンプルなハンドラーを作成できます。
src/lib.rs
extern crate cfg_if;
extern crate wasm_bindgen;
mod utils;
use cfg_if::cfg_if;
use wasm_bindgen::{JsCast, prelude::*};
use web_sys::{Request, Response, ResponseInit};
cfg_if! {
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
if #[cfg(feature = "wee_alloc")] {
extern crate wee_alloc;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
}
}
#[wasm_bindgen]
pub fn handle(kv: JsValue, req: JsValue) -> Result<Response, JsValue> {
let req: Request = req.dyn_into()?;
let mut init = ResponseInit::new();
init.status(200);
Response::new_with_opt_str_and_init(None, &init)
}
wasm_bindgenを使用してKVにバインドする
先程のsrc/lib.rs
をベースに発展させていきます。
KV APIはJavaScriptのpromiseを返すため、依存関係としてwasm-bindgen-futures
とjs-sys
を追加する必要があります。
Cargo.toml highlight=2-4
[dependencies]
cfg-if = "0.1.2"
wasm-bindgen = "=0.2.73"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
wasm_bindgen
を使用して型バインディングを作成してKVオブジェクトにアクセスするようにしたコードが下記になります。
src/lib.rs
.
.
.
#[wasm_bindgen]
pub fn handle(kv: WorkersKvJs, req: JsValue) -> Result<Response, JsValue> {
let req: Request = req.dyn_into()?;
let mut init = ResponseInit::new();
init.status(200);
Response::new_with_opt_str_and_init(None, &init)
}
#[wasm_bindgen]
extern "C" {
pub type WorkersKvJs;
#[wasm_bindgen(structural, method, catch)]
pub async fn put(
this: &WorkersKvJs,
k: JsValue,
v: JsValue,
options: JsValue,
) -> Result<JsValue, JsValue>;
#[wasm_bindgen(structural, method, catch)]
pub async fn get(
this: &WorkersKvJs,
key: JsValue,
options: JsValue,
) -> Result<JsValue, JsValue>;
}
KVのラッパーを作成
KVパラメーターをそのまま使用し始めることもできますが、wasm_bindgenによって生成された関数シグネチャはRust内で機能するのが難しい場合があります。より簡単に扱うために、WorkersKvJs型の周りに単純な構造体を作成して、よりRustに適したAPIでラップします。
src/lib.rs
.
.
.
use js_sys::{ArrayBuffer, Object, Reflect, Uint8Array};
struct WorkersKv {
kv: WorkersKvJs,
}
impl WorkersKv {
async fn put_text(&self, key: &str, value: &str, expiration_ttl: u64) -> Result<(), JsValue> {
let options = Object::new();
Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?;
self.kv
.put(JsValue::from_str(key), value.into(), options.into())
.await?;
Ok(())
}
async fn put_vec(&self, key: &str, value: &[u8], expiration_ttl: u64) -> Result<(), JsValue> {
let options = Object::new();
Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?;
let typed_array = Uint8Array::new_with_length(value.len() as u32);
typed_array.copy_from(value);
self.kv
.put(
JsValue::from_str(key),
typed_array.buffer().into(),
options.into(),
)
.await?;
Ok(())
}
async fn get_text(&self, key: &str) -> Result<Option<String>, JsValue> {
let options = Object::new();
Reflect::set(&options, &"type".into(), &"text".into())?;
Ok(self
.kv
.get(JsValue::from_str(key), options.into())
.await?
.as_string())
}
async fn get_vec(&self, key: &str) -> Result<Option<Vec<u8>>, JsValue> {
let options = Object::new();
Reflect::set(&options, &"type".into(), &"arrayBuffer".into())?;
let value = self.kv.get(JsValue::from_str(key), options.into()).await?;
if value.is_null() {
Ok(None)
} else {
let buffer = ArrayBuffer::from(value);
let typed_array = Uint8Array::new_with_byte_offset(&buffer, 0);
let mut v = vec![0; typed_array.length() as usize];
typed_array.copy_to(v.as_mut_slice());
Ok(Some(v))
}
}
}
上記のラッパーは、KV APIでサポートされているオプションのサブセットのみを利用可能にします。たとえば、putのexpirationTtlの代わりにexpirationや、getのtextやarrayBuffer以外のタイプなど、他のオプションも同様の方法でラップできます。概念的には、ラッパーメソッドはすべてReflect::set
を使用して手動でJSオブジェクトを作成し、必要に応じて戻り値を標準のRust型に変換します。
ラッパーを利用する処理を追加
ラッパーを使用してKV namespaceとの間で値を読み書きする準備が整いました。
次の関数は、URLセグメントを使用してKVドキュメントのキー名と値を決定し、PUTリクエストでKVに書き込むハンドラーの例です。たとえば、PUTリクエストを/foo?value=bar
に送信すると、「bar」値がfooキーに書き込まれます。
さらに、サンプルハンドラーは、GETリクエスト時に、URLパス名をキー名として使用してKVから読み取ります。たとえば、GET /foo
リクエストは、fooキーの値があればそれを返します。
src/lib.rs
.
.
.
#[wasm_bindgen]
pub async fn handle(kv: WorkersKvJs, req: JsValue) -> Result<Response, JsValue> {
let req: Request = req.dyn_into()?;
let url = web_sys::Url::new(&req.url())?;
let pathname = url.pathname();
let query_params = url.search_params();
let kv = WorkersKv { kv };
match req.method().as_str() {
"GET" => {
let value = kv.get_text(&pathname).await?.unwrap_or_default();
let mut init = ResponseInit::new();
init.status(200);
Response::new_with_opt_str_and_init(Some(&format!("\"{}\"\n", value)), &init)
},
"PUT" => {
let value = query_params.get("value").unwrap_or_default();
// set a TTL of 60 seconds:
kv.put_text(&pathname, &value, 60).await?;
let mut init = ResponseInit::new();
init.status(200);
Response::new_with_opt_str_and_init(None, &init)
},
_ => {
let mut init = ResponseInit::new();
init.status(400);
Response::new_with_opt_str_and_init(None, &init)
}
}
}
src/lib.rsのコード全体
チュートリアルで紹介されたlib.rsのコードは下記のようになります。
KVにデータをPUTするときにTTL60秒が設定されて保存されるようになっているのでご注意ください。
src/lib.rs
extern crate cfg_if;
extern crate wasm_bindgen;
mod utils;
use cfg_if::cfg_if;
use js_sys::{ArrayBuffer, Object, Reflect, Uint8Array};
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{Request, Response, ResponseInit};
cfg_if! {
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
if #[cfg(feature = "wee_alloc")] {
extern crate wee_alloc;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
}
}
#[wasm_bindgen]
pub async fn handle(kv: WorkersKvJs, req: JsValue) -> Result<Response, JsValue> {
let req: Request = req.dyn_into()?;
let url = web_sys::Url::new(&req.url())?;
let pathname = url.pathname();
let query_params = url.search_params();
let kv = WorkersKv { kv };
match req.method().as_str() {
"GET" => {
let value = kv.get_text(&pathname).await?.unwrap_or_default();
let mut init = ResponseInit::new();
init.status(200);
Response::new_with_opt_str_and_init(Some(&format!("\"{}\"\n", value)), &init)
}
"PUT" => {
let value = query_params.get("value").unwrap_or_default();
// set a TTL of 60 seconds:
kv.put_text(&pathname, &value, 60).await?;
let mut init = ResponseInit::new();
init.status(200);
Response::new_with_opt_str_and_init(None, &init)
}
_ => {
let mut init = ResponseInit::new();
init.status(400);
Response::new_with_opt_str_and_init(None, &init)
}
}
}
#[wasm_bindgen]
extern "C" {
pub type WorkersKvJs;
#[wasm_bindgen(structural, method, catch)]
pub async fn put(
this: &WorkersKvJs,
k: JsValue,
v: JsValue,
options: JsValue,
) -> Result<JsValue, JsValue>;
#[wasm_bindgen(structural, method, catch)]
pub async fn get(
this: &WorkersKvJs,
key: JsValue,
options: JsValue,
) -> Result<JsValue, JsValue>;
}
struct WorkersKv {
kv: WorkersKvJs,
}
impl WorkersKv {
async fn put_text(&self, key: &str, value: &str, expiration_ttl: u64) -> Result<(), JsValue> {
let options = Object::new();
Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?;
self.kv
.put(JsValue::from_str(key), value.into(), options.into())
.await?;
Ok(())
}
async fn put_vec(&self, key: &str, value: &[u8], expiration_ttl: u64) -> Result<(), JsValue> {
let options = Object::new();
Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?;
let typed_array = Uint8Array::new_with_length(value.len() as u32);
typed_array.copy_from(value);
self.kv
.put(
JsValue::from_str(key),
typed_array.buffer().into(),
options.into(),
)
.await?;
Ok(())
}
async fn get_text(&self, key: &str) -> Result<Option<String>, JsValue> {
let options = Object::new();
Reflect::set(&options, &"type".into(), &"text".into())?;
Ok(self
.kv
.get(JsValue::from_str(key), options.into())
.await?
.as_string())
}
async fn get_vec(&self, key: &str) -> Result<Option<Vec<u8>>, JsValue> {
let options = Object::new();
Reflect::set(&options, &"type".into(), &"arrayBuffer".into())?;
let value = self.kv.get(JsValue::from_str(key), options.into()).await?;
if value.is_null() {
Ok(None)
} else {
let buffer = ArrayBuffer::from(value);
let typed_array = Uint8Array::new_with_byte_offset(&buffer, 0);
let mut v = vec![0; typed_array.length() as usize];
typed_array.copy_to(v.as_mut_slice());
Ok(Some(v))
}
}
}
ローカルで動作確認
CLIを使ってローカルサーバを実行してみます。
するとエラーとなってしまいました。
$ wrangler dev
.
.
.
[INFO]: ⬇️ Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨ Done in 1.31s
[INFO]: ? Your wasm pkg is ready to publish at /Users/nishimura.yuji/study/cloudflare/kv/workers-kv-from-rust/build.
Error: Something went wrong with the request to Cloudflare...
Uncaught SyntaxError: The requested module './index_bg.mjs' does not provide an export named 'fetch'
at line 2
[API code: 10021]
エラー修正
wrangler.toml
の2行がjavascript
になっていたので、rust
に修正します。
wrangler.toml
name = "workers-kv-from-rust"
type = "rust"
workers_dev = true
compatibility_date = "2021-12-03"
kv_namespaces = [
.
.
.
再度、ローカルで動作確認
CLIを使ってローカルサーバを実行してみます。
一応エラーが出ずにロールサーバがたちあがりました。いくつか警告がでていますがチュートリアルということで今回は無視して進めます。
$ wrangler dev
.
.
.
[INFO]: ⬇️ Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨ Done in 0.53s
[INFO]: ? Your wasm pkg is ready to publish at /Users/nishimura.yuji/study/cloudflare/kv/workers-kv-from-rust/pkg.
[WARN]: ⚠️ There's a newer version of wasm-pack available, the new version is: 0.10.1, you are using: 0.10.0. To update, navigate to: https://rustwasm.github.io/wasm-pack/installer/
? watching "./"
? Listening on http://127.0.0.1:8787
curlを使って動作確認してみます。
$ curl 'localhost:8787/foo'
""
$ curl -X PUT 'localhost:8787/foo?value=bar'
$ curl 'localhost:8787/foo'
"bar"
問題なく動作しているようです。
また、TTLを60秒で設定しているので、時間が経過するとKVからデータが消えていることも確認できました。
webコンソールから確認してみます。
末尾に「preview」のKV namespaceの中にデータがあることが確認できました。
デプロイ
チュートリアルでは記載はないですが、実際にデプロイして動作確認もしてみます。
下記コマンドで簡単にデプロイできます。
❯ wrangler publish
? Compiling your project to WebAssembly...
[INFO]: ? Checking for the Wasm target...
[INFO]: ? Compiling to Wasm...
[INFO]: ⬇️ Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
.
.
.
✨ Build succeeded
✨ Successfully published your script to
https://<your url>
curlで動作確認してみます。
今度はJSON形式のデータの{"name":"foo","selected":[1,2,3],"flags":{"a":true,"b":false}}
を保存します。
jsonをurlにパスに変換する必要があります。
$ curl -X PUT 'https://<your url>/foo2?value=%7B%22name%22%3A%22foo%22%2C%22selected%22%3A%5B1%2C2%2C3%5D%2C%22flags%22%3A%7B%22a%22%3Atrue%2C%22b%22%3Afalse%7D%7D'
$ curl 'https://<your url>/foo2'
"{"name":"foo","selected":[1,2,3],"flags":{"a":true,"b":false}}"
webコンソールからもきちんとデータが保存されていることが確認できます。
また KV namespaceはpreview
がついていないnamespaceに保存されています。
さいごに
Cloudflare Workers(Rust)からCloudflare Workers KVを使うチュートリアルをやってみました。
Cloudflare WorkersがV8を直接実行できる環境ということもあって、RustとWebAssemblyの知識、wasm-bindgenの使い方などの知識が必要となり、なかなかハードルを感じましたがとても楽しく、勉強になるチュートリアルでした。
誰かの参考になれば幸いです。
参考サイト
Rust から WebAssembly にコンパイルする - WebAssembly | MDN
wasm-pack で JS の Promise を await できる非同期 Rust を書いて node.js で動かす - Qiita
fkettelhoit/workers-kv-from-rust: Example Cloudflare Worker that calls Workers KV directly from Rust