Cloudflare Pages&Workers を使う(Vercel からの移行)
はじめに
Vercel にホスティングしているウェブサイトがあるが、商用利用することになったので使えなくなった
以下に記載があるが、無料の Hobby プランは非営利目的の使用に限定されている
Vercel の移行先候補としては、Netlify、Cloudflare、Firebase あたりを考えたが、以下の条件を満たしたい
- 無料
- SSG しているので、静的ファイルのホスティングができること
- サーバーレス関数が使えること
- Vercel の Serverless Functions を使ってランタイムで実行する POST 処理があるため
Firebase は Spark プラン(無料)だと、Cloud Functions が使えなくなった
よって、Blaze プラン(従量課金)にする必要があるのでパス(今の所、無料枠を超えることは無さそうだが、、)
Netlify は Netlify Functions があるので 1 番移行コストが低そう
しかし、無料枠だと日本国内の Edge サーバーが使えず、国外(シンガポールらしい)のサーバーを経由して配信されてしまうため、レイテンシーが発生してしまう
また、FaaS のパフォーマンス的な話では Vercel の Serverless Functions と同様に Netlify Functions も AWS Lambda なので、コールドスタートの課題もあるかなと思う
一応サイトパフォーマンスに関しては、Vercel と同等を維持したいので Netlify はパス
Lambda と Cloudflare Workers のパフォーマンスの話は以下に書いてある
Edge サーバーで動くスクリプトという点で、Cloudflare Workers は Lambda@Edge と近い機能になるが、パフォーマンス的には Cloudflare が優れているとのこと(Cloudflare のドキュメントということを考慮して見る必要があるが)
(でも CloudFront Functions ってのもあったような・・・)
よって、今回の移行プランは以下で進める
- Cloudflare Pages による静的サイトホスティング
- Cloudflare Workers を FaaS として使う
現状、Cloudflare Pages は Free プランでも「Unlimited bandwidth」らしい
ただし、Edge サーバーで動くという特性上、課金するしないに関係なく制限はきつい
Cloudflare Pages への移行
公式のマイグレーションガイドがある
以下から Cloudflare のアカウントを作成
メールアドレス認証後、Cloudflare と連携する GitHub リポジトリを選択する画面に遷移するので、ビルドコマンドなどを設定後、ビルド&デプロイが行われる
Production ブランチ(master
,main
)以外のブランチで GitHub に変更を push した場合、Preview 環境にデプロイされる
環境変数についても、Production 環境と Preview 環境で分けて登録できる
また、Cloudflare Access を使えば、Preview 環境にメールによるワンタイム PIN 認証をかけられる(Cloudflare Pages の設定画面から Access policy を enable にするだけで自動で構築される)
Cloudflare Pages で出たエラー
Nuxt.js を使っているが、以下のエラーが Chrome コンソール上に出て JS が上手く読み込めていないっぽい
Uncaught SyntaxError: Unexpected token '<'
また、以下の Warning も出ていた
preload 属性で読み込んでいる payload.js が window の load イベント後、使用されなかったと
Chrome Dev ツールで見ると、ちゃんと payload.js はダウンロードできるてるっぽいが、、
The resource https://.../payload.js was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally.
調べていると、以下の Issue を見つけて、全然分からないけど Nuxt のバージョンを2.14.1
から2.14.8
に上げたら解決した
ビルドが遅い
Vercel と比べるとビルドが倍くらい遅い(Vercel は 2 分 30 秒くらい、Cloudflare は 5 分くらい)
Cloudflare のビルドログを見てみると、ビルド自体は 2 分くらいで終わっているが、Initializing build environment という工程に 2 分以上かかってる
これ何やってるんだろう?
Cloudflare Workers への移行
管理画面での設定
Cloudflare の管理画面からHomeタブ -> Workers
を選択
サブドメインを設定する
プランを選択する
Wrangler(CLI)をセットアップ
Cloudflare Workers をデプロイする際は Wrangler という CLI を利用する
npm i @cloudflare/wrangler -g
brew install cloudflare-wrangler # Homebrewでも公開されている
Wrangler を直接叩くのは、Worker の動作確認をローカルから行いたい場合になると思う
プロダクション運用する場合は CI を構築した方が良いが、cloudflare/wrangler-actionという GitHub Action がある
次に Wrangler を認証する
以下から API トークンを発行
Edit Cloudflare Workers
というトークンテンプレートを使用
Account Resources
とZone Resources
は、自アカウントのみ許可
その他の設定は変更せず
wrangler config
発行した API トークンを入力する
次に Example の worker を生成する
今回は以下の Quickstarts から TS のテンプレートを選択
wrangler generate my-ts-project https://github.com/cloudflare/worker-typescript-template
以下のコマンドでサーバーが起動し、http://127.0.0.1:8787
で確認ができる
cd my-ts-project
wrangler dev --host example.com
wrangler.toml を以下に設定
name = "example"
type = "javascript"
workers_dev = true
compatibility_date = "2021-09-28"
[build]
command = "npm install && npm run build"
[build.upload]
format = "service-worker"
以下のコマンドで[tomlのname].[サブドメイン].workers.dev
にデプロイできる
wrangler publish
この Worker が公開され、外部からのリクエストが可能になる
また、カスタムドメインを設定しないと、workers.dev
という名前固定なので、検索エンジンのクローラーにインデックスされると、site:workers.dev
で検索結果に表示される可能性がある(と思う)
Worker の実装
今回は、Worker で Chatwork にメッセージを送る API を試しに実装し、デプロイする
実装例が以下にまとまっていて、POST リクエストを処理する方法や CORS 設定など参考になった
以下が Service Worker のエントリーポイントとなる TS
import { handleChatwork, handleOptions } from '@/handler'
addEventListener('fetch', e => {
if (e.request.method === 'OPTIONS') {
return e.respondWith(handleOptions())
}
const { pathname } = new URL(e.request.url)
if (pathname.startsWith('/api/chatwork')) {
return e.respondWith(handleChatwork(e.request))
}
return e.respondWith(new Response('Not Found', { status: 404 }))
})
fetch イベントでリクエストを受け付け、関数を実行する
pathname で分岐しているが、今後たとえばメールの POST も作りたいってなった時にif (pathname.startsWith('/api/mail')) {
という分岐を追加するだけで対応できるようにした
handleOptions はこの Worker を利用するアプリケーションが POST で JSON を送っている(Content-Type: application/json
)ので、Preflight request に対応した
以下は index.ts から呼ばれる処理のメインとなるハンドラー
/* global CHATWORK_API_URL,CHATWORK_API_TOKEN */
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://...',
'Access-Control-Allow-Methods': 'POST,OPTIONS',
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Headers': 'Content-Type'
}
export const handleOptions = (): Response => new Response(null, { headers: corsHeaders })
export const handleChatwork = async (request: Request): Promise<Response> =>
fetch(CHATWORK_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-ChatWorkToken': CHATWORK_API_TOKEN
},
body: `body=${JSON.stringify(await request.json(), null, '\t')}&self_unread=1`
})
.then(res => {
if (res.ok)
return new Response('/api/chatwork send message success', { status: 200, headers: corsHeaders })
throw new Error(res.statusText)
})
.catch(
(err: Error) =>
new Response(`/api/chatwork send message error: ${err.message}`, { status: 500, headers: corsHeaders })
)
CHATWORK_API_URL,CHATWORK_API_TOKEN は環境変数になる
Cloudflare Workers は Node.js ではないので、process.env.CHATWORK_API_URL
的なのが使えない
よって、Cloudflare Workers の構文では、環境変数の変数名を直接コードに記載する
当然、TS や ESLint を使っているとエラーで怒られるので、以下で回避している
-
TS エラーに関しては以下の型定義を追加
@types/worker.d.tsdeclare const CHATWORK_API_URL: string declare const CHATWORK_API_TOKEN: string
-
ESLint エラーに関しては、グローバル変数化
/* global CHATWORK_API_URL,CHATWORK_API_TOKEN */
グローバル変数化するより、単純に ESLint エラーを ignore した方が安全かも
また、上記のような handler をローカルで動作確認する場合、wrangler dev
だと少々面倒なので、wrangler preview
で確認すると良い(watch オプションとか付いてるし)
webpack
Cloudflare Workers を実装する上で、AWS Lambda などと比べると開発体験は非常に良かったが、webpack を自分で書く必要があった
今回は ts-loader を使う程度だったが、この辺のビルド周りが隠蔽されるといいなーって思った(もしかしたら、wrangler.toml にオプションが色々あるのかもしれない)
const path = require('path')
module.exports = {
mode: 'development',
entry: path.join(__dirname, 'src', 'index.ts'),
output: {
filename: 'worker.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: 'ts-loader',
options: { transpileOnly: true }
}
],
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.js', '.ts'],
alias: { '@': path.join(__dirname, 'src') }
},
devtool: 'cheap-module-source-map'
}
Worker のデプロイを CI で実行
Vercel だと/api
ディレクトリに JS か TS を置いておけば、Vercel 側で勝手に Serverless Functions にデプロイしてくれて使えるようにしてくれていたが、Cloudflare Workers だと自分でデプロイする必要がある
デプロイ自体はwrangler publish
だけでいけるが、CI にやらせたい
公式のcloudflare/wrangler-actionという GitHub Actions があるのでこれを使う
ただ、wrangler publish
するだけなので、使わなくても実装は簡単だと思う
name: Wrangler
on:
push:
branches:
- 'master'
jobs:
deploy:
name: Run deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Publish
uses: cloudflare/wrangler-action@1.3.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
workingDirectory: 'dev/worker' # ルートにwrangler.tomlを置いていない場合、サブディレクトリを指定
Discussion