Open32

Cloudflare Workers メモ

前提

Cloudflare Workers は知っていたが最近触って、本番環境へ導入済み。

  • 普段は Erlang/OTP を書いてるので JavaScript は専門外
  • 自社サービスの本番環境には Cloudflare Workers を導入済み
  • OpenResty はエンタープライズ大規模環境向けで設計/開発経験あり

雑感

Cloudflare Workers®

ざーっと見た感じ Nginx + Lua (OpenResty) のマネージド、さらに Edge で動かしてくれて、スケールも勝手にしてくれるバージョンという認識。実際 shared.dict / redis の代わりが Workers KV が利用できる。

さらにローカル開発環境が充実している、デプロイ後のログ確認も管理画面から簡単に利用できる。

Lua

Introducing Cloudflare Workers

これを読むと Lua の話も出てくる。

Luaです。Luaはすでにnginxに深く統合されており、まさに私たちが必要とするスクリプトフックを提供しています。実際、現在エッジで実行されている独自のビジネスロジックの多くはLuaで書かれています。さらに、Luaにはサンドボックスの機能も備わっています。しかし、実際には、Luaのサンドボックスとしてのセキュリティは限定的に精査されてきました。これまでは、Luaのサンドボックスの脱獄を見つけてもあまり価値がなかったからです。さらに、現在のウェブ開発者の間では、Luaはあまり広く知られていません。

Wranger

Wrangler 2.0 を利用する。

商用利用する場合

  • 何はともあれ課金

OpenResty との比較

Cloudflare Worekrs が使える環境であれば OpenResty を採用するメリットは無いレベル

OpenResty はとにかく LuaJIT が高速にうまいことやってくれるということだったが、 Cloudflare Workers も遅いわけではなさそうだし、そもそも Edge に寄せてくれて、自前で運用しなくていいのはスゴイ楽だ。

とにかくローカルでの開発が楽なのが良い。OpenResty はデバッグするのが凄い大変だが、Cloudflare Workers は console.log でさくっと出せる。

環境構築もよく考えられていて数分で Hello World まで試せる。

Durable Objects

課金プランのみで利用可能

複数の Worker からアクセスをしても一貫性を保証する仕組み。 Workers に WebSocket エンドポイントも同時に来た。

非モジュールの JavaScript 初期コード

addEventListener("fetch", (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  return new Response("Hello worker!", {
    headers: { "content-type": "text/plain" },
  });
}

手動ルーティング

https://developers.cloudflare.com/workers/runtime-apis/request#properties

event.request には url が入ってるのでそれを URL に食べさせる。

https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
const url = new URL(request.url)
// url.pathname を使う

Workers KV

How KV works · Cloudflare Workers docs の DeepL Pro 翻訳

KVの仕組み

Workers KVは、グローバルな低遅延のキーバリュー・データストアです。低レイテンシーで大容量の読み取りをサポートしているため、キャッシュされた静的ファイルと同じくらい迅速に応答する、高度にダイナミックなAPIやWebサイトを構築することができます。

Workers KVは、一般的に、書き込み頻度は比較的低く、読み取り頻度は高く、かつ高速であることが求められる用途に適しています。KVは、このような読み取り頻度の高いアプリケーションに最適化されており、データが頻繁に読み取られるときにのみ、その性能を最大限に発揮します。頻繁に読まれない値は集中的に保存され、人気のある値は世界中のCloudflareデータセンターで管理されます。

KVは、最終的に一貫性を保つことでこのパフォーマンスを実現しています。変更は、変更が行われたエッジロケーションではすぐに表示されますが、他のすべてのデータセンターに伝搬するには最大で60秒かかることがあります。特に、ある鍵の前のバージョンを最近読んだ場所では、変更の伝播に時間がかかります(その鍵が存在しないことを示す読み取りも含む)。Workers KVは、アトミックな操作をサポートする必要がある場合や、1つのトランザクションで値の読み取りと書き込みを行う必要がある場合には適していません。

すべての値は、256ビットのAES-GCMで暗号化され、Workerスクリプトを実行するプロセスや、APIリクエストに応答するプロセスでのみ復号化されます。

Workers KVは、無料でお試しいただけます。また、Workers Bundledプランの一部として、追加の利用が可能です。

注意点

  • アトミックやトランザクションには KV は使えず Durable Objects を使う

wranger kv:

KV を作るのは wrangler からできる。

$ wrangler kv:namespace create "TEST_KV"

kV の一覧取得

$ wrangler kv:namespace list

ローカルで試す場合は --preview をつける。

$ wrangler kv:namespace create --preview "TEST_KV"

ローカルでもグローバルでも使う場合は id と preview_id 。

kv_namespaces = [
     { binding = "TEST_KV", id = "...", preview_id = "..." }
]

wrangler dev

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  await KV_TEST.put("abc", "xyz")
  let value = await KV_TEST.get("abc")
  return new Response(value, {
    headers: { 'content-type': 'text/plain' },
  })
}

