🙆

入門Cloudflare Workers

2023/04/12に公開約22,300字

はじめに

この記事はCloudflare Workersの入門記事です。

  • 名前は聞いたことがあるけれどCloudflare Workersが何者なのか知らない方
  • 「Cloudflare Workersはサーバーレス・エッジコンピューティングサービスだよ」と説明されて日本語でOKと感じた方
  • AWSのLambdaやGCPのCloud Runと似たようなサービスだろうと認識されている方

上記に当てはまる方のお役に立てるはずです。

開発環境の構築

まずは開発環境を構築しましょう。といっても、最新のnode.jsをインストールするだけです。

インストールできたらバージョンを確認しておきましょう。

$ node --version
v19.6.0

補足

  • v16.13.0より新しいバージョンのnodeが必要になります。記事を読み進めて不具合が発生した場合はnodeのバージョンを確認してください。
  • Windowsの動作検証はしていません。ここから先の手順は問題なく動作するはずですが、不具合が発生した場合はWSLなどのLinux環境を利用してください。

サインアップ

それではCloudflare Workersを試してみましょう。なんと完全に無料で使えるFreeプランが用意されています。太っ腹ですね。

もちろんクレジットカードの登録は不要です。気づかない間に課金されてクラウド破産することはありません。ご安心ください。

トップページのリンクは以下になります。Sign upをクリックしてサインアップを行ってください。

サブドメインの設定

サインアップが完了したらCloudflareのトップページを開いてLoginをクリックしてください。ダッシュボードが表示されます。

Cloudflare Workersはプロジェクトごとにhttps://プロジェクト名.サブドメイン.workers.devを発行してくれます;。サブドメインは自由に設定できます。

ダッシュボードのWorkersをクリックするとサブドメインの入力が求められます。お好みのサブドメインを設定してください。

以降はサブドメインとしてexampleを設定したものと仮定して説明を進めます。

wranglerのセットアップ

(追記 2023-06-04)wrangler initサブコマンドに代わりプロジェクトセットアップ用のコマンドとしてc3がリリースされました。時期を見計らってc3を利用した形の説明に修正する予定です。なお廃止予定はwrangler initサブコマンドを利用したプロジェクトの初期化のみであり、wranglerコマンドは引き続き利用できます。

続いてwranglerのセットアップを行います。wranglerはCloudflare Workersを操作するためのCLIツールです。

Cloudflare Workersはダッシュボードから操作できます。しかし、ダッシュボードの見た目は今後変更されるかもしれません。そうするとスクリーンショットを貼り付けた説明では混乱する恐れがあります。

そこで、wranglerを利用して説明することにします。一度コマンドを覚えれば作業効率もアップします。

インストール

以下のコマンドを実行してください。

$ npm install -g wrangler

インストールが終わったら、npx wrangler --versionを実行してください。バージョンが表示されたらインストールは成功です。

$ npx wrangler --version
 ⛅️ wrangler 2.14.0

認証

インストールしただけでは使えません。認証が必要になります。npx wrangler loginを実行してください。

参考までに実行令を示します。

$ npx wrangler login
 ⛅️ wrangler 2.14.0
--------------------
Attempting to login via OAuth...
Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=...

上記のメッセージが表示された後にブラウザが起動します。URLが間違いなくcloudflare.comであることを確認してからAllowボタンをクリックしてください。

認証が成功すると「Successfully logged in.」と表示されます。

$ npx wrangler login
...
Successfully logged in.

以上でwranglerのセットアップは完了です。

Hello, World!

準備は整いました。次のステップは「Hello, World!」です。

プロジェクトの初期化

まずはプロジェクトの初期化を行いましょう。ここではプロジェクト名としてhelloを設定することにします。

以下のコマンドを実行してください。

$ npx wrangler init hello

初期化の際にいくつか質問されます。今回は以下のように答えました。

  • gitで管理しますか? → no
  • TypeScriptを使いますか? → yes
  • Vitestのテストを作成しますか? → no

今回はサンプルということでgitの管理とテストコードの生成は外しました。TypeScriptは必須ではありません。説明を進める上で都合が良いためTypeScriptを選んだだけです。

プロジェクトの初期化が成功するとhelloディレクトリが生成されます。

生成されたファイルを確認する

続いて生成されたファイルを確認しましょう。helloディレクトリ内のsrc/index.tsを開いてください。

