新規投稿のお知らせを受信されたい方は、サブスクリプションをご登録ください:

Cloudflare Workersのご紹介:エッジでJavaScript Service Workersを実行する

2017-09-29

9分で読了
この投稿はEnglishでも表示されます。

更新 2018/3/13: Cloudflare Workersが、誰でも利用できるようになりました。

TL;DR: Service Workersに似たAPIで書かれたJavaScriptが、近いうちにCloudflareのエッジ環境にデプロイできるようになります。

playgroundでWorkerを記述してみましょう »

はじめに

すべての技術は、ある程度複雑化が進むと、プログラム可能になります。

いたるところで見られる光景ですが、生粋のゲーマーである私が個人的に気に入っている例は、グラフィックカードです。90年代のグラフィックスハードウェアは、決まった機能セットを提供するのが一般的でした。OpenGLの規格では、ジオメトリパイプラインが3D空間の点をビューポートに投影し、ラスタパイプラインが点と点の間に三角形を描き、グラデーションシェーディングと、テクスチャを適用することになっていました。一度に使用できるテクスチャは1つだけです。ただ一つのライティングアルゴリズムがあっただけで、それによってあらゆる面がプラスチックのように見えていました。他に何かやりたいことがあれば、ハードウェアを完全にあきらめて、ソフトウェアに戻らなければならないこともよくありました。

もちろん、新しいアルゴリズムや技術は常に開発されていました。そこで、ハードウェアベンダーは、最良のアイデアを「拡張機能」として自社のハードウェアに追加していました。OpenGLには最終的に、マルチテクスチャリング、バンプマップ、リフレクション、ダイナミックシャドウなどをサポートするために、何百ものベンダー独自の拡張機能が追加されました。

そして2001年、すべてが変わりました。プログラム可能なシェーディング(陰影計算)パイプラインを搭載した初のGPUが登場したのです。ハードウェア上で直接動作する小さなプログラムを書き、各頂点やピクセルを任意の方法で処理できるようになったのです。ユーザーはリアルな肌感を表現するためのアルゴリズムや、トゥーンシェーディングを表現するためのアルゴリズムなど、さまざまな実験が可能になったのです。

Cloudflareも同じような変遷を遂げようとしています。Cloudflareは、最も基本的なレベルでは、世界117箇所(現在も増加中)で稼働するHTTPキャッシュです。HTTP規格では、HTTPキャッシュの機能セットが定義されています。もちろんCloudflareは、DNSやSSLを提供したり、攻撃からサイトを保護したり、オリジンサーバー間の負荷分散をしたり、それ以外にも多くの機能を持っています。

しかし、これらはすべて固定された機能です。もし、カスタムアフィニティアルゴリズムで負荷分散をしたいとしたら?標準のHTTPキャッシュルールでは不十分で、キャッシュヒット率を高めるためのカスタムロジックが必要だとしたら?アプリケーションに合わせたWAFのカスタム規則を書きたいとしたらどうでしょう?

コードを書く

永遠に機能を追加し続けることはできますが、この方法ではすべての可能なユースケースをカバーすることはできません。その代わりとして、当社ではCloudflareのエッジネットワークをプログラム可能にしています。当社は世界の117以上の場所にサーバーを提供していますが、それをどう使うかはお客様が決めてください。

もちろん、数百の拠点を保有し数百万人のお客様がいる場合、従来のソフトウェアホスティングの方法ではうまくいきません。すべてのお客様に、それぞれの拠点で専用の仮想マシン、あるいは専用のコンテナを提供することはできないのです。もっとスケーラブルで、開発者が管理しやすいものが必要です。もちろん、セキュリティの問題もあります。Cloudflareにデプロイされたコードが当社のネットワークにダメージを与えたり、他のお客様に悪影響を与えることができないようにしなければなりません。

様々な可能性を検討した結果、現在のウェブ上で最もユビキタスな言語に落ち着きました。JavaScriptです。

