CloudFront Functions の活用パターン集

# はじめに

CloudFront Functions（以後CF2）は、L@Eと異なり、軽量のため、L@Eより優先して使用を検討する必要がようなので、CF2について使用パターンをまとめました。

ビューアーレスポンスは使用しなかったため、ビューアーリクエストのみのパターンになります。


# CF2とL@Eの比較

| 1 | 2 | 3 |
|:-:|:-:|:-:|
|   |  CloudFront Functions | Lambda@Edge  |
| ランタイムサポート  | JavaScript(ECMAScript 5.1準拠)  | Node.js、Python  |
| 実行場所	 | 218 以上の CloudFrontエッジロケーション |  13 の CloudFrontリージョンのエッジキャッシュ| 
| CloudFront トリガー	 | ビューアリクエスト / ビューアレスポンス | ビューアリクエスト / ビューアレスポンス / オリジンリクエスト / オリジンレスポンス |
| 最大実行時間 | 1 ミリ秒未満 | 5 秒 (ビューアトリガー) / 30 秒 (オリジントリガー)|
| 最大メモリ | 2 MB | 128 MB (ビューアトリガー) / 10 GB (オリジントリガー)|
| 合計パッケージサイズ | 10 KB | 1 MB (ビューアトリガー) / 50 MB (オリジントリガー) |
| ネットワークアクセス | なし |あり  |
| ファイルシステムアクセス| なし | あり |
| リクエスト本文へのアクセス | なし | あり |
| 料金 | 無料利用枠あり。リクエストごとに課金。 | 無料利用枠なし。リクエストと関数の実行時間ごとに課金。 |
| デプロイ反映時間 | 10秒以下 | 5分程度 |
| デプロイ方法| 複数を一度にデプロイ可能 | 1つずつデプロイ可能 |


CF2は、JavaScript(ECMAScript 5.1準拠)のため、constなど使用できない制限があります。下記で使用できる機能が挙げられています。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html

CF2の方が、軽量、デプロイ反映が早い、という点で始めに検討するとよいかと思います。

# IP制限
許可したIPの場合、リクエストが通り、NGの場合403をレスポンスします
```js
function handler(event) {
  var request = event.request;
  var clientIP = event.viewer.ip;
  // 許可するIPを指定
  var IP_WHITE_LIST = ["xxx.xxx.xxx.xxx", "xxx.xxx.xxx.xxx"];
  var allowIP = IP_WHITE_LIST.includes(clientIP);

  if (allowIP) {
    return request;
  } else {
    var response = {
      statusCode: 403,
      statusDescription: "Forbidden",
    };
    return response;
  }
}
```

# アクセス承認

Bearer認証やBasic認証でCF2は使用できます。

## Bearer認証

https://qiita.com/h_tyokinuhata/items/ab8e0337085997be04b1

https://architecting.hateblo.jp/entry/2020/03/27/130535

Bearerのトークンパラメータが成功した場合、リクエストが通り、失敗した場合、401エラーをレスポンスという処理になっています。

```js
function handler(event) {

  var request = event.request;
  var headers = request.headers;

  var authString = "Bearer <token値>";

  if (
    typeof headers.authorization === "undefined" ||
    headers.authorization.value !== authString
  ) {
    return {
      statusCode: 401,
      statusDescription: "Unauthorized",
      headers: { "www-authenticate": { value: "Bearer" } },
    };
  }
  console.log("request-success");
  return request;
}
```

ローカルで認証できているか確認する場合、curlを使いましょう。
```
$ curl https://www.hoge.com/test/ \
  -H "Authorization:Bearer token値"
```

## Basic認証
Basic認証のトークンパラメータが成功した場合、リクエストが通り、失敗した場合、401エラーを返すという処理になっています。

```js
function handler(event) {
  var request = event.request;
  var headers = request.headers;

  // echo -n user:pass | base64
  var authString = "Basic <token値>";

  if (
    typeof headers.authorization === "undefined" ||
    headers.authorization.value !== authString
  ) {
    return {
      statusCode: 401,
      statusDescription: "Unauthorized",
      headers: { "www-authenticate": { value: "Basic" } }
    };
  }
  console.log("request-success");
  return request;
}
```

# URLの書き換え
`/test/`のパスを`/fuga/`に書き換える