記事の執筆時点では以下のコードが生成されました。

/**
 * Welcome to Cloudflare Workers! This is your first worker.
 *
 * - Run `wrangler dev src/index.ts` in your terminal to start a development server
 * - Open a browser tab at http://localhost:8787/ to see your worker in action
 * - Run `wrangler publish src/index.ts --name my-worker` to publish your worker
 *
 * Learn more at https://developers.cloudflare.com/workers/
 */

export interface Env {
  // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
  // MY_KV_NAMESPACE: KVNamespace;
  //
  // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
  // MY_DURABLE_OBJECT: DurableObjectNamespace;
  //
  // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
  // MY_BUCKET: R2Bucket;
  //
  // Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/
  // MY_SERVICE: Fetcher;
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    return new Response("Hello World!");
  },
};

コードの解説

生成されたsrc/index.tsについて説明します。

まず、冒頭のブロックコメントはCloudflare Workersの紹介文ですから読み飛ばしてください。このコメントは削除して構いません。

次はEnvインターフェースの定義です。Cloudflareは様々なサービスを提供しており、それらと連携する際に必要になります。発展的な内容になるため、ひとまず読み飛ばしましょう。

最後は関数定義です。async fetchと書かれている部分に注目してください。これがWorkerのエントリーポイントになります。

ライフサイクル

fetch関数で実行される処理について、流れを説明します。

  1. HTTPリクエストを受け取る
  2. 何らかの処理を行う
  3. HTTPレスポンスを返す

以上です。

大まかに言えば、Workerはfetch関数の実行単位です。Cloudflareのサーバーにリクエストが到着すると、その都度Workerが作成されてfetch関数が実行されます。fetch関数の実行が完了するとWorkerは削除されます。

Cloudflare WorkersはWorkerのホスティングサービスです。サーバーを構築することなくサーバーサイドの処理を実装できるため「サーバーレス」とよばれています。

リクエストとレスポンスの型

改めてfetch関数に注目してください。引数のRequestと戻り値のResponseはJavaScript標準APIで定義されたインターフェースに準拠しています。

例えばレスポンスのヘッダーを設定するには、Responseコンストラクタの第2引数にHeadersのインスタンスを渡します。

// Content-Lengthヘッダーは自動的に設定されます。手作業で設定する必要はありません。
const headers = new Headers({
  "Content-Type": "application/json",
});

return new Response("{values: [1, 2, 3]}", { headers });

リクエストとレスポンスの構造については各自でMDNのドキュメントを参照してください。

なおfetch関数には第3の引数としてctx: ExecutionContextが定義されています。こちらもEnvインターフェースと同様に発展的な内容になるため、説明をスキップします。

ローカルで動作を確認する

お待たせしました。動作確認の時間です。まずは手元のマシンでWorkerを動かしてみましょう。

以下のコマンドを実行してください。

$ npx wrangler dev

デフォルトではhttp://localhost:8787でリクエストを待ち受けます。

それではcurlコマンドを使ってリクエストを投げてみましょう。以下のように表示されたら成功です。

$ curl http://localhost:8787
Hello, World!

おめでとうございます、「Hello, World!」が表示されました!

なお、レスポンスに改行文字「\n」を含めていないため、実際には「Hello, World!$ 」のようにプロンプトの文字「$」が続けて表示されるはずです。気になる方は\nを追加してください。

インターネットに公開する

おもしろいのはここからです。以下のコマンドを実行してください。

$ npx wrangler publish

これでインターネットを経由してWorkerにアクセスできるようになりました。curlコマンドを実行してみてください。

$ curl https://hello.example.workers.dev
Hello, World!

素晴らしい!

通常、デプロイは数秒で完了します。レスポンスのメッセージを書き換えてnpx wrangler publishを実行してみてください。この手軽さがCloudflare Workersの魅力です。

利用例の検討

ここからはCloudflare Workersがどのような場面で利用できるか検討します。

ロジックの隠蔽

まず考えられるのが、ロジックの隠蔽です。例えば、以下のような処理をWorkerで実行するとします。

export interface Env {
  // ...
}

const omikuji = ["大吉", "中吉", "小吉"];

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const i = Math.floor(Math.random() * omikuji.length);

    return new Response(omikuji[i]);
  },
};