Google Chromeのために開発されたJavaScriptエンジン「V8」を使用してJavaScriptを実行しています。つまり、Chromeが複数のウェブサイトからのスクリプトを実行するのと同じように、複数のお客様からのスクリプトを当社のサーバーで安全に実行することができるのです。(もちろん、この上に当社独自のサンドボックス層をいくつか追加しています)。

しかし、このJavaScriptはどのようなAPIに対して書かれているのでしょうか?そこで私たちは、Web標準、特にService Worker APIに注目しました。Service Workerとは、最近のブラウザに実装されている機能で、スクリプトをロードして、お客様のサーバに対するWebリクエストがネットワークに到達する前にインターセプトでき、リクエストを書き換えたり、リダイレクトしたり、あるいは直接応答したりすることができます。Service Workerはブラウザで動作するように設計されていましたが、Service Worker APIは私たちがエッジ環境でサポートしたかったものに最適であることがわかりました。Service Workerを書いたことがある人は、Cloudflare Service Workerの書き方も既にご存じでしょう。

外観の特徴

ここでは、Cloudflareで実行するService Workersの例を紹介します。

覚えておいていただきたいことは、これらは標準的な Service Workers APIに対して書かれているということです。唯一の違いは、ブラウザ内ではなく、Cloudflareのエッジ環境で実行されることです。

ここでは、Cookieヘッダを持つリクエストに対してキャッシュをスキップするワーカーを紹介します(ユーザがログインしているためなどの理由で)。もちろん、現実のサイトではもっと複雑なキャッシュの条件になると思いますが、これはコードなので何でもできます。

ここでは、サイト全体を検索して「Worker」という単語を「Minion」に置換するワーカーを紹介します。 このブログ記事で試してみてください。

// A Service Worker which skips cache if the request contains
// a cookie.
addEventListener('fetch', event => {
  let request = event.request
  if (request.headers.has('Cookie')) {
    // Cookie present. Add Cache-Control: no-cache.
    let newHeaders = new Headers(request.headers)
    newHeaders.set('Cache-Control', 'no-cache')
    event.respondWith(fetch(request, {headers: newHeaders}))
  }

  // Use default behavior.
  return
})

ここでは、二重丸括弧で囲まれた URL をページコンテンツから検索し、それらのURLを取得して、ページに代入するワーカーを紹介します。これは、「Edge Side Includes」のようなものをサポートする、一種のプリミティブテンプレートエンジンを実装しています。

// A Service Worker which replaces the word "Worker" with
// "Minion" in all site content.
addEventListener("fetch", event => {
  event.respondWith(fetchAndReplace(event.request))
})

async function fetchAndReplace(request) {
  // Fetch from origin server.
  let response = await fetch(request)

  // Make sure we only modify text, not images.
  let type = response.headers.get("Content-Type") || ""
  if (!type.startsWith("text/")) {
    // Not text. Don't modify.
    return response
  }

  // Read response body.
  let text = await response.text()

  // Modify it.
  let modified = text.replace(
      /Worker/g, "Minion")

  // Return modified response.
  return new Response(modified, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  })
}

自分で遊んでみよう!

// A Service Worker which replaces {{URL}} with the contents of
// the URL. (A simplified version of "Edge Side Includes".)
addEventListener("fetch", event => {
  event.respondWith(fetchAndInclude(event.request))
})

async function fetchAndInclude(request) {
  // Fetch from origin server.
  let response = await fetch(request)

  // Make sure we only modify text, not images.
  let type = response.headers.get("Content-Type") || ""
  if (!type.startsWith("text/")) {
    // Not text. Don't modify.
    return response
  }

  // Read response body.
  let text = await response.text()

  // Search for instances of {{URL}}.
  let regexp = /{{([^}]*)}}/g
  let parts = []
  let pos = 0
  let match
  while (match = regexp.exec(text)) {
    let url = new URL(match[1], request.url)
    parts.push({
      before: text.slice(pos, match.index),
      // Start asynchronous fetch of this URL.
      promise: fetch(url.toString())
          .then((response) => response.text())
    })
    pos = regexp.lastIndex
  }

  // Now that we've started all the subrequests,
  // wait for each and collect the text.
  let chunks = []
  for (let part of parts) {
    chunks.push(part.before)
    // Wait for the async fetch from earlier to complete.
    chunks.push(await part.promise)
  }
  chunks.push(text.slice(pos))
  // Concatenate all text and return.
  return new Response(chunks.join(""), {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  })
}