期限

https://developers.cloudflare.com/workers/runtime-apis/kv#expiring-keys の DeepL Pro 翻訳

キーの有効期限
Workers KVでは、一定期間しか有効でないキーを書くことがよくあります。このようなデータを適切なタイミングで削除することをアプリケーションに要求するのではなく、Workers KVは、特定の時点またはキーが最後に変更されてから一定の時間が経過した後に、自動的に失効するキーを作成する機能を提供します。

有効期限のある鍵は、その有効期限に達すると、システムから削除されます。削除された後、そのキーを読み取ろうとすると、そのキーが存在しないかのように動作し、課金目的のネームスペースのストレージ使用量にはカウントされません。

キーの有効期限を指定する方法には、次の2つがあります。

  • UNIX エポックからの秒数で指定した絶対時間を使用して、「有効期限」を設定する。例えば、2019年4月1日の午前12時(UTC)に鍵の有効期限が切れるようにしたい場合は、鍵の有効期限を1554076800に設定します。

  • 現在の時刻からの相対的な秒数を使って、その「有効期限TTL」(time to live)を設定します。例えば、鍵を作成してから10分後に有効期限が切れるようにしたい場合は、「有効期限TTL」を600に設定します。

これらのオプションは、Workerの中で鍵を書くときや、APIを使って鍵を書くときに使用できます。

なお、60秒未満の未来の有効期限や60秒未満の有効期限TTLは、現時点ではサポートされていません。

期限付きキーの作成

上記ではputメソッドの基本的な形式について説明しましたが、この呼び出しにはオプションの第3パラメータもあります。このパラメータには、putの動作をカスタマイズするためのオプションのフィールドを持つオブジェクトが渡されます。特に、キーの有効期限をどのように指定したいかによって、expirationまたはexpirationTlを設定することができます。つまり、Worker内でキーを書き込む際に有効期限を設定するには、以下の2つのコマンドのいずれかを実行します。

  NAMESPACE.put(key, value, {expiration: secondsSinceEpoch}) 
  NAMESPACE.put(key, value, {expirationTtl: secondsFromNow})

これらは、secondsSinceEpoch と secondsFromNow が Worker のコードのどこかで定義された変数であることを前提としています。

Wrangler や API を使って、コマンドラインで expiration を指定することもできます。

Pages + Funcitons なのか Workers + Sites なのか

  • 静的ファイルが前提で workers がおまけ的な扱いなら Pages + Functions
  • Workers がメインで Remix などを利用するなら Workers がよい

基本的に Pages + Functions の Functions (Workers) はおまけ程度の扱いで良い。

メモ

  • 2021 年 12 月の時点では Cloudflare Pages + Functions は微妙
    • Wrangler ver2 が必要
    • ないと wrangler pages dev が使えない
  • Sites はわかりやすくて良い、Remix の Cloudflare Workers もシンプルでイイ
    • wrangler generate --site で作ることができる
    • site で指定した ./public 部分に static ファイルを置く事になる

ミドルウェア屋からみた場合

  • OpenResty の代わりの便利ツール
  • ウェブアプリで複数箇所にリクエスト投げたりできるのは楽
  • fetch / Request / Response API は本当によくできてる

使い方

正直なんにでも使えるので、とりあえずはすべてのリクエストは Cloudflare Workers に飛ばしてしまえばいいと思う。

  • Remix のようにクライアント/サーバー(Workers) と External API (Go + sqlc) などで使う
  • レンダリングサーバとしての Workers
  • データベースからのデータを External API から取得して、Workers でレンダリングしてクライアントに返す
  • Proxy としての利用方法

