天然パーマです。

CDNのエッジで実行する系が面白い

先日の Next.js Conf で Vercel は Next.js の新しいバージョン「12」をリリースした。 興味深いのは、Vercel は同時にEdge Functionsというサービスを開始したことだ。

謳い文句のひとつに

Push your functions to the edge

とあるように、「エッジ」で実行される「関数」を提供するプラットフォームである。 ここで言うエッジとはなにかというと、Vercel は明言していないが CDN のエッジのことだ。

Vercel の例のように「CDN のエッジで実行する系」が増えている。例えば以下の 7 つだ。

  • Cloudflare Workers
  • Fastly Compute@Edge
  • AWS CloudFront Functions
  • AWS Lambda@Edge
  • Deno Deploy
  • Vercel Edge Functions
  • Netlify Functions

これらを我々はどう使っていくか。今回は「CDN のエッジで実行する系」のユースケースについて考えていく。 また、いくつかのサービスを試してみたので、それぞれの違いも紹介できるだろう。

名称

と、その前に…

「CDN のエッジで実行する系」と呼ぶのもめんどくさいので、なにかいい名称がないかと考えた。 意味的には「Functions at the Edge」で「F@E」なのだが、「at=@」を使うあたりが「Compute@Edge」とか「Lambda@Edge」に似てるし、「Functions」を使っているサービスが多いので、それも混同してしまいそうだ。それに「F@E」はどうやって発音すればいいのだろうか!

細かいことは一旦おいておきつつ、今回は F@E という言葉を使います。

特徴

これらのサービスは、CDN で実行されるゆえ共通の特徴がある。

  • 短い」。スクリプトが実行される時間は短い。
  • 小さい」。スクリプトは小さくないといけない。
  • 速い」。CDN で実行されるので速い。

例えば、Cloudflare Workers は「0ms cold starts」を、Vercel Edge Functions は 「Goodbye, cold boots」を掲げている。いわゆる FaaS でありながら、限りなく 0ms に近いコールドスタートを実現するという。Fastly Compute@Edge の起動時間は「35.4 マイクロ秒」だ。その代わり、スクリプトは小さくなければいけない。AWS CloudFront Functions は 10KB 以内と制限がきつい。非常にやれることが多い Cloudflare Workers でも JavaScript の場合、ビルドしたコードが 1MB 以下という制限がある。そして、CDN に乗っているのでリクエスト・レスポンスを素早くクライアントとやり取りすることが出来る。

いわゆるサーバーレスであるが、こうした都合上、AWS の Lambda や Google の Firebase と比べない方がよいだろう。

ユースケース

それぞれのサービスが ユースケースを主張しているのだが、みんな同じようなこと言っている。

  • Basic 認証
  • ヘッダの書き換え
  • Geo IP による処理
  • Cookie のパース
  • A/B テスト
  • IP でのブロック
  • リダイレクト

「そんなこと簡単にできるじゃんw」と思ってしまうが、上記の通り、CDN のエッジで、限りなく短い時間でこれらを叶えられるのがミソである。

例を見てみよう。

設定よりコード

Next.js 12 には Middleware という機能がベータで追加された。説明にはこうある。

Middleware enables you to use code over configuration.

Code over configuration!!かっこいい!

これは、Basic 認証やリダレイクト処理など、Apache や Nginx だと設定ですることを Middleware としてコードで書こうというもの。面白いのは、Vercel でホストした場合、Edge Functions で Middleware が動くという点である。

試しに、GET リクエストのクエリパラムから値を抜き取り、カスタムレスポンスヘッダの値にセットして返す、というものを Middleware で書いてみた。

import { NextRequest, NextResponse } from "next/server"

export function middleware(req: NextRequest) {
  const message = req.nextUrl.searchParams.get("message") || " NOTHING "
  const res = NextResponse.next()
  res.headers.append("x-message", message)
  return res
}

他のページに関しては SSG でスタティックに吐き出すことにする。Vercel でビルドすると、出力はこうなった。

ミドルウェアが Edge Functions と認識されているではないか!試しにアクセスしてみると、ヘッダには値がしっかり値が入っている。CDN のキャッシュにもしっかりヒットしている。

Next.js だけに限らず Jamstack の構成では静的 HTML を CDN に置く。Apache や Nginx でやっていた「ちょっとしたこと」が出来なくなる。CDN とはそういうものだと思っていたが、F@E のおかげで、Jamstack の利点を損ねずに「ちょっとしたこと」が可能になりそうだ。

キャッシュのコントロール

現在、Cloudflare の CDN にコンテンツをのせているサイトがある。ほとんどの HTML ページを TTL 付きでキャシュしている。ここで困ったのが、デバイスの判定、つまりスマホか PC かの判定ができずないことだ。ページはそれぞれスマホ、PC 用と用意してあり、ユーザーエージェントを見て、切り替えるようにしていた。レスポンシブは使ってない。Cloudflare では Enterprise 版ではないとモバイルディテクトは利用できない。

さて、知恵を絞ったところ、Cloudflare Workers でやれた。 この場合、キャッシュキーにユーザーエージェントの情報を含めることができればいい。 Workers は CDN に対するリクエストを受け取り、バックエンドのキャッシュをひっぱってきて、レスポンスと返すことができる。 なので、Request ヘッダにあるユーザーエージェント情報から、スマホかどうかを判断。 これまで URL のみをキーにしていたが、それにデバイスの情報を付加。キャッシュキーにして、キャッシュがあればそれを返却。 なければ、オリジンからフェッチしてキャッシュにセットしてから返却する。