cloudflareworkers.comに、ご自身でスクリプトを書いてサイトに適用してみることができるCloudflare Workersのplaygroundを作りました。

今すぐ試してみる »

質問と回答

これは「Cloudflare Workers」または「Cloudflare Service Workers」ですか?

「Cloudflare Worker」は、お客様が書いたJavaScriptがCloudflareのエッジ環境の上で動作するものです。「Cloudflare Service Worker」は、特にHTTPトラフィックを処理するワーカーで、Service Worker APIに対して記述します。現在、当社が実装しているワーカーの種類はこれだけですが、将来的には特定の特殊なタスクのために他の種類のワーカーを導入する可能性もあります。

サービスワーカーはエッジ上で何ができますか?

何から何まですべて。コードを書いているわけですから、可能性は無限大です。あなたのService Workerは、お客様のドメインを宛先とするすべてのHTTPリクエストをインターセプトし、あらゆる有効なHTTPレスポンスを返すことができます。お客様のワーカーは、公開されているインターネット上の任意のサーバーに、発信するHTTPリクエストを行うことができます。

ここでは、CloudflareでのService Workersの使い方をいくつかご紹介します。

パフォーマンスを向上

  • カスタムロジックを使用して、どのリクエストがエッジでキャッシュ可能かを判断し、それらを正規化してキャッシュヒット率を向上させる。

  • HTMLテンプレートをエッジ上で直接展開し、サーバーから動的コンテンツのみをフェッチする。

  • オリジンサーバーに一切接続することなく、エッジから直接ステートレスなリクエストに対応する。

  • 1つのリクエストを、異なるサーバーへの複数の並列リクエストに分割し、そのレスポンスを結合する。

セキュリティを改善

  • カスタムのセキュリティルールやフィルターを実装する。

  • カスタムの認証・承認メカニズムを実装する。

信頼性の向上

  • 自分で管理しているサーバを更新することなく、高速な修正プログラムを数秒でサイトにデプロイする。

  • カスタムの負荷分散とフェイルオーバーロジックを実装する。

  • オリジンサーバーが到達できない場合に、動的に対応する。

しかし、これらは単なる例に過ぎません。Cloudflare Workersの本質は、私たちが考えもしなかったことができることにあります。

なぜJavaScriptなのですか?

Cloudflare WorkersはJavaScriptで書かれており、V8 JavaScriptエンジン(Google Chromeのもの)を使って実行されます。私たちがJavaScriptとV8を選んだ理由は、主に2つあります。

  • セキュリティ: V8 JavaScriptエンジンは、コンピューティングの歴史の中で最も精査されたコードサンドボックスと言っても過言ではなく、Chromeのセキュリティチームは世界でも有数の存在です。さらに、Googleは脆弱性の発見者に多額のバグ報奨金を支払っています。(とはいえ、私たちはV8の上にさらに独自のサンドボックスのレイヤーを追加しています。)

  • 普遍性: JavaScript はどこにでもあります。ウェブアプリケーションを構築する方であれば、すでにJavaScriptを知っています。一方で、サーバー側はさまざまな言語で書くことができますが、クライアント側はJavaScriptでなければなりません、なぜならブラウザはJavaScriptで動作するからです。

