Compute@Edge の開発の流れやツールを紹介するためのハンズオンワークショップです。
ワークショップへ参加する為には Fastly CLI のインストールと JavaScript の開発環境が必要です。 まだ準備ができていない場合は、こちらの記事を参考にご対応ください。 https://zenn.dev/hrmsk66/articles/74e2e890726e99
Fastly CLI のバージョンの違いによるトラブルを避けるために fastly update
コマンドを実行しておいてください。
- プロジェクトの作成
- Hello, World!
- バックエンドサーバの設定
- クライアント IP と環境変数
- プロジェクトのデプロイ
- ライブログモニタリング
- キャッシュオーバーライド
- ディクショナリ
- 外部パッケージを使う
- プロジェクトの作り方の紹介
適当なディレクトリを作成して、ディレクトリ内で fastly compute init
を実行してください。プロンプトに従って開発言語とスターターキットを選択していきます。今回は [2] JavaScript と [2] Empty starter for Javascript を選択してください。
mkdir example && cd example
fastly compute init
コマンドの実行がおわると、以下のようなファイル・ディレクトリが作成されているはずです。
このワークショップのなかで編集するのは index.js
と fastly.toml
だけです。
├── README.md
├── fastly.toml
├── node_modules
├── npm-shrinkwrap.json
├── package.json
├── webpack.config.js
└── src
└── index.js
src/index.js
を見てみましょう。
/// <reference types="@fastly/js-compute" />
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
return new Response("OK", { status: 200 });
}
addEventListener
によって fetch
イベントが呼ばれたときに実行する処理を登録しています。fetch
はクライアントからのリクエストを受信したときに発生するイベントです。
handleRequest
に渡されている引数 event
にはリクエストの情報等が含まれています。リクエストされたパスや、リクエストヘッダの値に基づく処理を実行する場合は event
から情報を取得します。
アプリケーションのロジックは handleRequest
の中に追加します。レスポンスメッセージを変更してみましょう。
/// <reference types="@fastly/js-compute" />
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
return new Response("Hello, World!", { status: 200 });
}
開発用のローカルサーバで動作を確認してみましょう。
プロジェクトのディレクトリで fastly compute serve
を実行して、ブラウザでアクセスしてみてください。
ほとんどの機能をテストすることができるものの、ローカルテストは Fastly で動作するサービスの完全なレプリカではありません。例えばキャッシュの機能はありませんし、Geo ロケーションの情報取得には対応していません。ローカルテストの制約についてはこちらのドキュメントに記載されています。
エッジだけで処理が完結するサービスをつくることもできますが、バックエンドサーバからのレスポンスをキャッシュする CDN 的な使い方もできますし、サーバのレスポンスをカスタマイズしてクライアントに返すこともできます。
バックエンドサーバを追加してみましょう。
まずバックエンドリクエストの宛先を設定します。
ローカルテストでは fastly.toml
にバックエンドサーバの設定を記述します。バックエンドサーバはローカルマシン上で動作する別のサーバでもいいですし、外部のサーバでも構いません。
fastly.toml
に以下の内容をコピペしてください。今回は example.com
をバックエンドサーバとして使います。
[local_server]
[local_server.backends]
[local_server.backends.backend_1]
url = "https://example.com"
override_host = "example.com"
ローカルテストでのリクエストの宛先は 127.0.0.1:7676
ですので Host ヘッダの値も特に指定しない限りその値になります。リクエストをそのままバックエンドに転送するとサーバによってはエラーが返ってきます。 上記のように override_host
を設定すると、Host ヘッダはローカルの HTTP サーバからバックエンドにリクエストを送信するときに指定した値で上書きされます。
/
へのリクエストを example.com
へ送信し、それ以外のパスへのリクエストには今まで通り Hello, World!
を返すようにコーディングします。
/// <reference types="@fastly/js-compute" />
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
const req = event.request;
const url = new URL(req.url);
if (url.pathname === "/") {
return fetch(req, {
backend: "backend_1",
});
}
return new Response("Hello, World!", { status: 200 });
}
fastly compute serve
を実行しなおして動作を確認してください。
環境変数を使ってサービスの ID やバージョン、リクエストを処理している POP やサーバ名を取得することができます。 例えば、リクエストを処理しているサーバ名は以下のコードで取得することができます。
const server_name = fastly.env.get("FASTLY_HOSTNAME");
クライアント IP は event.client.address
から取得することができます。
const client_ip = event.client.address;
これらの情報とリクエストされたパスを JSON 形式でログに出力してみましょう。
/// <reference types="@fastly/js-compute" />
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
const req = event.request;
const url = new URL(req.url);
const client_ip = event.client.address;
const server_name = fastly.env.get("FASTLY_HOSTNAME");
console.log(JSON.stringify({ path: url.pathname, client: client_ip, server: server_name }));
if (url.pathname === "/") {
return fetch(req, {
backend: "backend_1",
});
}
return new Response("Hello, World!", { status: 200 });
}
VCL のサービスでは Web UI にログフォーマットを入力するところがありますが、Compute@Edge のログフォーマットはこのようにコードの中で表現します。
fastly compute serve
を実行しなおしてリクエストを送信すると、以下のメッセージが表示されると思います。
Log: {"path":"/","client":"127.0.0.1","server":"localhost"}
ご覧の通りローカルテストでは、FASTLY_HOSTNAME
には localhost
が格納されています。これを利用してローカルで動いている場合と、Fastly で動いている場合とで処理を書き分けることができます。
Compute@Edge で使用できるその他の環境変数はこちらに記載されています。
Environment variables reference for Compute@Edge
こちらは JS SDK のドキュメントです。 event.client.address
やその他の API について記載されています。
プロジェクトを Fastly にデプロイして先ほどのログがどのように表示されるか確認してみましょう。
fastly compute publish
を実行してビルドとデプロイを行います。初回はビルドが問題なく完了するとサービスを設定するためのプロンプトが表示されます。
- ドメインは
edgecompute.app
のサブドメインが自動的に生成されるのでそれを利用します - バックエンドサーバのホスト名とポート番号にはそれぞれ
example.com
と443
を設定します - Backend name はデフォルトの
backend_1
のままにします
$ fastly compute publish
✓ Initializing...
( 省略 )
Create new service: [y/N] y
✓ Initializing...
✓ Creating service...
Domain: [yearly-funny-hermit.edgecompute.app]
Backend (hostname or IP address, or leave blank to stop adding backends): example.com
Backend port number: [80] 443
Backend name: [backend_1]
( 省略 )
デプロイが終わるとサービスと、サービスの管理画面の URL が表示されます。ブラウザでアクセスしてみてください。
プロジェクトのルートディレクトリで fastly log-tail
を実行すると Fastly にデプロイしたサービスの stdout, stderr へのアウトプットがリアルタイムにコンソールに配信されます。リクエストを送信するとこのようなログが表示されると思います。
stdout | c23b0f12 | Log: {"path":"/","client":"xxx.xxx.xxx.xxx","server":"cache-hnd18744"}
c23b0f12
はリクエスト毎にアサインされる ID の一部です。アプリケーションが 1 リクエストに対して複数のログを生成する場合、この ID を使ってログをグルーピングすることができます。完全な ID は環境変数 FASTLY_TRACE_ID
で取得することができます。
ライブログモニタリングは開発中のデバッグツールとして有用ですが、扱えるログのボリュームは限られています。プロダクション環境のログ等、ハイボリュームなログを取得する場合はログストリーミングをご利用ください。
Compute@Edge のデフォルトキャッシュルールは基本的には VCL のサービスと同じで、バックエンドサーバからのレスポンスはキャッシュ関連ヘッダの指示に従ってキャッシュされます。example.com
のレスポンスには以下のヘッダがあるため、何もしなければ 604800s (1 week) の TTL がセットされます。
cache-control: max-age=604800
VCL のサービスと同様、適用する TTL を指定したり、キャッシュしないように指示する等、デフォルトの動作を一部オーバーライドすることもできます。
example.com
からのレスポンスをキャッシュしないように動作を変更してみましょう。CacheOverride
を使います。
/// <reference types="@fastly/js-compute" />
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
const req = event.request;
const url = new URL(req.url);
const client_ip = event.client.address;
const server_name = fastly.env.get("FASTLY_HOSTNAME");
console.log(JSON.stringify({ path: url.pathname, client: client_ip, server: server_name }));
if (url.pathname === "/") {
const cacheOverride = new CacheOverride("pass");
return fetch(req, {
backend: "backend_1",
cacheOverride,
});
}
return new Response("Hello, World!", { status: 200 });
}
fastly compute publish
を実行して動作を確認してみましょう。以下のようにレスポンスの x-cache
ヘッダの右端の値が MISS
になっていれば設定が効いていると考えられます(example.com
は GCP でキャッシュしているので x-cache
のひとつめの値は HIT
になることが多いです)。
< x-cache: HIT, MISS
CacheOverride のその他の設定例についてこちらに記載されています。
2022 年 6 月現在、以下の動作には対応していません。
- Set-Cookie をもつレスポンスをキャッシュする
- Cache-Control private もしくは no-cache をもつレスポンスをキャッシュする
- オリジンからのレスポンスを Fastly にキャッシュする前に Vary ヘッダをカスタマイズする
- stale-if-error を Compute@Edge のプログラムからセットする
ローカルテストにはキャッシュの機能はありません。CacheOverride は無視されます。
Compute@Edge のコードから Dictionary を参照することができます。Dictionary は Key-Value 型のデータストアです。 Dictionary を使ってアプリケーションにリダイレクトのロジックを追加してみましょう。
ローカルテストでは fastly.toml
の中に Dictionary の設定を記述します。バックエンドサーバの下に定義を追加してください。
[local_server]
[local_server.backends]
[local_server.backends.backend_1]
override_host = "example.com"
url = "https://example.com"
[local_server.dictionaries]
[local_server.dictionaries.redirects]
format = "inline-toml"
[local_server.dictionaries.redirects.contents]
"/cat" = "https://http.cat/200"
"/dog" = "https://httpstatusdogs.com/200-ok"
Dictionary のコンテンツは別ファイル(JSON 形式)に移して以下のように fastly.toml
から参照させることもできます。
[local_server.dictionaries]
[local_server.dictionaries.redirects]
file = "redirects.json"
format = "json"
JSON ファイルの内容は以下のようになるでしょう。
{
"/cat": "https://http.cat/200",
"/dog": "https://httpstatusdogs.com/200-ok"
}
リクエストされたパスが Dictionary に登録されていたら、対応する URL にリダイレクトするようにコードを追加します。
/// <reference types="@fastly/js-compute" />
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
const req = event.request;
const url = new URL(req.url);
const client_ip = event.client.address;
const server_name = fastly.env.get("FASTLY_HOSTNAME");
console.log(JSON.stringify({ path: url.pathname, client: client_ip, server: server_name }));
if (url.pathname === "/") {
const cacheOverride = new CacheOverride("pass");
return fetch(req, {
backend: "backend_1",
cacheOverride,
});
}
const dict = new Dictionary("redirects");
const redirect = dict.get(url.pathname);
if (redirect) {
headers = new Headers({ location: redirect });
return new Response("", {
status: 302,
headers,
});
}
return new Response("Hello, World!", { status: 200 });
}
パスが見つからない場合、dict.get(url.pathname)
は null
を返します。
null
は if
で false
判定となるので、リダイレクトの対象になりません。
fastly compute serve
を実行してブラウザで /cat
や /dog
にアクセスしてみてください。
cheerio
という外部パッケージを使って example.com
のレスポンスをエッジで書き換えてみましょう。
(cheerio
は jQuery ライクな構文で HTML を解析し、値の取得や書き換えを可能にするパッケージです)
プロジェクトのディレクトリで npm i cheerio
を実行してください。
src/index.js
は以下のように変更します。
/// <reference types="@fastly/js-compute" />
import * as cheerio from "cheerio";
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
const req = event.request;
const url = new URL(req.url);
const client_ip = event.client.address;
const server_name = fastly.env.get("FASTLY_HOSTNAME");
console.log(JSON.stringify({ path: url.pathname, client: client_ip, server: server_name }));
if (url.pathname === "/") {
const cacheOverride = new CacheOverride("pass");
let beresp = await fetch(req, {
backend: "backend_1",
cacheOverride,
});
return rewriteHTML(beresp);
}
const dict = new Dictionary("redirects");
const redirect = dict.get(url.pathname);
if (redirect) {
headers = new Headers({ location: redirect });
return new Response("", {
status: 302,
headers,
});
}
return new Response("Hello, World!", { status: 200 });
}
async function rewriteHTML(beresp) {
let body = await beresp.text();
const $ = cheerio.load(body);
$("h1").html("Hello from Compute@Edge!");
return new Response($.html(), {
status: beresp.status,
headers: beresp.headers,
});
}
fastly compute serve
を実行してページを確認してみてください。
cheerio
と同じように、多くの外部パッケージを Compute@Edge 上で使うことができますが、内部で特定のプラットフォームの API を呼んでいるパッケージは使用することができず、プログラムがクラッシュします。使いたいパッケージが Compute@Edge で使えるか分からない場合は、実際にインストールして試してみるのが手っ取り早いでしょう。
最後にプロジェクトの作り方について紹介します。
ワークショップではスターターキットからプロジェクトをつくりましたが、Github のリポジトリや Fastly Fiddle をベースとしてプロジェクトを作成することもできます。
example.com
をデコレーションするコードサンプルのリポジトリを使ってプロジェクトを作成してみます。
(コードはワークショップで実装したレスポンス書き換えの発展型です)
https://github.com/hrmsk66/ecp-rewrite-html-js
適当なディレクトリを作成して、以下のコマンドを実行します。
fastly compute init --from=https://github.com/hrmsk66/ecp-rewrite-html-js
fastly compute serve
を実行して動作を確認してみてください。
次は Fiddle でつくったベーシック認証のコード例をつかってプロジェクトを作ってみます。
https://fiddle.fastlydemo.net/fiddle/c5dcaa46
fastly compute init --from=https://fiddle.fastlydemo.net/fiddle/c5dcaa46
このベーシック認証のソリューションは、アクセスを許可するユーザの情報をディクショナリにセットしておくことを想定しています。
まず、ユーザ・パスワードの組み合わせを Base64 でエンコードした文字列を生成します。
下記の例ではユーザは fastly
パスワードは password
としています。
echo -n "fastly:password" | base64
ZmFzdGx5OnBhc3N3b3Jk
fastly.toml
にディクショナリを定義して、生成した文字列をキーとするエントリを追加すれば準備完了です。
[local_server.dictionaries]
[local_server.dictionaries.username_password]
format = "inline-toml"
[local_server.dictionaries.username_password.contents]
"ZmFzdGx5OnBhc3N3b3Jk" = "ok"
fastly compute serve
を実行して動作を確認してみてください。
Developer Hub の Code Examples の中には Fiddle がページに埋め込まれているタイプのコード例がありますが、それらのコード例は同様にプロジェクトをつくるときのベースとして使用することができます。右上のINSTALLをクリックすると実行すべき fastly compute init
コマンドが表示されるので試してみましょう。
開発体験は VS Code 等のエディタを使った方がよいと思いますが、ブラウザ上でコードが実行できますし、プロジェクトのベースとしても使えるので、チーム内でアイデアをシェアしたり、サポートチームに問い合わせするとき等に便利に使えると思います。Fastly Fiddle に入力したデータは URL が分かれば誰でもアクセスできますので、パスワードや API トークン等の機密情報を入力しないように注意してください。
Fiddle で使うことのできる外部パッケージはこちらにリストされています。
- Compute@Edge services - C@E のはじめ方やリソース制約
- Migrate from VCL - VCL からのマイグレ
- JavaScript on Compute@Edge - C@E の開発ガイド(JS)
- JavaScript SDK - JS SDK のドキュメント
- Testing and debugging on Compute@Edge - デバッグツールやログについて
- Code examples in JavaScript - コード例(JS)
- fastly.toml package manifest format - fastly.toml について
- Environment variables reference for Compute@Edge - 環境変数