おみくじをを引くWorkerです。リクエストごとにランダムな結果が帰ってきます。

$ curl http://localhost:8787
大吉
$ curl http://localhost:8787
中吉
$ curl http://localhost:8787
中吉

どれだけ難読化してもクライアントサイドのJavaScriptは解析される恐れがあります。そこで、解析されて困るロジックをWorkerで実行することで隠蔽します。

おみくじを例に考えましょう。

  • どのような候補が定義されているか?
  • 候補の中から1つを選ぶ処理はどのように実装されているか?

おみくじがクライアントサイドのJavaScriptに実装されている場合、上記について解析される恐れがあります。これを隠蔽するのにCloudflare Workersは最適です。

cfプロパティを用いた動的なコンテンツ配信

Requestインターフェースに注目してください。JavaScript標準APIに準拠すると同時に、実はCloudflareが独自に定義したcfプロパティが追加されています。

多くのcfプロパティが用意されているのですが、ここでは以下をピックアップします。

  • country: 国コード
  • asn: AS番号
  • asOrganization: AS組織名

上記のcfプロパティを返す実装例を示します。

export interface Env {
  // ...
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const body = {
      country: request.cf.country,
      asn: request.cf.asn,
      asOrganization: request.cf.asOrganization,
    };

    const headers = new Headers({
      "Content-Type": "application/json",
    });

    return new Response(JSON.stringify(body, null, 2), { headers });
  },
};

以下のような結果が得られます。

$ curl http://localhost:8787
{
  "country": "JP",
  "asn": 9605,
  "asOrganization": "NTT Docomo"
}

ここでは例としてdocomo回線のスマホにテザリングした状態でcurlコマンドを実行しました。リクエスト送信元のネットワークが正しく判定されています。

cfプロパティを利用すれば動的なコンテンツの配信が簡単に実装できます。例えばニュースフィードを実装するとして、countryの値でユーザーが居住している国ごとにコンテンツを出汁分けることができます。asnの値を記録しておけば、トラブルが発生したときに原因の切り分けに役立つかもしれません。

ローカルで動作確認する際の注意点

鋭い方はhttp://localhost:8787にリクエストを送信していることに気がついたはずです。国コードはOSの環境変数から取得できるかもしれませんが、AS番号や組織名はどのように取得しているのでしょうか。

実はnpx wrangler devで起動したWorkerはCloudflareのサーバー上で実行されるWorkerと同じ振る舞いをします。従って、ローカルで動作確認をする場合もインターネットに接続されている必要があります。

Cloudflare Workersを深く知る

ここからはCloudflare Workersを使いこなすために把握しておくべき項目について説明します。

Workerは世界中にデプロイされる

ここまで何度かnpx wrangler publishを実行しました。その際、デプロイ先のサーバーは指定しませんでした。

デプロイしたコードはどこに設置されたサーバーで実行されるのでしょうか。日本でしょうか、あるいは米国でしょうか。

答えは世界中です。

coloに格納される値の検証

それでは確かめてみましょう。cfプロパティにはリクエストが到着したデータセンターを識別するcoloが用意されています。

colo string

The three-letter IATA airport code of the data center that the request hit, for example, "DFW".

(引用元)https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties

引用のとおり、coloに格納される値はデータセンター最寄りの空港を示すIATAコードです。

以下のコードをnpx wrangler publishでデプロイしてください。

export interface Env {
  // ...
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const body = {
      colo: request.cf.colo,
      country: request.cf.country,
      asn: request.cf.asn,
      asOrganization: request.cf.asOrganization,
    };

    const headers = new Headers({
      "Content-Type": "application/json",
    });

    return new Response(JSON.stringify(body, null, 2), { headers });
  },
};

以下、AWSのEC2からCloudflare Workersに向けてリクエストを送信したときの結果を示します。検証を行ったリージョンは米国のバージニア北部(us-east-1a)と日本の東京(ap-northeast-1a)です。末尾に1aとあるように、AZはそれぞれ1aを選んでいます。

リクエストの送信元 country colo asn asOrganization
us-east-1a US IAD 14618 Amazon.com
ap-northeast-1a JP NRT 16509 Amazon.com

ご覧のとおり、coloの値が変化しています。リクエストの送信元に最も近い地域のデータセンターにデプロイされていることを確認できました。