```js
function handler(event) {
  var request = event.request;
  var uri = request.uri;

  if (uri.startsWith("/test/")) {
    request.uri = "/fuga/";
  }
  return request;
}
```
:::note warn
CFの代替ドメイン名 (CNAME)に記載がないホスト名は、ビューアーリクエストで書き変えることは、基本的にできません。
CNAME に複数指定するよう `*.hoge.com`等の対応するとできますが、それ以外の方法でホストを書き換えたい場合、L@Eのオリジンリクエストで実現できます。
:::

# サブディレクトリのドキュメントルートを指定
静的サイトをS3に構築した場合、サブドメイン名と同じS3のディレクトリをルーティングできます。

`fuga.hoge.com`の場合、S3のあるバケットの`fuga`ディレクトリを指定する。

```js
function handler(event) {
  var request = event.request;
  var uri = request.uri;
  var host = request.headers.host.value;

  var prefix = host.split(".", 1)[0];
  if (uri.startsWith("/")) {
    request.uri = "/" + prefix + uri;
  } else {
    request.uri = "/" + prefix + "/" + uri;
  }
  return request;
}
```

# リダイレクト

## URLのリダイレクト
### 特定のパスのみリダイレクト
`/test/`配下のパスは、`/test/403.html`にリダイレクトさせる

```js
function handler(event) {
  var request = event.request;
  var host = request.headers.host.value;
  var uri = request.uri;

  if (uri.startsWith("/test/") && uri != "/test/403.html") {
    return {
      statusCode: 301,
      statusDescription: "Moved Permanently",
      headers: { location: { value: `https://${host}/test/403.html` } },
    };
  }
  return request;
}
```
### 必ずリダイレクトさせる
必ず`/test/403.html`にリダイレクトさせる

```js
function handler(event) {
  var request = event.request;
  var host = request.headers.host.value;
  var uri = request.uri;

  if (uri != "/test/403.html") {
    return {
      statusCode: 301,
      statusDescription: "Moved Permanently",
      headers: { location: { value: `https://${host}/test/403.html` } },
    };
  }
  return request;
}
```
リダイレクトの場合、ホストを変えることは、もちろん可能です。

## 403を返す
403を返す

```js
function handler(event) {
  var response = {
    statusCode: 403,
    statusDescription: "Forbidden",
  };
  return response;
}
```
## wwwなしのhostをwwwにリダイレクトさせる

`hoge.com`をリクエストした場合、`www.hoge.com`にリダイレクトさせます。
CloudFrontには、元々`xxx.cloudfront.net`というドメインが使用されているため、そのドメインでないことを判定に含めます。
```js
function handler(event) {
  var request = event.request;
  var host = request.headers.host.value;
  var uri = request.uri;

  if (!host.includes('cloudfront.net') && !host.startsWith('www')) {
    return {
      statusCode: 301,
      statusDescription: 'Moved Permanently',
      headers: { location: { value: `https://www.${host}${uri}` } },
    };
  }

  return request;
}
```

# ルートディレクトリをindex.phpに指定

`fuga.com/test/`やfuga.com/test`の場合、index.htmlがデフォルトで表示されるようにします。

```js
function handler(event) {
  var request = event.request;
  var uri = request.uri;

  if (uri.endsWith("/")) {
    request.uri += "index.html";
  } else if (!uri.includes(".")) {
    request.uri += "/index.html";
  }

  return request;
}
```

# 他のCF2の使用パターン

ドキュメントでは、使用例が豊富でした。
>- レスポンスに Cache-Control ヘッダーを追加する
>- Cross-Origin Resource Sharing (CORS) ヘッダーをレスポンスに追加
>- Cross-Origin Resource Sharing (CORS) ヘッダーをリクエストに追加
>- レスポンスにセキュリティヘッダーを追加する
>- リクエストに True-Client-IP ヘッダーを追加する
>- ビューワーを新しい URL にリダイレクトさせる
>- index.html を追加してファイル名を含まない URL をリクエストする
>- リクエストの単純なトークンを検証する

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-example-code.html

# CF2のログ出力方法

ログの出力方法は、下記のドキュメントにも記載がありますが、`console.log()`を記述し、CloudFrontに設置し、`実際に起動させる`必要があります。
L@Eと異なり、ログは、`us-east-1`のみで出力・記録されます。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/monitoring-functions.html

# eventの構造
下記で、event構造が記載されています。
例えば、user-agentヘッダーは、`request.headers["user-agent"].value`で取得できます。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html#functions-event-structure-example

# 参考

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html