Cloudflare Workers からツイートする
#tech
Cloudflare Workers で動く、TwitterのAPIを叩くSlackのコマンドを実装してた話。
以前に twit と bolt を使ってHerokuで動くものを作ったが、コールドスタートで困っていたので作り直した。
Node.jsのモジュールのpolyfillが不要でWebWorkerでも動くTwitterのライブラリがなさそうだったので、そこから実装した。
後はドキュメントを見ながらビルド環境を整備し、PRを作ってとりあえず動くようにした。
https://github.com/ahuglajbclajep/slack-tweet-command/tree/11e7f6b
https://github.com/ahuglajbclajep/slack-tweet-command/pull/1
https://github.com/ahuglajbclajep/slack-tweet-command/pull/2
https://github.com/ahuglajbclajep/slack-tweet-command/pull/3
所感
azuさんもブログに書いていたように、WebWorkerでも動くライブラリの選定がとにかく難しい。
ブラウザで動くと書いてあっても、window に依存していたりNode.jsのモジュールのpolyfillが必要だったりがよくある。
(polyfillはやれば動くと思うが、webpack@5 では自分で設定する必要があるしあまりメンテナンスされていない。)
とにかく、zero-dependencyなライブラリを頑張って探すかビルドを工夫しましょうという話になって基本的に大変。
wrangler コマンド自体も発展途上で、デフォルトだと webpack@4 でのビルドが強制されるなど癖が強い。
https://efcl.info/2021/03/12/next.js-vercel-cloudflare-workers-kv/
https://developers.cloudflare.com/workers/platform/limits#worker-limits
https://webpack.js.org/blog/2020-10-10-webpack-5-release/#automatic-nodejs-polyfills-removed
https://github.com/webpack/node-libs-browser
https://github.com/cloudflare/wrangler/issues/1836
https://twitter.com/ahuglajbclajep/status/1380822043487203328
ただコールドスタートは本当に無なので、要件によってはかなり刺さる頼もしい技術ではあると思う。
また、Deno Deploy などのAPI互換な環境の登場や、WorkerをHTTPサーバーの標準にしようという動きもあり、今後が楽しみ。
https://lealog.hateblo.jp/entry/2021/03/29/084429
https://zenn.dev/mizchi/scraps/8e09ff87540a40
https://workers.js.org
2021/4/17 追記
Cloudflare Workersの環境で動作するライブラリの一覧も発表され、ライブラリの選定が楽になった。
https://airtable.com/shrTR0QCusxZoCgiJ/tbloKKErinTfrIHsB
https://blog.cloudflare.com/node-js-support-cloudflare-workers/
また wrangler@1.16 で正式にカスタムビルドがサポートされ、webpack@5 が使えるようになった。
https://github.com/cloudflare/wrangler/releases/tag/v1.16.0
類似の技術について
https://mizchi.dev/202009122126-cloudflare-workers
https://efcl.info/2021/04/26/layer0/
AWS CloudFront Functions
https://aws.amazon.com/jp/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/
https://dev.classmethod.jp/articles/amazon-cloudfront-functions-release/
Fastly Compute@Edge
AWS Lambda@Edge
Vercel Edge Network
https://vercel.com/docs/edge-network/overview
内部的には AWS Lambda@Edge を使っている?
Netlify Edge Handlers
https://www.netlify.com/products/edge/edge-handlers/
内部的には AWS Lambda@Edge を使っている?
Fastly VCL
Varnish をベースにしているらしい
実装の様子
ここからは、実装するにあたって具体的にどこで困ったかについて雑に書いていく。
似たようなことをしたい人や wrangler でデプロイする手順を知りたい人向け。
TwitterのAPIを叩く
https://github.com/ahuglajbclajep/slack-tweet-command/pull/1/files
Twitterのライブラリはたくさんあるが、そこそこ使われておりブラウザでも動くと書いているのは twitter-lite だけ。
ただしtwitter-liteも1行目から require('crypto') しており、Node.jsのモジュールのpolyfillが必須だったりする。
(OAuthの部分は oauth-1.0a というゼロ依存でNode.jsや window にも非依存なライブラリを使っているので問題なし。)
ちなみに twitter-ng は ntwitter の派生で、ntwitterと twitter は node-twitter の派生で、node-twitterは twitter-node の派生。
「所感」でも書いたがNode.jsのモジュールをpolyfillするのは気が進まなかったので、最初はこれをforkして不要な部分を削り、 crypto など必須なモジュールだけAPI互換なライブラリや Web Crypto API による実装で置き換えようと考えていた。
https://www.npmtrends.com/twitter-node-vs-node-twitter-vs-ntwitter-vs-twitter-ng-vs-twitter-vs-twit-vs-node-twitter-api-vs-twitter-api-vs-easy-twitter-vs-twitter-lite
しかし実際には、crypto.createHmac() の代替である create-hmac が他のNode.jsのpolyfillに依存していたり、Web Crypto APIが非同期的で扱いにくかったりといった問題があり、twitter-liteを参考に簡単な実装を自分で書く形になった。
crypto 自体は、crypto-js を resolve: { fallback: { "crypto": false } } な環境で使うことで代替できる。
https://stackoverflow.com/questions/47329132/how-to-get-hmac-with-crypto-web-api
https://github.com/w3c/webcrypto/issues/167
https://github.com/brix/crypto-js/issues/326
https://webpack.js.org/configuration/resolve/#resolvefallback
ちなみに没になったWeb Crypto APIを使った crypto.createHmac(...) に相当するコードはこんな感じ。
code:js
// digest === crypto.createHmac('sha1', key).update(base_string).digest('base64')
const encoder = new TextEncoder();
const digest = await crypto.subtle
.importKey(
"raw",
encoder.encode(key),
{ name: "HMAC", hash: { name: "SHA-256" } },
false,
"sign", "verify"
)
.then((ckey) =>
crypto.subtle
.sign("HMAC", ckey, encoder.encode(base_string))
.then((signature) =>
btoa(String.fromCharCode(...new Uint8Array(signature)))
)
);
twitter-lite以外にはCloudflare Workersのチュートリアルなども参考にして実装した。
またこれはTwitterのAPIに特有の話だが、英数字と -._~ は以外は % でエンコードする必要がある。
encodeURIComponent() と new URLSearchParams() では !'()~ の扱いなどが違うので要注意。
https://developers.cloudflare.com/workers/tutorials/build-a-slackbot
https://developer.twitter.com/en/docs/authentication/oauth-1-0a/percent-encoding-parameters
https://github.com/draftbit/twitter-lite/blob/v1.1.0/twitter.js#L53-L61
http://var.blog.jp/archives/80569433.html
例えば new URLSearchParams() を使ったエンコードはこんな感じになる。
code:ts
const params = { status: "foo -._~%7E+*" };
const body = ${new URLSearchParams(params)}
.replace(/%7E/g, "~")
.replace(/\+/g, "%20")
.replace(/\*/g, "%2A");
console.log(body); // status=foo%20-._~%257E%2B%2A
デプロイする
https://github.com/ahuglajbclajep/slack-tweet-command/pull/2/files
https://github.com/ahuglajbclajep/slack-tweet-command/pull/3/files
Cloudflare Workersのガイドに公開までのおよその手順が載っているので、まずはそれを読むとよい。
TSで書く場合は、テンプレートと同様に ts-loader を挟み、@cloudflare/workers-types の設定もする。
wrangler secret put で変数を設定するなら、その変数の型を declare global { ... } で宣言しておく必要もある。
なお request.text() などの body を得るメソッドは、実装の関係で2度呼ぶと例外を吐くので気を付ける。
https://developers.cloudflare.com/workers/get-started/guide
https://developers.cloudflare.com/workers/cli-wrangler/commands
https://developers.cloudflare.com/workers/cli-wrangler/webpack
https://github.com/cloudflare/worker-typescript-template
https://github.com/cloudflare/workers-types
流れとしてはおよそ以下のような感じ。
code:sh
$ yarn add -D @cloudflare/wrangler typescript webpack{,-cli} ts-loader
$ npx wrangler init
# 実装を用意して webpack や wrangler の設定などをする #
$ npx wrangler login
$ npx wrangler whoami # wrangler.toml の account_id を埋める
$ npx wrangler publish
$ npx wrangler secret put TWITTER_CONSUMER_KEY
$ ...