CloudFront Functions を AWS CLI で触る ― ついでにブログの URL 正規化を Lambda@Edge から移行した
2021-05-04CloudFront Functions という新しい機能がリリースされたので AWS CLI で触ってみます。ついでに、 Lambda@Edge でやっているこのブログの URL 正規化処理を CloudFront Functions に移行してみました。
目次
CloudFront Functions とは
Amazon CloudFront announces the general availability of CloudFront Functions, a new serverless edge compute capability. You can use this new CloudFront feature to run JavaScript functions across 225+ CloudFront edge locations in 90 cities across 47 countries. CloudFront Functions is built for lightweight HTTP(S) transformations and manipulations, allowing you to deliver richer, more personalized content with low latency to your customers.
Amazon CloudFront announces CloudFront Functions, a lightweight edge compute capability
CloudFront のエッジロケーションで、 JavaScript で実装された関数を実行できるというものです。
CloudFront には似たような機能として Lambda@Edge というのがありますが、本当にざっくりとしたイメージでは同じような機能だという認識です。が、もちろん違いはあり、詳細については既にクラメソさんの記事で解説されています。
エッジで爆速コード実行!CloudFront Functionsがリリースされました! | DevelopersIO
ちなみに、今回 触る範囲で気になる差は下記の部分です。
CloudFront Functions | Lambda@Edge | |
---|---|---|
ランタイム | JavaScript | Node.js, Python |
最大パッケージサイズ | 10 KB | 1 MB1 |
最大メモリ | 2 MB | 128 MB2 |
最大実行時間 | 1ms 未満 | 5 秒3 |
CloudFront Functions は、略して CF2 と呼ぶようです。
Announcing CloudFront Functions (CF2), a new serverless edge compute capability for lightweight customizations. CF2 runs custom Javascript code at all of CloudFront’s 225+ edge locations with minimal latency and will cost ~1/6th that of Lambda@Edge.. https://t.co/tlD5dvwpmj
— Amazon CloudFront (@cloudfront) May 3, 2021
Lambda@Edge でやってたことをそのままやる
今回やるのは、 このブログに対して既に Lambda@Edge でやっている URL の正規化を、 CloudFront Functions に移行します。
Lambda@Edge での正規化については下記の記事で書いています。
Lambda@Edge で静的サイトの URL を正規化する - michimani.net
Viewer Request をトリガーに、 /
無しのリクエストを /
にリダイレクトしたり、 /
でのリクエストに /index..html
を補完してオリジンにリクエストを流したり、ということをやっています。
CloudFront Functions の要件に合うのか
前述したとおり Lambda@Edge と CloudFront Functions とでは違いがいくつかあるので、そこをクリアできるか確認しておきます。
ランタイム
これは同じ処理を JavaScript で書き換えればよいだけなので、作業は発生するもの移行は可能です。ちなみに今回対象となっている Lambda@Edge は Node.js で実装しているものなので、大きな変更はなさそうです。
最大パッケージサイズ
Lambda@Edge で使っている Lambda 関数のサイズは 991.0 byte なので、 10 KB 以内に収まっています。
最大メモリ
これはどうしようもないので 2 MB でがんばります。
最大実行時間
ここが一番の問題です。
CloudFront Functions では最大実行時間が 1ms 未満 ということで、本当に軽い処理の実行しかできなさそうです。とはいえ、 URL の正規化程度であれば実行できそうですが、念のため現状の Lambda@Edge の処理にどれくらいかかっているのか確認しておきます。ちなみに Lambda 関数に割り当てられているメモリは 128 MB です。
確認方法としては、 Lambda 関数の実行ログから REPORT 内の Duration と Billed Duration の差をみます。
$ aws logs get-log-events \
--log-group-name ${LOG_GP_NAME} \
--log-stream-name "${LOG_ST_NAME}" \
--query "events[?contains(message, \`REPORT\`)].message" \
--output text
REPORT RequestId: 889e0e28-4980-48f3-a134-8327666ccc97 Duration: 6.44 ms Billed Duration: 7 ms Memory Size: 128 MB Max Memory Used: 63 MB Init Duration: 164.61 ms
REPORT RequestId: d2e219b5-757a-487f-aa3e-148db76e1292 Duration: 113.00 ms Billed Duration: 113 ms Memory Size: 128 MB Max Memory Used: 64 MB
REPORT RequestId: 9005ebdf-0783-4577-b45a-426db365a5a4 Duration: 128.46 ms Billed Duration: 129 ms Memory Size: 128 MB Max Memory Used: 65 MB
REPORT RequestId: e0facf39-87a9-4fb6-a2a5-98example2dc6 Duration: 1.35 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 65 MB
REPORT RequestId: f12d9777-142d-4d07-8aa9-6eexample2ebb Duration: 5.51 ms Billed Duration: 6 ms Memory Size: 128 MB Max Memory Used: 65 MB
REPORT RequestId: ad884280-69a2-4784-9883-6aexample6a0c Duration: 1.36 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 65 MB
REPORT RequestId: 93c493a1-f07d-405f-8702-14examplee3d3 Duration: 1.33 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 65 MB
REPORT RequestId: 280d7b33-9cee-4aa4-89f7-afexamplebf8e Duration: 1.30 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 65 MB
REPORT RequestId: 26d94ea2-90fe-470b-8140-6bexample9432 Duration: 1.16 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 65 MB
REPORT RequestId: 2691e2c7-14e2-45f4-bd48-02examplee3de Duration: 1.15 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 65 MB
REPORT RequestId: e42686f5-5d52-4fdf-906a-40example24b2 Duration: 1.10 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 65 MB
シェルスクリプト力が無いので出力結果をスプレッドシートに貼り付けて確認してみると、最大で 0.9 ms くらい、平均では 0.7334 ms ということがわかりました。実行時間的には問題なさそうですが、ログからも分かる通り毎回 65 MB くらいメモリを使ってのこの結果です。 CloudFront Functions の 2 MB でいけるかどうかは、 Functions のテストを実施したタイミングでわかります。
AWS CLI でやってみる
AWS CLI では、下記の 8 個のサブコマンドが追加されています。 なお、この記事を書いている時点 (2021/05/04) では v1 の最新バージョン 1.19.64
でのみ対応しており、 v2 の最新バージョン 2.2.1
ではまだ使えません。
v1 では 1.19.64
以降で、 v2 では 2.2.2
以降で利用可能になっています。
create-function
delete-function
describe-function
get-function
list-functions
publish-function
test-function
update-function
やることとしては、 CloudFront Functions 用のスクリプトを作って、 Function を作って、テストして、公開して、ディストリビューションと紐付ける、です。
CloudFront Functions 用のスクリプトを作成
CloudFront Functions の Function の実装方法については、下記 CloudFront のドキュメントを参考にします。
Writing function code (CloudFront Functions programming model) - Amazon CloudFront
書き方としては、 event
を受け取る関数 handler
を実装します。
function handler(event) {
var request = event.request;
var uri = request.uri;
/**
いろいろやる
*/
// リダイレクトさせたい場合
if (hoge) {
var response = {
statusCode: 302,
statusDescription: 'Found',
headers: {
"location": {
"value": redirectUrl
}
}
}
return response;
}
return request;
}
今回は Viewer Request をトリガーに実行するので、 return
する先はオリジン (S3) となります。 ただし、リクエストに location
ヘッダーを付与することで、オリジンに到達する前にリダイレクトされることになります。ここは Lambda@Edge と同じです。
今回やりたいのは下記のとおりです。
/
無しは/
ありに 301 リダイレクト/index..html
付きの場合は/index..html
無しに 301 リダイレクト- オリジンへは
/index..html
を補完してリクエスト
/
ありが正規の URL としたいため、 301 (恒久的な) リダイレクトとしています。
function handler(event) {
var host = 'https://michimani.net';
var request = event.request;
var requestUri = request.uri;
// トップページへのリクエストに対しては何もしない
if (requestUri == '' || requestUri == '/') {
return request;
}
// `/` 無し または `index.html` 付きは `/` にリダイレクトする
if (requestUri.match(/((page|post|posts|tags|categories|archives|about)(\/.*[^\/])?|\/index\.html)$/)) {
var redirectUrl = host + requestUri.replace('/index.html', '') + '/';
var response = {
statusCode: 301,
statusDescription: 'Found',
headers: {
location: {
value: redirectUrl
}
}
}
// Viewer に対してレスポンスを返す
return response;
}
// オリジンに対しては `/index.html` を補完してリクエストする
var actualUri = requestUri.replace(/\/$/, '\/index.html');
request.uri = actualUri;
// オリジンへリクエスト
return request;
}
これを cf2-redirect.js
として保存しておきます。
Function を作成
Function の作成には cloudfront reate-function
コマンドを使います。
$ aws cloudfront create-function help
...
SYNOPSIS
create-function
--name <value>
--function-config <value>
--function-code <value>
[--cli-input-json <value>]
[--generate-cli-skeleton <value>]
--function-config
では Comment
と Runtime
を指定しますが、現時点で Runtime
に指定できるのは cloudfront-js-1.0
のみです。
先ほど作成したスクリプトをもとに、 Function を作成します。
$ aws cloudfront create-function \
--name cf2-redirect \
--function-config Comment="Redirect Function",Runtime=cloudfront-js-1.0 \
--function-code file://cf2-redirect.js
{
"Location": "https://cloudfront.amazonaws.com/2020-05-31/function/arn:aws:cloudfront::XXXXXXXXXXXX:function/cf2-redirect",
"ETag": "ETVEXAMPLE",
"FunctionSummary": {
"Name": "cf2-redirect",
"Status": "UNPUBLISHED",
"FunctionConfig": {
"Comment": "Redirect Function",
"Runtime": "cloudfront-js-1.0"
},
"FunctionMetadata": {
"FunctionARN": "arn:aws:cloudfront::XXXXXXXXXXXX:function/cf2-redirect",
"Stage": "DEVELOPMENT",
"CreatedTime": "2021-05-04T14:26:41.142Z",
"LastModifiedTime": "2021-05-04T14:26:41.142Z"
}
}
}
ステータスが UNPUBLISHED
、 ステージは DEVELOPMENT
になっています。
describe-function
で確認してみます。
$ aws cloudfront describe-function \
--name cf2-redirect
{
"ETag": "ETVEXAMPLE",
"FunctionSummary": {
"Name": "cf2-redirect",
"Status": "UNPUBLISHED",
"FunctionConfig": {
"Comment": "Redirect Function",
"Runtime": "cloudfront-js-1.0"
},
"FunctionMetadata": {
"FunctionARN": "arn:aws:cloudfront::XXXXXXXXXXXX:function/cf2-redirect",
"Stage": "DEVELOPMENT",
"CreatedTime": "2021-05-04T14:26:41.142Z",
"LastModifiedTime": "2021-05-04T14:26:41.185Z"
}
}
}
ちなみに get-function
では、指定した Function のコードを ouput
として取得できます。
Function をテスト
作成した Function をテストしてみます。テストするには cloudfront test-function
コマンドを使います。
$ aws cloudfront test-function help
...
SYNOPSIS
test-function
--name <value>
--if-match <value>
[--stage <value>]
--event-object <value>
[--cli-input-json <value>]
[--generate-cli-skeleton <value>]
--if-match
には、対象の Function の ETag
の値を指定します。(describe-function
で確認)
--event-object
については、下記 CloudFront のドキュメントを参考にして、次のような JSON オブジェクトを作成して event-object.json
として保存しておきます。
{
"version": "1.0",
"context": {
"distributionDomainName": "test.cloudfront.net",
"distributionId": "TESTDISTID",
"eventType": "viewer-request",
"requestId": "TEST-REQUEST-ID"
},
"viewer": {
"ip": "111.111.111.111"
},
"request": {
"method": "GET",
"uri": "/categories/aws",
"headers": {
"host": {
"value": "test.example.com"
},
"user-agent": {
"value": "test user agent0"
}
}
}
}
CloudFront Functions event structure - Amazon CloudFront
上記のイベントでは https://michimani.net/category/aws
へのリクエストをテストするので、 /
ありへのリダイレクトレスポンスが取得できれば OK です。
$ CFF_NAME="cf2-redirect"
$ aws cloudfront test-function \
--name "${CFF_NAME}" \
--if-match $( \
aws cloudfront describe-function \
--name "${CFF_NAME}" \
--query "ETag" \
--output text) \
--event-object file://event-object.json
{
"TestResult": {
"FunctionSummary": {
"Name": "cf2-redirect",
"Status": "UNPUBLISHED",
"FunctionConfig": {
"Comment": "Redirect Function",
"Runtime": "cloudfront-js-1.0"
},
"FunctionMetadata": {
"FunctionARN": "arn:aws:cloudfront::XXXXXXXXXXXX:function/cf2-redirect",
"Stage": "DEVELOPMENT",
"CreatedTime": "2021-05-04T14:26:41.142Z",
"LastModifiedTime": "2021-05-04T15:28:21.410Z"
}
},
"ComputeUtilization": "35",
"FunctionExecutionLogs": [],
"FunctionErrorMessage": "",
"FunctionOutput": "{\"response\":{\"headers\":{\"location\":{\"value\":\"https://michimani.net/categories/aws/\"}},\"statusDescription\":\"Found\",\"cookies\":{},\"statusCode\":301}}"
}
}
FunctionOutput
を見てみると、 301 リダイレクト用のレスポンスが返っているのがわかります。また、 ComputeUtilization
の値が 35 ということで、これは実行時間が 1ms 未満であることを示しています。
It also shows the compute utilization, which is a number between 0 and 100 that indicates the amount of time that the function took to run as a percentage of the maximum allowed time. For example, a compute utilization of 35 means that the function completed in 35% of the maximum allowed time.
Function を Publish
Function のテストができたら、 cloudfront publish-function
コマンドで publish します。
$ aws cloudfront publish-function help
...
SYNOPSIS
publish-function
--name <value>
--if-match <value>
[--cli-input-json <value>]
[--generate-cli-skeleton <value>]
必要なのは Function の名前と ETag なので、下記のように実行します。
$ aws cloudfront publish-function \
--name "${CFF_NAME}" \
--if-match $( \
aws cloudfront describe-function \
--name "${CFF_NAME}" \
--query "ETag" \
--output text)
{
"FunctionSummary": {
"Name": "cf2-redirect",
"Status": "UNASSOCIATED",
"FunctionConfig": {
"Comment": "Redirect Function",
"Runtime": "cloudfront-js-1.0"
},
"FunctionMetadata": {
"FunctionARN": "arn:aws:cloudfront::XXXXXXXXXXXX:function/cf2-redirect",
"Stage": "LIVE",
"CreatedTime": "2021-05-04T16:05:34.967Z",
"LastModifiedTime": "2021-05-04T16:05:34.967Z"
}
}
}
Distribution との紐付け
Distribution との紐付けは CloudFront Functions 関連の API ではなく CloudFront の update-distribution
で行います。 マネジメントコンソール上では CloudFront Functions の画面の Associate タブから実行できます。
update-distribution
を行うには DistributionConfig が必要になるので、既存の DistributionConfig を get-distribution
で取得します。
$ CF_DIST_ID="YOURDISTID"
$ aws cloudfront get-distribution \
--id ${CF_DIST_ID} \
| jq ".Distribution.DistributionConfig" \
> distribution-config.json
そして、 DistributionConfig の LambdaFunctionAssociations
を削除して、代わりに FunctionAssociations
の部分を下記のように記述します。
{
"FunctionAssociations": {
"Quantity": 1,
"Items": [
{
"FunctionARN": "<CloudFront Function の ARN>",
"EventType": "viewer-request"
}
]
}
}
Function の ARN は下記コマンドで取得します。
$ aws cloudfront describe-function \
--name "${CFF_NAME}" \
--query "FunctionSummary.FunctionMetadata.FunctionARN" \
--output text
更新前後の差分は下記のようになります。
--- distribution-config.json 2021-05-05 00:45:35.000000000 +0900
+++ distribution-config-update.json 2021-05-05 00:59:53.000000000 +0900
@@ -59,18 +59,18 @@
"SmoothStreaming": false,
"Compress": false,
"LambdaFunctionAssociations": {
+ "Quantity": 0
+ },
+ "FunctionAssociations": {
"Quantity": 1,
"Items": [
{
- "LambdaFunctionARN": "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:CFRedirectIndexDocument:29",
+ "FunctionARN": "arn:aws:cloudfront::XXXXXXXXXXXX:function/cf2-redirect",
"EventType": "viewer-request",
"IncludeBody": false
}
]
},
- "FunctionAssociations": {
- "Quantity": 0
- },
"FieldLevelEncryptionId": "",
"ForwardedValues": {
"QueryString": true,
更新用の JSON ができたので、 cloudfront update-distribution
コマンドで更新します。
$ aws cloudfront update-distribution \
--id ${CF_DIST_ID} \
--if-match $( \
aws cloudfront get-distribution \
--id ${CF_DIST_ID} \
--query "ETag" \
--output text) \
--distribution-config file://distribution-config-update.json
まとめ
CloudFront Functions を AWS CLI で触ってみるついでに、ブログの URL 正規化処理を Lambda@Edge から移行した話でした。
CloudFront Functions は Lambda@Edge と比較して、よりクライアントに近いところで動くため、レスポンスの高速化が期待できます。また、料金についても安くなり、今回の移行により毎月 Lambda@Edge で 0.1 USD くらいかかっていたのが、おそらく無料枠の範囲内に収まりそうです。
2,000,000 CloudFront Function Invocations
Invocation pricing is $0.10 per 1 million invocations ($0.0000001 per request).CDN Pricing | Free Tier Eligible, Pay-as-you-go | Amazon CloudFront
Lambda@Edge と比較して実行時間やメモリ、パッケージサイズの制限は厳しいですが、今回のようなパスルーティング程度の処理であればメリットしか無いので、すぐにでも移行するのが良いかなと思いました。
comments powered by Disqus