レイテンシーの測定

疑い深い方のために、さらに追加で検証してみましょう。以下のようにtimeコマンドを用いてレスポンスが返ってくるまでの時間を測定します。

# docomo回線のスマホにテザリングした状態で測定(※1)
$ time curl https://hello.example.workers.dev
{
  "colo": "NRT",
  "country": "JP",
  "asn": 9605,
  "asOrganization": "NTT Docomo"
}
real	0m0.415s
user	0m0.022s
sys	0m0.012s

以下、us-east-1aとap-northeast-1aの実行結果を示します。

リージョン 1回目の測定結果 2回目の測定結果 3回目の測定結果 4回目の測定結果 5回目の測定結果
us-east-1a 44 ms 32 ms 226 ms (※2) 38 ms 34 ms
ap-northeast-1a 48 ms 36 ms 39 ms 35 ms 37 ms

仮にCloudflare Workersのデプロイ先が米国であれば、ap-northeast-1aからリクエストを送信してレスポンスを受信するまでに太平洋を往復することになります。しかし、リクエストの送信元に関わらずレイテンシーは40 ms前後でほぼ均一です。

この結果から、リクエストの送信元に最も近いデータセンターが選択されており、それらのデータセンターで同じコードが動作していることが確認できます。

補足

  1. docomo回線のスマホにテザリングした状態での測定結果です。たかだか数十バイトのレスポンスを返すのに415 msも待たされていますが、fast.comの測定によると元々docomo回線はUnloadedのレイテンシーが大きいため上記のような結果になっています。
  2. us-east-1aからCloudflare Workersにリクエストを送信すると、まれに200 ms程度のレイテンシーが発生しました。どのような条件下でレイテンシーが増えるのかは特定できませんでしたが、リクエストがbotによるDDoS攻撃かどうか判定しているのかもしれません。

IATA空港コードについて

余談になりますが、Cloudflareのドキュメントによると日本国内のCloudflareサーバーは東京・大阪・福岡・那覇の4都市に配置されているそうです。

ここで先ほどの検証結果を見てみると、coloの値がHND(羽田空港)ではなくNRT(成田空港)と表示されています。Cloudflareは千葉を東京圏内と判定しているのか、あるいはcoloの割り振りは大雑把なのか、その点はよくわかりません。

それはともかく、そもそもcoloの値に依存した処理を実装するべきではありません。せっかく冗長化されているわけですから、どのデータセンターにデプロイされても問題ない実装を考えるべきです。

ランタイム

ここまでランタイムについては触れていませんでした。サーバーレスとはいえ、仕組みがどうなっているか把握しておくことは必要です。

まず、Cloudflare WorkersのJavaScriptランタイムにはV8が使われています。V8はnode.jsやChromiumで採用されているJavaScriptランタイムです。別の言い方をすれば、CloudflareのサーバーでホストされているV8をCloudflare Workersと命名したのだと考えて差し支えないでしょう。

V8は関数を実行するためのサンドボックスを提供します。サンドボックスはリクエストごとに作成されるため、Worker同士は隔離されます。Cloudflare WorkersはVMやコンテナの代わりにV8を採用することで軽量な仮想化を実現していると言えます。

ブラウザでタブを開いた状況をイメージしてください。例えば、github.comを開いたタブとzenn.devを開いたタブがあるとします。それぞれのタブは隔離されているため、互いの通信を盗み見たり勝手にスクリプトを操作したりできません。V8は仮想化ソフトウェアではありませんが、仮想化と同等の機能を実現していると言えます。

従って、Cloudflare Workersではプロセス間の通信などは行えません。また、異なるWorker同士で処理の結果を共有する場合はCloudflare KV、処理の結果を永続化する場合はCloudflare R2などの別サービスと連携する必要があります。

グローバル変数

ところで、サンドボックスの話に関連して注意が必要な箇所があります。グローバル変数です。

以下のコードをみてください。

export interface Env {
  // ...
}

// 説明を簡単にするためcount変数のロックは考慮していません。
var count = 0;

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    count += 1;

    return new Response(count);
  },
};

上記はアクセスカウンターです。リクエストを受け取る旅にグローバル変数countがインクリメントされます。