私たちは以下に挙げる、いくつかの他の可能性も検討しました。

  • Lua: Luaはすでにnginxに深く統合されており、まさに私たちが必要とするスクリプトフックを提供しています。実際、現在エッジで実行されている独自のビジネスロジックの多くはLuaで書かれています。さらに、Luaはサンドボックスの機能も備えています。しかし、これまでLuaのサンドボックスのブレイクアウトを見つけることにあまり価値が無く、実際にはLuaのサンドボックスとしてのセキュリティは限定的な精査しかされてきませんでした。これを選択した場合、急速に変化してしまうためトラブルに繋がるでしょう。さらに、現在のWeb開発者の間では、Luaはあまり広く知られていない、ということもあります。

  • Virtual machines: Virtual machinesは、もちろんサンドボックスとして広く利用され、精査されており、Webサービスのバックエンド開発者であればほとんどの方がすでに精通しています。しかし、Virtual machinesは重く、1台あたり数百メガバイトのRAMを割り当てなければならず、一般的に起動に数十秒かかります。私たちは、すべてのお客様のコードを、何百もの拠点のすべてに展開できるソリューションが必要です。そのためには、各拠点のRAMオーバーヘッドを可能な限り小さくする必要があります。また、トラフィックを受信していないworkersを安全にシャットダウンできるように起動はオンデマンドで実行できるように十分に高速である必要があります。Virtual machinesではこのようなニーズに対応できません。

  • コンテナ: 私の個人的なバックグラウンドは、コンテナベースのサンドボックスです。Linuxの「名前空間」を慎重に使用し、強力なseccomp-bpfフィルタやその他の攻撃表面を縮小するテクニックを組み合わせることで、ネイティブなLinuxバイナリを実行できる非常に安全なサンドボックスをセットアップすることができます。これにより、開発者はネイティブコードやLinux上で動作する任意の言語で書かれたコードを展開できるという利点があります。しかし、コンテナはVirtual machinesよりもはるかに効率的ではありますが、それでも十分ではありません。各ワーカーはOSレベルのプロセスで動作する必要があり、RAMを消費し、コンテキストスイッチのオーバーヘッドが発生します。また、ネイティブコードは素早くロードできますが、サーバー指向の言語環境の多くは起動時間が最適化されていません。最後に、コンテナのセキュリティはまだ未熟です。適切に設定されたコンテナはかなり安全ですが、Linuxカーネルにコンテナがブレイクアウトするバグが時々発見されています。

  • Vx32: 私たちは、Vx32として知られる魅力的な小さなサンドボックスについて検討しました。このサンドボックスは、「ソフトウェアフォールトアイソレーション(ソフトウェア障害の検出と隔離)」を使用して、ネイティブコードのバイナリをプロセスごとに複数のサンドボックスで実行できるようにしたものです。このアプローチはエレガントさが魅力的でしたが、開発者がコードを異なるプラットフォームにクロスコンパイルする必要があり、スムーズな使用のためのツールに多大な時間を費やさなければならないというデメリットがありました。さらに、複数のプロセスに比べてコンテキストスイッチのオーバーヘッドが軽減されますが、サンドボックス間で共有できるソフトウェアスタックがほとんどないため、RAMの使用量が依然として多くなる可能性があります。

最終的に、V8が最良の選択であることが明らかになりました。決定的だったのはV8にはWebAssemblyが組み込まれていたことで、これによって他の言語で書かれたコードをデプロイすることを本当に望むユーザーもそれができることです。

なぜNode.jsではないのですか?

Node.jsはサーバー指向のJavaScriptランタイムで、V8も使用しています。一見すると、V8で直接ビルドするよりもNode.jsを再利用する方が理にかなっているように思えます。

しかし、結局のところ、V8上に構築されているにもかかわらず、Nodeはサンドボックスとして設計されていません。確かに、私たちはvmモジュールについての知見がありますが、よく見ると、そのページに「注意:vmモジュールはセキュリティの仕組みではありません。信頼できないコードの実行には使用しないでください。」 としっかりと書かれています。

そのため、Nodeをベースにした場合、V8のサンドボックスの利点を失うことになります。代わりにプロセスレベル(別名コンテナベース)のサンドボックスを使用しなければなりませんが、これは前述のように安全性と効率性に欠けます。

なぜService Worker APIなのですか?

設計プロセスの初期段階で、私たちはもう少しで大きなミスを犯すところでした。