設定と環境変数

この設定は legacy_env であり、今後は利用が推奨されないです

Cloudflare Workers を利用する場合は設定と環境変数がとても重要になる。商用環境に突っ込む場合はこれをしっかり理解するのが良い。

複数人で開発する際も重要になる。miniflare を使わない場合は wrangler dev はローカルでは無く実際にはCloudflare 側にデプロイされる。開発環境用の設定は各自が持っておくべきだろうし、stating も色々考慮する必要がある。

設定

Configuration · Cloudflare Workers docs

  • [dev] は wrangler dev 時に呼ばれる。key/name で定義可能
  • [env.production] は wrangler publish -e production 時に利用される
    • production などの名前は好きに指定して良い

dev

[dev] は開発向けの設定を入れられる。

  • ip
    • wrangler dev 時の ip を設定できる
    • デフォルトは 127.0.0.1
  • port
    • wrangler dev 時の listen port を指定できる
    • デフォルトは 8787
  • local_protocol
    • wrangler dev 時の server listen 時のプロトコルを指定できる
    • デフォルトは http
  • upstream_protocol
    • wrangler dev 時の server listen 時の転送プロトコルを指定できる
    • デフォルトは https
[dev]
port = 9000
local_protocol = "https"

build

  • command
    • wrangler build 時に実行するコマンド
  • cwd
    • wrangler build 時の実行する際のディレクトリ
    • デフォルトはプロジェクトの root
  • watch_dir
    • wrangler dev 時に監視するディレクトリ
    • デフォルトは src
  • [build.upload]
    • format は service-worker または module のどちらか
      • format = "service-worker"
      • format = "module"
    • format が service-worker の場合は main を指定しない
    • format が module の場合は main を ./worker.mjs のように指定する

環境

Environments · Cloudflare Workers docs

env の後ろに指定可能。 publish 時に -e で指定することでデプロイ先を変更可能。

[env.dev]
name = "my-worker-dev"

[env.staging]
name = "my-worker-staging"

[env.production]
name = "my-worker-production"

環境変数

Environment variables · Cloudflare Workers docs

worker から直接呼び出すことができる。ABC と定義があれば worker から ABC で呼び出せる。

vars で指定できる。 toml では [vars] で指定して KEY = VALUE で書くか、 vars = { KEY = VALUE } で書くかのどちらか。

vars = { KEY = VALUE }

[vars]
KEY = VALUE

それぞれの環境でも定義できる。もちろん [env.dev] に vars = { KEY = VALUE } で書いても良い。

[env.dev]
[env.dev.vars]

[env.staging]
[env.staging.vars]

[env.production]
[env.production.vars]

以下のようにも書ける。

[env.dev]
vars = { KEY = VALUE }

[env.staging]
vars = { KEY = VALUE }

[env.production]
vars = { KEY = VALUE }

設定戦略

検討中

  • wrangler publish 時にその個人の開発環境にデプロイされるで良い
  • トップレベルには個人の設定をする

main

TypeScript を利用する場合は main = で指定すれば十分です。

main = "src/index.ts"

package.json は以下のようになります

  "scripts": {
    "dev": "wrangler dev --local",
    "publish": "cross-env NODE_ENV=production wrangler publish"
  },

読んだ資料

利用できる API がかなり少ない事もあり、勉強する範囲はとても少ない。
Cloudflare Workers 固有の挙動を覚えるのが凄く重要になる。

ブログ

お勧め

ESModules

Service Worker 形式から ESModules 形式へ切り替えることができるようになった。

最小

worker.mjs

export default {
  async fetch(request, environment, context) {
    return new Response("I’m a module!");
  },
  async scheduled(controller, environment, context) {
    // await doATask();
  }
}

2番目のパラメータは、環境変数を含むオブジェクトです(「バインディング」とも呼ばれます)。以前は、各変数はWorkerのグローバルスコープに挿入されていました。単純なソリューションではありますが、コードに変数を魔法のように表示させるのは混乱を招きます。これで、環境オブジェクトを使用して、環境変数にアクセスするモジュールとライブラリを制御できます。このメカニズムは、欠陥のある、または不要な挙動をするサードパーティライブラリがすべての変数または機密を列挙するのを防止することができるため、より安全です。

