CloudFront FunctionsはLambda@Edgeより安い。それ本当?!

CloudFront FunctionsよりLambda@Edgeの方が低コストな場合もあるという話です
2021.06.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部@大阪の岩田です。先月GAされたCloudFront Functions(以後CF2とします)ですが、従来から利用できたLambda@Edge(以後L@Eとします)と比較して、高速かつ低コストというのがウリの1つになっています。最大実行時間や最大メモリといった制約はありますが、レスポンスヘッダを固定付与するようなライトな処理はCF2のユースケースとしてAWSの公式ドキュメントでも紹介されています。

Customizing at the edge with CloudFront Functions

では今後はレスポンスヘッダの固定付与のような処理は全てCF2で実装すべきなのでしょうか?基本的にはCF2で良いと思いますが、使い方次第ではCF2よりもL@Eの方が低コストになることも考えられるので、なぜこのようなケースが起こり得るのかをご紹介します。

CF2とL@Eの料金体系比較

まず2つのサービスの料金体系を比較してみます。料金は2021/6時点の東京リージョンでの料金となります。

CF2

CF2はシンプルに

  • 呼び出し 1,000,000 件あたり 0.10 USD

です。1年間は2,000,000件/月の無料利用枠も存在します。

Amazon CloudFront の料金

L@E

L@Eはリクエスト数に応じた課金と、メモリの使用時間に対する課金があり、それぞれ

  • リクエスト 1,000,000 件あたり 0.60USD
  • GB-秒あたり 0.00005001USD

となります。そして無料利用枠はありません。

AWS Lambda 料金

この情報だけ見ると、CF2のコストはL@Eの6分の1以下になりそうです。が、実際にはもう少し考慮事項が必要になります。

CF2とL@Eの違い

CF2とL@Eの機能には様々な違いがあります。詳しくは以下のブログを参照して下さい。

今回注目したいのが関数をトリガーするイベントです。CF2はビューワーリクエストとビューワーレスポンスの2つしかサポートしませんが、L@Eはこれら2つに加えてさらにオリジンリクエストとオリジンレスポンスの2つがサポートされています。このビューワーXXXとオリジンXXXの違いですが、ビューワーxxxはCloudFrontのキャッシュヒット有無に関わらず関数実行が実行されるのに対し、オリジンXXXはリクエストされたオブジェクトがCloudFrontのキャッシュにヒットしなかった場合のみ実行されるという特徴があります。オリジンレスポンスをトリガーにL@Eを起動してレスポンスヘッダを書き換えた場合、Cloud Frontは書き換え後のレスポンスをキャッシュします。そしてキャッシュが有効な間は以後の同一オブジェクトに対するリクエストはキャッシュから返却し、オリジンへのリクエストは発生しません。例えばindex.htmlというオブジェクトに対して100万件のリクエストが発生し、うち初回の1回のみキャッシュミスが発生するとします。この場合、オリジンレスポンスを使用してレスポンスヘッダを書き換えるとL@Eの起動回数は1回だけで済みますが、ビューワーレスポンスを使用してレスポンスヘッダを書き換えるとL@E(もしくはCF2)は100万回起動することになります。

関数呼び出しの回数が同一であればL@EよりCF2の方が安上がりですが、オリジンレスポンスからトリガーできるL@EはCF2よりも関数呼び出しの回数そのものを削減できる可能性があり、必ずしもL@EよりCF2の方が低コストになるというわけではないのです。

実行回数を比較してみる

理屈は分かったので、実際に動作を比較してみます。

CFのオリジンに静的WEBサイトホスティングを有効化したS3を設定した定番の環境を構築して静的ファイルを配信

  • オリジンレスポンスからトリガーしたL@E
  • ビューワーレスポンスからトリガーしたCF2

それぞれでレスポンスヘッダを設定しつつ、各関数の実行回数を比較します。

L@Eでレスポンスヘッダを設定した場合

Node.js14xで以下のLambdaを用意してCFのオリジンレスポンスに設定します。L@EとCF2のユースケースとしてよく紹介される、セキュリティ関連のレスポンスヘッダを設定するコードです。

