Cloudflare Workers + Node.jsでマークダウン変換APIを動かす
Cloudflare WorkersでNode.jsを動かしてみたので、その手順を整理してまとめておきます(元のスクラップ)。例として、Node.js + markedでHTMLをマークダウンへと変換するAPIをCloudflare Workersにデプロイしてみます。
関連記事
事前準備
Cloudflareのアカウントを作成しておきましょう。
Wranglerをインストールする
Cloudflare Workersで関数のビルド・プレビュー・デプロイをするときにはWrangler(ラングラー)という公式の提供するCLIを使うことになります。ここではWranglerをグローバルインストールしておきます。
npmでインストールする場合
$ npm install -g @cloudflare/wrangler
:::message warning
2021/02/07時点ではM1 Mac + npmで@cloudflare/wrangler
をインストールすることができない(#1685)ので、cargoを使います。
:::
cargoでインストールする場合
cargoはRustのパッケージマネージャーです。cargoが入っていない場合はcurl https://sh.rustup.rs -sSf | sh
でインストールします。
$ cargo install wrangler
これでインストール完了です。
wrangler config
にアカウントを紐付ける
続いてCLIにアカウントを紐付けます。wrangler config
を実行すると、API Tokenを聞かれるので、Cloudflareのダッシュボードで取得したものを入力します。
API Tokenの取得について
Cloudflareのダッシュボードの「Managing API Tokens and Keys」というようなメニューから発行できます。
今回はCloudflare Workersを動かすので[Edit Cloudflare Workers]のテンプレートを選んでTokenを生成しました。
スターターを使ってCloudflare Workersのプロジェクトを作成
Cloudflare Workersにはたくさんのスターター(テンプレート)が用意されています。
今回はTypeScriptのスターターを使います。
$ wrangler generate my-app https://github.com/EverlastingBugstopper/worker-typescript-template
M1 Macでエラーが発生する場合
2021/02/06時点だとM1 Macでwrangler generateを実行すると以下のようなエラーが発生します。
Error: could not download `cargo-generate`
no prebuilt cargo-generate binaries are available for this platform
仕方がないのでスターターのGitHubリポジトリをcloneしてwrangler.toml
をプロジェクトのルートに配置します。その後、ドキュメントを参考にwrangler.toml
にCloudflareアカウントのaccount_id
などを指定しておきます。
これでwrangler generate
を実行したときと同じ構成になると思われます。
生成されたプロジェクトのルートにwrangler.toml
というものがあると思います。こちらにCloudflareのアカウントIDやデプロイしたときのWorkerの名前(URLにも含まれる文字列)を指定します。
Examplesを参考にしてWorkerを作る
公式のExamplesが充実しているため、困ったらまず同じようなことをやっているサンプルがないか探してみると良いでしょう。
今回とくに参考にしたのは以下のサンプルです。
基本的な書き方
Cloudflare WorkersではFetch Listener(addEventListener('fetch', (event) => {})
)というものでHTTPリクエストを受けることになります。
addEventListener("fetch", (event) => {
return event.respondWith(
new Response("Hello world")
)
})
引数のevent
にはヘッダー情報やパラメーターが含まれているほか、レスポンスを生成する際にも使います。
雑にCORS設定
ブラウザからリクエストを受けるAPIで、CORS設定が必要になる場合はプリフライトリクエスト(OPTIONS
リクエスト)に応じるようにします。今回は単純にマークダウンをHTMLに変換するだけのAPIなので、すべてのオリジンを許可してしまいます。
addEventListener('fetch', (event) => {
const request = event.request // requestオブジェクトもeventに含まれている
// リクエストメソッドがOPTIONSの場合はプリフライトリクエストとみなす
if (request.method === 'OPTIONS') {
return event.respondWith(
new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST,OPTIONS', // POSTリクエストを受け付ける
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Headers': 'Content-Type'
}
})
)
}
// ...あとで...
})
JSONを返す
Cloudflare WorkersでレスポンスとしてJSONを返すには以下のような書き方をします(ほぼ公式のサンプルの通り)。
addEventListener("fetch", event => {
const data = {
message: "Hello!"
}
const json = JSON.stringify(data)
return event.respondWith(
new Response(json, {
headers: {
"content-type": "application/json;charset=UTF-8"
}
})
)
})
POSTリクエストbodyを取得する
POSTリクエストのbodyはContent-Type
がapplication/json
の場合、request.json()
で取得できます(参考にしたサンプル)。
addEventListener("fetch", (event) => {
const request = event.request
const { markdown } = await request.json()
}
最終的な実装例
以下の処理を行うAPIの実装サンプルを載せておきます。
- POSTリクエストでbodyの
markdown
を受け取る - markedでマークダウンをHTMLへと変換
- jsonで
{ html: "変換後のHTML" }
を返す
import marked from "marked"
function handleOptions(request: Request) {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST,OPTIONS',
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Headers': 'Content-Type'
}
})
}
async function handlePost(request: Request): Promise<Response> {
if (!request.headers.get('content-type')?.includes('application/json')) {
return new Response('Invalid Content Type', {
status: 415,
})
}
const { markdown } = await request.json()
const html = marked(markdown) // マークダウンをHTMLに変換
const json = JSON.stringify({ html })
return new Response(json, {
headers: {
'content-type': 'application/json;charset=UTF-8',
},
})
}
addEventListener('fetch', (event) => {
const request = event.request
// プリフライトリクエストに応じる
if (request.method === 'OPTIONS') {
return event.respondWith(handleOptions(request))
}
// POSTリクエスト以外はエラーに
if (request.method !== 'POST') {
return event.respondWith(
new Response('Method Not Allowed', {
status: 405,
}),
)
}
event.respondWith(handlePost(request))
})
これで実装自体は完了です。
localhostで動かしてみる
下記のコマンドを実行するとlocalhostでサーバーが立ち上がり関数を試すことができます。
$ wrangler dev
# 👂 Listening on http://127.0.0.1:8787
デプロイする
下記のコマンドを実行すると、Cloudflare Workersにデプロイされて外部からリクエストを受け付けられるようになります。
$ wrangler publish
Cloudflare Workersを試してみた感想
レスポンスが速く、コールドスタートによる遅延のようなものがない(ように感じられる?)のが嬉しいです。今後ガンガン使っていこうと思います。
Discussion
今はNode.js側でmarkedを使って実行してるので多分同じマークダウンの量でもクライアントでやった方がhttpのオーバーヘッド分を超えるパフォーマンスが出るのかなと思ったりしてますが、これをwasmに変えてみたり、別の言語で書いてみたりしてどのぐらいでEdgeでやった方が早いみたいな分岐点が来るのか気になりますね👀
追記です
ユースケースを想定できてなかったのですが例えばgithubにあるmdをhtmlに変換するなどzennにありそうなケースの場合は、確かにCloud Functionでやってるからそこを
から解決したいみたいなモチベーションですね。
管理画面的なものとか、zenn.devでclient上でpreviewをしてる部分などの変換に使うのかと思ってコメントしました
違いないですね。そのあたりを今後いろいろと試していきたいです。
Node.jsやC/Kotolin/Dartなどの対応言語でしかできない処理を任せるのにはかなり良さそうです。これまでCloudFunctionsやAWS Lambdaに任せていたことを今後は積極的にCloudflare Workersでやっていきたい気持ちです。