インフィニットループ 技術ブログ

2022年12月11日 (日)

著者 : to-kimura

CloudFront Functionsのいい感じの使い方を考えてみる

この記事はAWS for Games Advent Calendar 2022の11日目の記事です。

仙台支社の木村です。
いつの間にか寒くなってきて、外では毎年やってるイルミネーションのイベントの点灯テストをやってたりしています。なんだかんだで、今年はどんな感じなのか少し楽しみにしている自分がいます。

さて、冒頭でも触れましたがこの記事はAWS for Games Advent Calendar 2022の11日目の記事です。
最近CloudFront Functionsに触る機会がありましたので、今回はその簡単な概要を説明したり各種サンプルコードを提示し、どんな感じで使えそうかという話を書いていきます。

そもそもCloudFront Functionsとは

簡単に言うと、コードをCloudFront Distributionに紐付け、それを各エッジロケーションで動作させる実行環境です。エッジ関数とも呼んだりします。
Webブラウザやアプリといったクライアントとオリジンの間に挟まって動作することで、認証認可やレスポンスの生成といったちょっとしたカスタマイズをすることができます。

詳しい方であればLambda@Edgeもご存じかと思いますが、主に次の点が異なります。

  • 言語環境が異なる
    • CloudFront Functionsは制約のある(後述)JavaScriptで動作するが、Lambda@EdgeはNode.jsやPythonが利用可能。
  • 利用できるメモリ数や実行時間、関数サイズが異なる
    • CloudFront Functionsは関数サイズは10KBまで、Lambda@Edgeは1MB~50MBまで。
  • スケール量が段違い
    • CloudFront Functionsは毎秒1000万リクエストを捌けるが、Lambda@Edgeは毎秒1万リクエストまで。
  • 料金体系も異なる
    • CloudFront Functionsはリクエスト数のみ、Lambda@Edgeはリクエスト数と実行時間。
  • 処理できるデータも異なる
    • CloudFront Functionsはリクエスト及びレスポンスのヘッダのみ取れるが、Lambda@Edgeではそれに加えて本文も取得可能。

他にもありますが、詳しくはドキュメントを参照していただければと思います。

つまり、CloudFront Functionsは性能はありますがその分ピーキーな環境になっているので、本当にちょっとした処理を動かすのに特化しています。制約もありますし。
それに対してLambda@Edgeはその名の通り、CloudFront上で実行できるLambda関数と捉えるとわかりやすいかもしれません。

CloudFront FunctionsにおけるJavaScript

先に書いたように、CloudFront Functionsでは言語として、制約があるJavaScriptが利用可能です。
この制約があるというのが鬼門で、次のような仕様になっています。

  • ECMAScript 5.1準拠
  • ECMAScript 6(2015)~9(2018)の機能を一部サポート

この仕様で一番の制約となるのは let / const が利用不可という点です。モダンなJavaScriptやTypeScriptを書いているとクセで使いそうになりますが、ここでは懐かしの var を使う必要があります。

他のサポート状況はドキュメントに載っていますので、合わせて参照してください。 また、関数サイズの制限(10KB)を考えると、外部ライブラリはほぼ利用不可能と思っていた方が良いでしょう。

つまり、ピュアなJavaScriptでピュアな処理を書く必要があります。

いい感じの使い方の例

さて、ここからはいくつかサンプルを提示してみたいと思います。
今回CloudFront自体の細かい設定は記載しませんが、オリジンはS3の想定です。なので、アセットの配信や簡易的なWebページのホスティング的な用途を想定しています。

IPアドレスによる制限

まずはIPアドレスによる制限です。単純に、認可されたネットワーク外からのアクセスを弾きたい、みたいなシンプルな制限に使えそうです。

function handler(event) {
    var request = event.request;
    
    var client_ip = event.viewer.ip;
    var allow_ips = ['192.0.2.1', '198.51.100.2', '203.0.113.1'];
    
    if (allow_ips.indexOf(client_ip) == -1) {
        return {
            statusCode: 403,
            statusDescription: 'Access Denied'
        };
    }
    
    return request;
}

コードはすごいシンプルです。allow_ipsに許可したいIPの一覧が入っていて、その中に接続元のIPが入っていなければ403 Access Deniedを返して拒否します。
許可IPアドレスは/32前提なので、/24のような指定をしたい場合は、CIDRのチェック処理を入れる必要があります。

ただ、許可先が多い場合はWAFv2のIPセット一致ルールの方が便利かもしれません。この例はあくまでシンプルな制限の場合、ということで…。

IPアドレスによるパス変更