紛らわしいのですが、サンドボックスで隔離されるのはWorker単位です。例えばhttps://aaa.example.workers.devhttps://bbb.example.workers.devがあるとします。当然お互いのグローバル変数にはアクセスできません。しかし、同じWorkerであれば実行時の環境は引き継がれるためグローバル変数が参照できます。

さらに紛らわしいのですが、npx wrangler devを実行してローカルでWorkerを動作させる場合と、CloudflareのサーバーでWorkerを動作させる場合を比べると、挙動が異なるように見えることがあります。

ローカルでWorkerを動作させる場合の実行結果

$ curl http://localhost:8787
1
$ curl http://localhost:8787
2
$ curl http://localhost:8787
3

CloudflareのサーバーでWorkerを動作させる場合の実行結果

$ curl https://hello.example.workers.dev
1
$ curl https://hello.example.workers.dev
1
$ curl https://hello.example.workers.dev
1

CloudflareのサーバーでWorkerを動作させると、レスポンスとして常に1が返されます。しかし、これは不具合ではありません。Cloudflareのサーバーで実行されるWorkerは冗長化されています。つまり、上記の例で1番目・2番目・3番目のリクエストを受け取ったWorkerはそれぞれ異なります。

一見すると正しくインクリメントされていないように見えますが、それぞれのWorkerでは正しくインクリメントされています。そのため、偶然に同じWorkerが使い回されるとレスポンスとして2や3が返ってくる可能性はあります。

ただし、グローバル変数に依存した処理を実装するべきではありません。CloudflareのドキュメントにもWorkerがどのタイミングで破棄されるのか言及がありません。

基本的には、リクエストのたびにWorkerが作成されて、処理の完了と同時にWorkerが破棄されると考えて処理を実装するべきです。

リソースの制限

あんなこといいな、できたらいいなと検討中の皆様に残念なお知らせがあります。Cloudflare Workersには以下の制限が設けられています。

Worker単位の制限

項目 無料プラン 有料プラン
CPUの利用時間 10 ms 50 ms
メモリ容量 128 MB 128 MB
環境変数の個数 64個 128個
環境変数の容量 5 KB 5 KB
Worker自体の容量 1 MB 5 MB

CPU利用時間の制限

注目していただきたいのがCPUの利用時間です。数値の単位はms(ミリ秒)です。「無料プランは10ミリ秒!?これじゃあHello, World!の文字列を返すくらいしか使い道がないぞ?」と思われるかもしれません。

しかし、悲しむ必要はありません。以下のコードを実行してみてください。

export interface Env {
  // ...
}

async function sleep(duration: number): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`I slept for ${duration} ms.`);
    }, duration);
  });
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const url = new URL(request.url);
    const duration = url.searchParams.get("duration");
    const message = await sleep(Number.parseInt(duration) || 1000);

    return new Response(message);
  },
};

上記はクエリパラメータdurationで指定された時間スリープするコードです。

それでは試してみましょう。例として5000ミリ秒スリープさせてみます。

$ curl 'https://hello.example.workers.dev?duration=5000
I slept for 5000 ms.

curlコマンドを実行してから5秒後に上記のメッセージが表示されます。CPUの利用時間は10 msに制限されているのに、なぜ実行できるのでしょうか。

そうです、カウントされるのはリクエストを受け取ってからレスポンスを返すまでの所要時間ではなく、CPUの利用時間です。

上記のコードの場合、await sleep(duration)が実行された段階で処理は一時停止して実行キューに積まれます。その後、5秒経過するとスケジューラが処理を再開してレスポンスを返します。setTimeoutでスリープしている間の5秒間はCPU利用時間としてカウントされません。

他に時間のかかりそうな処理と言えば、URLのパースと数値型への変換くらいのものです。細かく見ればメッセージを組み立てるのに文字列テンプレートを利用しているため、その辺りもCPU利用時間を消費しそうです。とはいえ合計しても数百マイクロ秒か、多く見積もっても数ミリ秒のオーダーで処理は完了するはずです。

従って、JavaScriptの非同期処理をうまく利用すれば、10 msの制限の中でもある程度複雑な処理が行えます。

例えばWorkerからHTTPリクエストを送信することが可能です。なぜなら実行時間の内訳はそのほとんどがレスポンスが帰ってくるまでの待ち時間だからです。CPUの利用時間はごくわずかです。

所要時間の制限