async function handleRequest(event) {
  const request = event.request

  let isMobile = false
  const userAgent = request.headers.get('User-Agent') || ''

  if (userAgent.match(/(iPhone|iPod|Android|Mobile)/)) {
    if (!userAgent.match(/(iPad|Table)/)) {
      isMobile = true
    }
  }

  const device = isMobile ? 'Mobile' : 'Desktop'
  const cacheKey = request.url + '-' + device;

  let response = await cache.match(cacheKey)

  if (!response) {
    // If not in cache, get it from origin
    response = await fetch(request)
    ...
  }
}

このようにそのまま JavaScript で書けるのだ!それでいて、Workers を挟んでも CDN からのレスポンスはほぼ変わらず速い!

VCL から Compute@Edge へ

キャッシュの管理という点で言えば、Fastly のアプローチが面白い。Fastly では Varnish をキャッシュのエンジンとして使っている。 Varnish は古来から VCL という独自の記法を使う。一方で Cumpute@Edge が使える。これは Rust か JavaScript で書く。Fastly をいじった当初は、これら 2 つは別物かと思ったが、どうもそうではないっぽい。なんと、Compute@Edge が VCL に置き換わるかもしれないのだ!事実、Fastly のドキュメントには「Migrate from VCL」というものがある。

例えば、クライアントからのリクエストヘッダに含まれるとあるキー・値をセットするには VCL でこう書く。

set req.http.Host = "example.com";

一方、JavaScript ではこうだ。

req.headers.set("Host", "example.com")

同じように書ける。そして、Compute@Edge ならば「VCL だとどう書くんだ??」っていう処理が JavaScript で書ける。

async function handleRequest(event) {
  const req = event.request

  const url = new URL(req.url)
  const message = url.searchParams.get("message") || "NO MESSAGE"

  const response = await fetch(req, {
    backend: "origin_0",
  })

  response.headers.append("x-message", message)

  return response
}

とはいえ、VCL の方が簡単にかけるケースもたくさんある。キャッシュを pass するのに VCL だと

return(pass);

で済むところを JavaScript だと 5 行かかる。

let cacheOverride = new CacheOverride("pass")
return fetch(req, {
  backend: "example_backend",
  cacheOverride,
})

実際のところ、Compute@Edge は「LIMITED AVAILABILITY」で、今すぐ VCL を置き換えるものではない。 確認していないだけで、VCL ではできて、Compute@Edge ではできないことがたぶんある。 だが、その時は近いかもしれない。 VCL を「設定ファイル」と見立てれば Fastly のケースも「Code over configuration」と呼べよう(とはいえ、VCL も Varnish Configuration Language である)。

ちなみに、Fastly。ドメインごとに設定をしていくわけだが、Compute@Edge が有効になっていると、 VCL で書くか、Compute@Edge で書くか、どちらかひとつを選ぶことになる。 一部を Compute@Edge で書く、とかできない。という意味でも、VCL は「置き換わる」のかもしれない。

Server Push

HTTP/2 の技術でアセットを先読みさせる Server Push も F@E でできる場合がある。Linkヘッダを付随するだけの場合はできる。

また、似たような目的で103 Early Hintsというテクニックがあって、それが Fastly に実装されている。ただ、今のところ VCL でのやり方しか分かってないないので気になるところだ。そういえば、Cloudflare もEarly Hintsをやりだす。Web アプリを作る身としては、HTTP のプロトコルに近い部分が CDN でやれると嬉しいのである。

Signed Exchange

Signed Exchange という仕組みがある。これが実現すれば、Google 検索でコンテンツが prefetch されて LCP が劇的に向上し、Google AMP キャッシュでもオリジンのドメインが表示されたりとなかなか熱い。ただ、リクエスト、レスポンスに対して署名を行う必要があり、なるべく自分で実装と運用をしたくない。

そこで、F@E で Signed Exchange をやっちゃおうというのがちょうど見つかった。Cloudflare Workers と Fastly Compute@Edge での実装例があった。

こういうのも CDN でやってもらうのが一番よい。ちなみに、Cloudflare では、Workers を使うまでもなく「Automatic Signed Exchanges」という機能をベータで提供し始めている。さすがだ。

それぞれの特徴

  • Cloudflare Workers
  • Fastly Compute@Edge
  • AWS CloudFront Functions
  • Deno Deploy
  • Vercel Edge Functions

を実際に動かしてみたので、その特徴を述べようとしたが、疲れてしまったのでまた今度。

まとめ

以上、「CDN のエッジで実行する系」を「Functions at Edge=F@E」と略して、そのユースケースについてまとめてみました。 CDN のメリットを活かしつつ、かゆいところに手が届く、といったところでしょうか。 まとめきれてなかったのですが、Cloudflare Wokers が Key-Value ストアを備えてそれなりにリッチなアプリケーションを作れる F@E である一方、AWS は「Lambda@Edge があるのにさらに簡素なもの」として CloudFront Functions を作りました。 同じようで特徴があるので、そのあたりを理解するのもいいでしょう。

この分野は面白いので、今後も追っていきたいです。

追記

F@E でService Worker APIがよく使われているという記事を書いたので、よかったら見てみてください。