次はIPアドレスによるパス変更です。「社内ネットワーク端末からのアクセスだけ違うアセットにしたい」とか、「作成中のお知らせを表示したい」みたいな用途にはよさそうです。

function handler(event) {
    var request = event.request;
    
    var client_ip = event.viewer.ip;
    var allow_ips = ['192.0.2.1', '198.51.100.2', '203.0.113.1'];
    var add_path = '/customize'
    
    if (allow_ips.indexOf(client_ip) !== -1) {
        request.uri = add_path + request.uri;
    }
    
    return request;
}

先ほどの派生系です。
allow_ipsにある範囲のIPアドレスであれば、パスの頭に/customizeをくっ付けてアクセスするようにします。

ヘッダによる認証とコードの検証

次はヘッダによる認証とコードの検証です。「このご時世リモートだけど、色々あってIPアドレスによる制限はちょっと…」「でもJWTにする程じゃない」という時にいかがでしょうか。

var crypto = require('crypto');

var algorithm = 'sha256';
var hashed_key = '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8';

var failed_response = {
    statusCode: 403,
    statusDescription: 'Access Denied'
};

function handler(event) {
    var request = event.request;
    var header_key = request.headers['x-auth-key'];
    
    if (typeof header_key === 'undefined' || header_key === null) {
        return failed_response;
    }
    
    if (hashed_key !== getHashedKey(header_key.value)) {
        return failed_response;
    }
    
    delete request.headers['x-auth-key'];
    
    return request;
}

function getHashedKey(key) {
    return crypto.createHash(algorithm).update(key).digest('hex');
}

最初にピュアなJavaScriptで書くという話をしましたが、舌の根が乾かぬうちにrequireが登場していますね。CloudFront Functionsにはありがたいことにいくつかモジュールが搭載されていて、そのうちの1つがcryptoです。これを使ってハッシュ値の計算をします。(ドキュメントはこちら)

処理としてはx-auth-keyに入っているキーを取得し、その中身からsha256でハッシュ値を取得して、スクリプトに埋め込まれたハッシュ値と照らし合わせています。最後に念のためヘッダを削除してオリジンへ送信します。
ヘッダがない場合やハッシュ値が合わない場合は403 Access Deniedを返します。

ちなみに、何の文字列をハッシュにして埋め込んでいるかは想像にお任せします。

特定のURIへのアクセスをリダイレクトする

最後にリダイレクトです。「埋め込めるURIは先に決まってしまっているので、具体的なURIが決まればリダイレクトしたい」「間違ったURIにしたので取り急ぎリダイレクトしたい」という用途にぴったりです。

function handler(event) {
    var request = event.request;
    var uri = request.uri;
    
    var uri_maps = [
        ['/event1', '/2022/01/02/event1-detail'],
        ['/wrong-uri', '/correct-uri'],
        ['/external-uri', 'https://www.example.com/']
    ];
    
    for (var idx in uri_maps) {
        if (uri == uri_maps[idx][0]) {
            return {
                statusCode: 302,
                statusDescription: 'Found',
                headers: {
                    'location': {'value': uri_maps[idx][1]}
                }
            };
        }
    }
    
    return request;
}

uri_mapsに旧URIと新URIのマップを作成し、それらをfor-inで回します。旧URIとリクエストされたURIがマッチすれば、新URIへのリダイレクトをするようレスポンスを返します。
ここでは302 Foundを返していますが、307 Temporary Redirectでも問題なさそうです。
(両者の違いは、307の場合リダイレクト後にメソッドと本文が同一であるという保障があるという点で、GETの場合はどちらでも挙動が変わらないため)

ちなみに、uri_mapsに対して、for-inではなく中身をダイレクトに取り出すfor-ofを使えばいいのでは?と思われるかもしれませんが、for-ofはECMAScript 6(2015)からの構文のためサポートされていません。悲しい。

おわりに

ここまで、CloudFront Functionsの簡単な概要を説明したり、各種サンプルコードを提示しました。
コードが書けるので自由度はありつつ、言語やプラットフォームとしての仕様の縛りがあるため、どのように使えるかイメージがつかないというパターンも多いかと思います。今回は具体的なコードや想定使用例も合わせて紹介できましたので、どんな感じで運用できるのか、という雰囲気だけでも伝われば幸いです。
制限が多すぎる環境ではありますが、処理速度の速さは魅力的なはずですので、是非お使いのCloudFrontを一歩先へアップグレードしてみてはいかがでしょうか。

最後に、弊社ではxR/サーバーサイドに負けず劣らず、AWSをはじめとしたクラウドサービスの知見があるエンジニアも募集しています。よろしくお願いします。→ 採用情報

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!