Cloudflareのドキュメントによると、所要時間に制限はありません。ただし、実行から30秒が経過すると強制的にタイムアウトされる可能性があるそうです。

There is no hard limit for duration. However, after 30 seconds, there is a higher chance of eviction.

(引用元)Limits - Cloudflare Workers docs

試してみましょう。先ほどのコードを使って60秒間スリープさせてみます。

$ curl https://hello.example.workers.dev?duration=60000

以下のようにタイムアウトした旨のメッセージが表示されました。

$ curl https://hello.example.workers.dev?duration=60000
<html>
<head><title>504 Gateway Time-out</title></head>
<body>
...

なお、上記のメッセージが表示されたのはcurlコマンドを実行してから90秒ほどが経過した時点でした。60秒のスリープから復帰した時点ですぐにタイムアウトされるわけではありません。また、30秒の制限に到達した瞬間にタイムアウトするわけでもありません。

おそらく、30秒の制限に達した段階でスケジューラーはWorkerの優先度を下げるのでしょう。その後、サーバーの混み具合などを考慮して適当なタイミングでタイムアウトした旨のレスポンスを返すようです。

メモリ容量の制限

CPU利用時間の制限とは異なり、メモリ容量128 MBの制限は工夫でどうにかする余地がありません。また、無料プランと有料プランでメモリ容量には差がないため、課金すれば解決する問題でもありません。

メモリの浪費を防ぐ工夫

メモリの制限については工夫の余地がないと説明しましたが、浪費を防ぐ工夫はできます。以下のコードをみてください。

export interface Env {
  // ...
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const a = new Uint8Array(100 * 1024 * 1024);
    const b = new Uint8Array(100 * 1024 * 1024);
    const c = new Uint8Array(100 * 1024 * 1024);

    return new Response("Done!");
  },
};

上記のコードは100 MBのUint8Arrayを3つ作成するため、合計300 MBのメモリを確保することになります。当然、メモリ容量の制限に達してエラーが発生します。

$ curl https://hello.example.workers.dev
<!DOCTYPE html>
<head>
<title>Worker exceeded resource limits | memory.example.workers.dev | Cloudflare</title>
<meta charset="UTF-8" />
...

それでは、以下のコードはどうでしょうか?

export interface Env {
  // ...
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    for (let i = 0; i < 3; i++) {
      const a = new Uint8Array(100 * 1024 * 1024);
    }

    return new Response("Done!");
  },
};

実行してみると、エラーは発生しません。

$ curl https://hello.example.workers.dev
Done!

上記のコードはforループの中で100 MBのUint8Arrayを作成しています。ループは3回実行されるため、合計300 MBのメモリが確保されそうに見えます。

V8の実装を確認したわけではないので推測になりますが、各ループのスコープから抜けるたびに変数aのリファレンスカウントは0になるはずです。そうすると誰からも参照されていないのですから、その時点でメモリは未使用と見なされます。

GCに回収されるまで本当の意味でメモリが解放されたとは言えないかもしれませんが、使用中と見なされるメモリの容量が128 MBを下回っていれば処理は継続できるようです。

AWS LambdaやGCP Cloud Runとの比較

Cloudflare Workersと似たコンセプトのサービスとしてAWS LambdaやGCP Cloud Runがあります。これらのサービスとの違いを比較します。

コンテナ仮想化ではない

LambdaやCloud Runはコンテナ仮想化を利用しています。一方、JavaScriptランタイムのV8を実質的な仮想化ソフトウェアとして利用しているのがCloudflare Workersの特徴です。

Cloudflare Workersの場合はコンテナの起動を待つ必要がありません。従って、コールドスタートとホットスタートの概念はありません。言うならばCloudflare Workersは常にホットな状態です。

加えて、Cloudflare Workersは公式に「0 ms Cold Starts」をうたっています。V8による仮想化が軽量とはいえ0 msは誇張表現と思われるかもしれません。それを実現するための技術的な背景についてまとめられた記事がありましたので、参考にしてください。

厳しいリソース制限

LambdaやCloud Runは目的に応じて性能を選ぶことができます。また、ほとんどの場合、性能不足に陥ったとしても課金することで解決できます。

一方、Cloudflare WorkersはCPU利用時間とメモリ容量に厳しい制限が課されています。さらに、記事執筆時点ではメモリ容量128 MBの制限は無料プランと有料プランで同一です。いくら課金しても性能不足を解決できません。