'use strict';
exports.handler = (event, context, callback) => {
    
    const response = event.Records[0].cf.response;
    const headers = response.headers;

     headers['strict-transport-security'] = [{key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload'}]; 
     headers['content-security-policy'] = [{key: 'Content-Security-Policy', value: "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'"}]; 
     headers['x-content-type-options'] = [{key: 'X-Content-Type-Options', value: 'nosniff'}]; 
     headers['x-frame-options'] = [{key: 'X-Frame-Options', value: 'DENY'}]; 
     headers['x-xss-protection'] = [{key: 'X-XSS-Protection', value: '1; mode=block'}]; 
     headers['x-response-from'] = [{key: 'X-Response-From', value: 'lae'}]; 
     callback(null, response);
};

L@Eからのレスポンスということが分かりやすいようにセキュリティ関連のヘッダに加えてX-Response-Fromというヘッダにlaeとセットしています。軽くcurlで動作確認してみます

$ curl https://xxxxxx.cloudfront.net/lae/index.html --dump-header - -s -o /dev/null
HTTP/2 200
content-type: text/html
content-length: 13
date: Sun, 20 Jun 2021 02:10:56 GMT
last-modified: Sat, 19 Jun 2021 11:48:51 GMT
etag: "01bcb1fe182a23a65c5efe8326250da8"
server: AmazonS3
strict-transport-security: max-age=63072000; includeSubdomains; preload
content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
x-response-from: lae
x-cache: Miss from cloudfront
via: 1.1 xxxxxx.cloudfront.net (CloudFront)
x-amz-cf-pop: KIX56-C1
x-amz-cf-id: LiFeUzLkJrXDnxgbi1zXLHhCqbRSDCSAakA7Wr4RMMIFIeAeXWeWiQ==

L@Eで設定したセキュリティ関連のヘッダがレスポンスされています。x-cacheMiss from cloudfrontとなっており、キャッシュにヒットしていないことが分かります。再度同じコマンドを実行してみます。

$ curl https://xxxxxx.cloudfront.net/lae/index.html --dump-header - -s -o /dev/null
HTTP/2 200
content-type: text/html
content-length: 13
date: Sun, 20 Jun 2021 02:10:56 GMT
last-modified: Sat, 19 Jun 2021 11:48:51 GMT
etag: "01bcb1fe182a23a65c5efe8326250da8"
server: AmazonS3
strict-transport-security: max-age=63072000; includeSubdomains; preload
content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
x-response-from: lae
x-cache: Hit from cloudfront
via: 1.1 xxxxxx.cloudfront.net (CloudFront)
x-amz-cf-pop: KIX56-C1
x-amz-cf-id: TxAcvdc_5Ml98Ljroqh3OpfFT0sx315LzrJqY8gBAADa8vLMx2tKuA==
age: 124

先ほどと同様セキュリティ関連のヘッダが設定されていますが、今度はx-cache: Hit from cloudfrontとなっており、キャッシュからレスポンスが返却されていることが分かります。つまり。L@Eは起動していないはずです。

続いてabコマンドで10万回ほどリクエストを発行してみます。

$ ab -n 100000 -c 100  https://xxxxxx.cloudfront.net/lae/index.html
This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking xxxxxx.cloudfront.net (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests


Server Software:        AmazonS3
Server Hostname:        xxxxxx.cloudfront.net
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key:        ECDH P-256 256 bits
TLS Server Name:        xxxxxx.cloudfront.net

Document Path:          /lae/index.html
Document Length:        13 bytes

Concurrency Level:      100
Time taken for tests:   68.481 seconds
Complete requests:      100000
Failed requests:        0
Total transferred:      74485283 bytes
HTML transferred:       1300000 bytes
Requests per second:    1460.25 [#/sec] (mean)
Time per request:       68.481 [ms] (mean)
Time per request:       0.685 [ms] (mean, across all concurrent requests)
Transfer rate:          1062.18 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       11   40   6.8     38     118
Processing:     7   29   5.5     30     142
Waiting:        3   11   2.9     10     140
Total:         38   68   8.2     66     184

Percentage of the requests served within a certain time (ms)
  50%     66
  66%     71
  75%     75
  80%     76
  90%     78
  95%     80
  98%     87
  99%     91
 100%    184 (longest request)

しばらくしてからCloudWatchのメトリクスを確認します

L@Eは1回しか実行されていないことが分かります。

CF2でレスポンスヘッダを設定した場合

続いてCF2で同様の処理を実装してみます。関数のコードは以下です。

function handler(event) {
    var response = event.response;
    var headers = response.headers;

    headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; 
    headers['content-security-policy'] = { value: "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'"}; 
    headers['x-content-type-options'] = { value: 'nosniff'}; 
    headers['x-frame-options'] = {value: 'DENY'}; 
    headers['x-xss-protection'] = {value: '1; mode=block'}; 
    headers['x-response-from'] = {value: 'cf2'}; 
    return response;
}

X-Response-Fromというヘッダにはcf2とセットしています。こちらの関数をビューワーレスポンスからトリガーするように設定した後L@Eと同様にcurlで動作確認してみます。

$ curl https://xxxxxx.cloudfront.net/cf2/index.html --dump-header - -s -o /dev/null
HTTP/2 200
content-type: text/html
content-length: 13
date: Sun, 20 Jun 2021 02:17:02 GMT
last-modified: Sat, 19 Jun 2021 11:48:51 GMT
etag: "01bcb1fe182a23a65c5efe8326250da8"
server: AmazonS3
via: 1.1 xxxxxx.cloudfront.net (CloudFront)
content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'
x-response-from: cf2
strict-transport-security: max-age=63072000; includeSubdomains; preload
x-xss-protection: 1; mode=block
x-frame-options: DENY
x-content-type-options: nosniff
x-cache: Miss from cloudfront
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: deWrm4d4pZqZ0_9hYaAhk2LcyGSU_VAJHgZsvSkhg-j6P1e1uKzbTg==

L@Eと同様にセキュリティ関連のヘッダが設定できていますね。もう1度同じコマンドを実行します。

$ curl https://xxxxxx.cloudfront.net/cf2/index.html --dump-header - -s -o /dev/null
HTTP/2 200
content-type: text/html
content-length: 13
date: Sun, 20 Jun 2021 02:17:02 GMT
last-modified: Sat, 19 Jun 2021 11:48:51 GMT
etag: "01bcb1fe182a23a65c5efe8326250da8"
server: AmazonS3
via: 1.1 xxxxxx.cloudfront.net (CloudFront)
content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'
x-response-from: cf2
strict-transport-security: max-age=63072000; includeSubdomains; preload
x-xss-protection: 1; mode=block
x-frame-options: DENY
x-content-type-options: nosniff
x-cache: Hit from cloudfront
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: 6LZw6YXuhwAHaj5KaFIXtPTLJyI_1bugEcbY_CVTdenG8vsJuH6UzA==

x-cache: Hit from cloudfrontのレスポンスが返却されており、キャッシュにヒットしていることが分かります。キャッシュヒット人もL@Eと同様のレスポンスヘッダがセットされています。

先程のL@Eと同様abコマンドで10万回ほどリクエストを発行してみます。

$ ab -n 100000 -c 100  https://xxxxxx.cloudfront.net/cf2/index.html
This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking xxxxxx.cloudfront.net (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests


Server Software:        AmazonS3
Server Hostname:        xxxxxx.cloudfront.net
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key:        ECDH P-256 256 bits
TLS Server Name:        xxxxxx.cloudfront.net

Document Path:          /cf2/index.html
Document Length:        13 bytes

Concurrency Level:      100
Time taken for tests:   69.183 seconds
Complete requests:      100000
Failed requests:        0
Total transferred:      73600001 bytes
HTML transferred:       1300000 bytes
Requests per second:    1445.44 [#/sec] (mean)
Time per request:       69.183 [ms] (mean)
Time per request:       0.692 [ms] (mean, across all concurrent requests)
Transfer rate:          1038.91 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        7   40   6.7     38     194
Processing:     8   29   5.5     30     152
Waiting:        4   11   3.4     10     149
Total:         40   69   8.4     67     232

Percentage of the requests served within a certain time (ms)
  50%     67
  66%     73
  75%     76
  80%     77
  90%     79
  95%     81
  98%     90
  99%     92
 100%    232 (longest request)

CloudWatchのメトリクスを確認してみましょう

curlコマンドによるリクエスト×2回 + abコマンドによるリクエスト×10万回で100,002回実行されていることが分かります。

料金比較

オリジンレスポンスはCloudFrontにキャッシュされるという特性から、L@Eの方が実行回数を抑えられることが分かりました。今回の検証にかかった料金を試算してみましょう。

L@Eの料金

今回L@Eのメモリ割り当ては128M、CW Logsから確認した実行時間は80msでした。

  • 呼び出しに対する課金:1回 / 100万回 × 0.6USD  → 0.0000006USD
  • メモリの使用時間に対する課金:(128M/1024M) × (80ms/1000ms) × 0.00005001USD → 0.0000005001USD

合計で 0.0000006 + 0.0000005001 → 0.0000011001USD となる試算です。

CF2の料金

無料利用枠は無視し、実行回数10万2回のうち半端な2回分の実行は無視して計算します。

  • 呼び出しに対する課金: 10万回 / 100万回 × 0.10USD → 0.01USD

合計で0.01USDになる試算です。

今回の検証と同等のキャッシュヒット率と実行回数であればCF2よりもL@Eの方が安くなることが分かります。

まとめ

CF2よりもL@Eの方が低コストになる場合があることを紹介しました。そうはいってもCF2の料金は十分に安く、今回の検証にかかったCF2の料金は1円程度です。CF2で処理できる内容であれば基本的にはCF2を選択しつつ、アクセス数=関数の実行回数が大きくなることが想定される場合はL@Eを検討するぐらいの使い方でも問題無さそうです。実際は他にもキャッシュの有効期限やキャッシュヒット率、ブラウザキャッシュを考慮する必要がありますし、L@Eのコールドスタートによるレイテンシへの悪影響にも注意が必要です。単純にコストだけでL@Eを選択するのも良い選択とは言えません。CF2とL@E両者の特性をしっかり抑えた上で適切なサービスを選択するようにしましょう。