nginx のスクリプトを書くのに多くの時間を費やしたり、HTTPプロキシサービスを扱ったことのあるほとんどの人(基本的にはCloudflareの全員)は、API がどのようにあるべきかについて非常に具体的なアイデアを持っている傾向があります。私たちは皆、開発者がコールバックを挿入できる2つの主な「フック」、つまりリクエストフックとレスポンスフックを提供するという前提でスタートしています。リクエストフックのコールバックはリクエストを変更し、レスポンスフックのコールバックはレスポンスを変更することができます。次に、キャッシュについて考え、「あるフックはプリキャッシュで、あるフックはポストキャッシュで実行しよう」と言うことになりました。これでフックが4つになりました。一般的に、これらのフックは純粋なノンブロッキング関数であると想定されていました。

その後、私は当社のロンドンのオフィスでデザインミーティングの合間に、ブラウザでService Workersを使用して仕事をしていたIngvar Stepanyan氏とランチを共にしました。Ingvar氏は、これこそがW3C Service Workers APIが設計されたユースケースだ、と指し示しました。Service Workersはプロキシを実装し、キャッシングを制御しますが、これは従来ブラウザで行われてきました。

ただし、Service Worker APIは、リクエストフックとレスポンスフックに基づいたものではありません。代わりに、Service Workerはエンドポイントを実装します。これはリクエストを受信してそれらのリクエストに応答するイベントハンドラを1つ登録します。ただし、そのハンドラは非同期であるため、レスポンスを生成する前に他のI / Oを実行できます。特に、独自のHTTPリクエスト(「サブリクエスト」と呼びます)を作成できます。したがって、単純なService Workerは、オリジナルのリクエストを変更し、それをサブリクエストとしてオリジンに転送し、レスポンスを受け取り、変更して、それを返すことができます。フックモデルが実行できるすべてのことです。

しかし、Service Workerの方がはるかに強力です。Service Workerは、複数のサブリクエストを直列または並列に送信し、その結果を組み合わせることができます。サブリクエストを作成せずに直接応答することもできます。また、既にオリジナルのリクエストに応答した後に、非同期のサブリクエストを作成することもできます。Service WorkerはCache APIを通じてキャッシュを直接操作することもできます。そのため、「プリキャッシュ」や「ポストキャッシュ」のようなフックは必要ありません。キャッシュのルックアップをコードの必要な箇所に貼り付けるだけです。

さらに嬉しいことに、Service Worker APIや、FetchやStreamsなどの関連する最新のWeb APIは、非常に賢い人々によって細心の注意を払って設計されています。Promisesのような最新のJavaScriptのイディオムが使われており、MDNなどで十分に文書化されています。もし私たちが独自のAPIを設計していたら、間違いなくすべての面で粗悪なものになっていたでしょう。

私たちのユースケースにとって、Service Worker APIが適切なAPIであることがすぐにわかりました。

いつから利用できるか

Cloudflare Workersは、Cloudflareにとって大きな変化であり、徐々に展開しています。早期アクセスをご希望の方、または準備が整ったら通知を受けたい方は、以下から手続きをお願いします。

ベータ版にサインアップ »

Cloudflareは企業ネットワーク全体を保護し、お客様がインターネット規模のアプリケーションを効率的に構築し、あらゆるWebサイトやインターネットアプリケーションを高速化し、DDoS攻撃を退けハッカーの侵入を防ぎゼロトラスト導入を推進できるようお手伝いしています。

ご使用のデバイスから1.1.1.1 にアクセスし、インターネットを高速化し安全性を高めるCloudflareの無料アプリをご利用ください。

より良いインターネットの構築支援という当社の使命について、詳しくはこちらをご覧ください。新たなキャリアの方向性を模索中の方は、当社の求人情報をご覧ください。
Birthday WeekJavaScript製品ニュース開発者ServerlessProgramming

Xでフォロー

Kenton Varda|@kentonvarda
Cloudflare|@cloudflare

関連ブログ投稿