https://blog.cloudflare.com/ja-jp/workers-javascript-modules-ja-jp/

マイグレーションの利点

一部翻訳

  • Durable Objectは、モジュール構文を必要とします
  • モジュールワーカーは、グローバルバインディングに依存しないため、ワーカーのランタイムが新たな実行コンテキストを設定する必要がなく、モジュールワーカーをより安全かつ高速に実行できます。
  • モジュールワーカーは ES モジュールなので、例えば npm で共有したり公開したりすることができます。モジュールワーカーは、他のモジュールワーカーからインポートしたり、他のモジュールワーカー内で構成することができます

注意点

  • KV などがグローバルから呼べないため environment を利用する必要がある

Cloudflare Workers の route でサブサブドメインを割り当てる

a.b.example.com これを実現する場合はビジネスプランに入るか 月 $10 のプランにはいって、
証明書を追加で発行できるようにする必要があります。自分で用意してもいいですが、せっかく Cloudflare 使っているのであれば $10 で解決すべきです。

Image from Gyazo

route で使うドメインを Cloudflare DNS で設定する場合

  • A レコードに x.example.com と定義し IP は 192.0.2.1 に設定
  • AAAA レコードに x.example.com と定義し IP は 100:: に設定

https://developers.cloudflare.com/workers/platform/routes

以下 DeepL Pro による翻訳。

ルートがホスト名に設定されている場合は、ホスト名が外部で解決できるように、CloudflareにDNSレコードを追加する必要があります。Workerがオリジンとして動作する場合(つまり、リクエストがWorkerで終了する場合)、DNSレコードを追加する必要があります。
100::を指すプレースホルダーAAAAレコードを入力することができますが、これはCloudflare(DNS設定のorange-cloud)を介してプロキシされる必要があります。この値は、特に予約されたIPv6廃棄用プレフィックスですが、許可される唯一の値ではありません。例えば、192.0.2.1を指すAレコードや、解決可能な任意のターゲットを指すCNAMEを使用することもできます。

Cron Triggers を使う

https://developers.cloudflare.com/workers/platform/cron-triggers

index.mts

悩むことはなく、気軽に書ける。戻りは Promise<voide> で。

export default {
  async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
    ctx.waitUntil(triggerEvent(controller, env))
  },
}

async function triggerEvent(controller: ScheduledController, env: Env): Promise<void> {
  console.log("cron processed", controller.scheduledTime);
}

wrangler.toml

cron そのままなので、悩む事無し。

[triggers]
# https://crontab.guru/
crons = ["*/10 * * * *"]

miniflare + cron trigger

https://miniflare.dev/core/scheduled

強制実行できる。

curl "http://localhost:8787/cdn-cgi/mf/scheduled"

Logger

Cloudflare Workers のログは外部に記録する必要がある。開発中は Cloudflare のサイトか wrangler で見ることができる。エラーログは sentry に記録するのが無難。

Sentry

worker 向け logger

pages だと Pages Plugin Sentry がある

Sentry · Cloudflare Pages docs

サービス

新しい環境

今は wrangler.toml に [env.production] などを書いて分けていますが、同じ名前でサブドメインを追加するというかたちで利用できるようになります。これはサービスという概念が追加され、その中に環境が用意されます。

イメージとしては my-worker.voluntas.workers.dev というサービスで stating という環境を作った場合 staging.my-worker.voluntas.workers.dev が作られます。

サービスの環境自体がコピーされるため、1からセットアップする必要はありません。ただ今はダッシュボードからしか利用できません。今後リリース予定の wrangler v2 でコマンドや設定から利用できるようになる模様です。

これが使えるようになると [env.production] と [env.staging] で name が異なる仕組みを作らなくて良くなるので、待ち遠しい機能です。

プロモート

環境から環境へプロモートできるため、staging で検証した環境を production 環境へプロモートしてデプロイできるようになりました。切り替えが簡単なのは本当に良いです。

バージョン管理