Cloudflare Workersは軽いタスクを素早く終わらせるのに適しています。従って重量級のタスク、例えば画像処理の実行基盤としては不向きです。

この点はCloudflareも承知しているようです。例えば画像のリサイズと最適化を行うフルマネージドなサービスとしてCloudflare Imagesが提供されています。

少ない資源をやりくりするのは腕の見せ所です。しかし、要件にフィットするなら素直にマネージドサービスを利用する方がお手軽かつ安上がりかもしれません。

言語のサポート

LambdaやCloud Runはコンテナ仮想化を採用しています。そのため、カスタムイメージのコンテナさえビルドできれば利用する言語は問いません。広く利用されているかはともかく、RustやC++を選択することはできます。

一方、Cloudflare WorkersはランタイムにV8を採用しています。従って、基本的にはJavaScriptで実装することになります。あるいはTypeScriptなど、JavaScriptにトランスパイル可能な言語を選ぶことになります。

なお実験的な機能ではありますが、Cloudflare WorkersはWASI(Web Assembly System Interface)をサポートしているそうです。興味のある方はチュートリアルが公開されているので試してみてください。

ただし、WASIが正式にサポートされたとしても任意の言語が活かせるとは限りません。

例えば先ほどのThe Cloudflare Blogでは利用できる言語としてGoやSwiftが紹介されています。Goコンパイラは直接WASIバイナリを出力できますし、SwiftはLLVMを経由してWASIバイナリを出力できます。

しかし、Goの魅力は高速なランタイムにあります。SwiftであればApple製品をターゲットにした高効率なフレームワークが用意されているのが魅力です。Goの場合、コードはWASIランタイムで実行されることになるため、せっかくのGoランタイムが行かせません。Swiftの場合はAppleが提供しているフレームワークが利用できるとは限りません。

従って、WASIバイナリが出力できると言ってもメリットは任意の言語のシンタックスでコードが書けるだけになりかねません。そうすると、それぞれの言語が持つメリットが行かせません。

その点、RustはWASIとの相性が抜群です。強力な型システムに加えて、借用とライフタイムの概念によりメモリ安全なコードを実装できます。これあの言語機能はコンパイル時に適用されるため、ターゲットのアーキテクチャに依存しません。

まとめると、Cloudflare Workersの厳しいリソース制限を踏まえるとWASIが活用できる言語は限られます。とはいえ私自身はWASIに詳しくないため、今後状況は変化するかもしれません。

(追記)CDNとCloudflare Workersの関係

話が発散しそうなので意図的にCDNの説明は避けていたのですが、CDNとCloudflare Workersの関係について説明されている記事を発見しました。参考になる記事ですので、ぜひ読んでみてください。

おわりに

CloudflareのR2やD1が話題になっていることは知っていたのですが、Workersが何者かは知りませんでした。R2やD1を操作するための踏み台的なサービスなのかと想像していたのですが、調べてみるとWorkersそれ自体もかなり強力なサービスであることを知りました。今後、個人でなにかサービスを開発するときはプラットフォームとしてCloudflare Workersが第一候補になりそうです。

この記事がお役に立てたなら幸いです。もし内容や私の理解に誤りがある場合は知らせていただけると助かります。

ところで私は視覚に障害があるため、よく感じの変換ミスをします。音声で読み上げると気がつかないこともあるので、誤字脱字の指摘も歓迎です。

参考資料

  1. Get started guide - Cloudflare Workers docs
  2. Building a To-Do List with Workers and KV - The Cloudflare Blog
  3. Request - Cloudflare Workers docs
  4. Response - Cloudflare Workers docs
  5. How Workers works - Cloudflare Workers docs
  6. Cloudflare Global Network - Data Center Locations - Cloudflare
  7. Limits - Cloudflare Workers docs
  8. Pricing - Cloudflare Workers docs
  9. Eliminating cold starts with Cloudflare Workers - The Cloudflare Blog
  10. Announcing support for WASI on Cloudflare Workers - The Cloudflare Blog

参考文献

  1. ハンズオンWebAssembly - O'Reilly Japan
  2. JavaScript 第7版 O'Reilly Japan
  3. ハイパフォーマンスブラウザネットワーキング - O'Reilly Japan

Discussion

ログインするとコメントできます