環境はバージョン管理されるため、いつでも好きな状態に戻すことができるようになります。さらに誰が何を変更したのかを記録するため「この変更はいつ誰がやったのか」を記録できるようになります。

サービスバインディング

サービスバインディングが追加され、サービスからサービスへ気軽に連携をすることができるようになります。なにより使い方が fetch というのが素晴らしく、さらにネットワークは発生しないという夢のような仕組み。

参考

サービスバインディング 翻訳

以下は Introducing Services: Build Composable, Distributed Applications on Cloudflare Workers のサービスバインディング部分の DeepL Pro の翻訳です。

サービス同士の会話が可能

サービスはコンポーザブルであり、あるサービスが別のサービスと会話できるようになっています。これをサポートするために、私たちはサービス間のコミュニケーションを促進する新しいAPI、サービスバインディングを導入します。

サービスバインディングで広がるコンポーザビリティの世界
サービスバインディングを使うと、インターネットを経由せずに、別のサービスにHTTPリクエストを送ることができます。つまり、自分のコードから直接他のWorkerを呼び出すことができるのです。サービスバインディングは、コンポーザビリティの新しい世界を開きます。以下の例では、リクエストは認証サービスによって検証されます。

export default {
  async fetch(request, environment) {
    const response = await environment.AUTH.fetch(request);
    if (response.status !== 200) {
      return response;
    }
    return new Response("Authenticated!");
  }
}

サービスバインディングによる懸念事項の分離

サービスバインディングは標準のフェッチ API を使用しているため、既存のユーティリティやライブラリを引き続き使用することができます。また、サービスバインディングの環境を変更することもできるので、サービスの新バージョンをテストすることもできます。次の例では、リクエストの1%がサービスの「カナリア」デプロイメントにルーティングされます。カナリア」へのリクエストが失敗すると、本番用のデプロイメントに送られ、再度チャンスが与えられます。

export default {
  canRetry(request) {
    return request.method === "GET" || request.method === "HEAD";
  },
  async fetch(request, environment) {
    if (Math.random() < 0.01) {
      const response = await environment.CANARY.fetch(request.clone());
      if (response.status < 500 || !canRetry(request)) {
        return response;
      }
    }
    return environment.PRODUCTION.fetch(request);
  }
}

サービス間のインターフェースはHTTPですが、ネットワークはそうではありません。実は、ネットワークは存在しないのです。一般的な「マイクロサービスアーキテクチャ」では、サービスがネットワークを介して通信するため、遅延や中断の影響を受けることがありますが、サービスバインディングはゼロコストで抽象化されています。サービスをデプロイする際には、そのサービスバインディングの依存関係グラフを作成し、それらのサービスをすべて1つのデプロイメントにパッケージ化します。あるサービスが別のサービスを呼び出すとき、ネットワークの遅延はなく、リクエストは直ちに実行されます。

従来のサービス指向アーキテクチャとサービスバインディングの比較
このゼロコストモデルにより、遅延やパフォーマンスを犠牲にすることなく、チームが組織内でコードを共有・再利用できるようになります。複雑なYAMLテンプレートやサービスを編成するための指数関数的なバックオフの時代は終わりました - コードを書くだけで、すべてをつなぎ合わせます。

GitHub Actions

公式が用意してくれているのを使えば良い。

cloudflare/wrangler-action: 🧙‍♀️ zero-config cloudflare workers application deployment using wrangler and github actions

CF_API_TOKEN と CF_ACCOUNT_ID を作ったらあとは実行するだけ。自動デプロイは怖い人は手動デプロイがオススメ。

まだ legacy env を使ってるので --env production をつけている。以下は例。

on: [ workflow_dispatch ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - uses: actions/checkout@v3
      - name: Publish
        uses: cloudflare/wrangler-action@2.0.0
        with:
          accountId: ${{ secrets.CF_ACCOUNT_ID }}
          apiToken: ${{ secrets.CF_API_TOKEN }}
          workingDirectory: 'subfoldername'
          command: publish --env production

Slack 通知系 Actions を利用してデプロイしたことを通知するのをオススメしたい。
Slack 公式もあるが自分は rtCamp/action-slack-notify: GitHub Action for sending a notification to a Slack channel を利用している。

作成者以外のコメントは許可されていません