Amazon Web Services ブログ

Visitor PrioritizationソリューションをCloudFront Functionsを使って実装するための考慮点 Part2

2021年5月に「Visitor PrioritizationソリューションをCloudFront Functionsを使って実装するための考慮点」の Blog を公開しました。以降、多くのお客様、システムにおいて CloudFront Functions を使った活用がされており、チケットの購入や予約などに関わるシステムにおいて利用されているユースケースが増えております。年末に向けて多くのお客様においてイベントやチケット販売などにおいて利用してみたいというご要望を受ける機会が増えておりますので、改めてこのソリューションについて紹介していきたいと思います(一部再掲を含みます)。

動作するイベントポイントについて

CloudFront Functionsの紹介ブログにより詳細な内容が載っているのでここでは詳しく説明しませんが、Lambda@EdgeとCloudFront Functionsは動作するイベントポイントが異なります。以下の図のようにLambda@EdgeはViewerフェーズ、Originフェーズの双方においてリクエスト/レスポンスのイベントで利用することができますが、CloudFront FunctionsはViewerフェーズのリクエスト/レスポンスのイベントでのみ使用することができます。

Viewer/Origin Phase CFF

このソリューションを利用するための事前条件

CloudFront Functions の作成

以下のコードを利用しCloudFront Functions を作成します。

/*
 * A flag indicating whether the origin is ready to accept traffic.
 * Unlike Lambda@Edge, CloudFront Functions doesn't support network call.
 * So if you want to change this value, you need to modify then re-deploy
 * this function.
 */
var originAcceptingTraffic = true;

/*
 * The origin hit rate (a value between 0 and 1) specifies a percentage of
 * users that go directly to the origin, while the rest go to
 * a "waiting room." Premium users always go to the origin.  if you want to
 * change this value, you need to modify then re-deploy this function.
 */
var originHitRate = 0.3;

/*
 * Waiting Room Redirect URL
 */

var FullClose = `https://FullCLOSE SITE` // Change the redirect URL to your choice

function handler(event) {
    var request = event.request;
    var uri = event.request.uri;
    var cookies = event.request.cookies;
    var premiumUserCookieValue = 'some-secret-cookie-value';


    if(!originAcceptingTraffic) {
        console.log("Origin is not accepting any traffic. " +
                    "All requests go to the Full close waiting room.");
        var response = {
                 statusCode: 302,
                 statusDescription: 'Found',
                 headers:
                         { "location": { "value": FullClose } }
                     }
        return response;
    }

    // Check Whether Cookie is available or not.
    // in this sample it checks premium-user-cookie. This name is case
    // sensitive, so if you use upper charactor, please modify name parameter.
    if(cookies.hasOwnProperty("premium-user-cookie") && cookies["premium-user-cookie"].value === premiumUserCookieValue){
        console.log(`Verified Premium user cookie, this request goes to Origin cause it has Cookie with a valid secret value of "${premiumUserCookieValue}".`);
        return request;
      }

    // Lotterly to check go to origin
    if (Math.random() >= originHitRate) {
        console.log("An unlucky user goes to the waiting room.");
        request.uri = '/waitingroom.html';
        return request;
    }
    console.log("A lucky user goes to the origin.");
    return request;
};

動作をカスタマイズしたい場合は以下の値をテキストエディタなどで編集を行います。

  • originAcceptingTraffic : デフォルト値はTrueに設定されています。もし、オリジンサーバーが過負荷などでリクエストを受け入れ不能の場合はFalseに設定します。
  • originHitRate : オリジンに転送されるリクエストの割合を0から1の値で設定します。この例では0.3なので30%のトラフィックがWaiting Roomのページに誘導されます。
  • FullClose : 予約の受け入れ前や過負荷による受け入れ停止などの理由でoriginAcceptingTrafficの値をFalseに設定した場合において、誘導するページのURLを記載します。

CloudFront Functionを作成するための手順は以下のとおりです。

  1. CloudFront consoleから create CloudFront Function を選択し、上記のコードをコピーし貼り付けます。
  2. Stageに対してデプロイを行い、Save changes ボタンをクリックし保存します。

CloudFront Functions のテスト

CloudFront Functions のデプロイが完了したら、本番環境にデプロイする前にテストコンソールを利用することで動作確認を行うことができます。

テストケース1: premium cookie の値ありのユースケース

  1. CloudFront Functions Test タブの表示
    • CloudFront Functions のページで Test タブを開きます。
  2. イベントタイプの選択
    • 上述の通りCloudFront Functions は Viewer のリクエスト及びレスポンスでのみ動作します。ここでは Viewer RequestEvent Type を選択します。
  3. ステージの選択
    • 本番環境にデプロイ前なので DevelopmentStage を選択します。
  4. リクエストパラメータの設定(以下の値を選択)
    • HTTP Method : GET
    • URL Path : デフォルトページを選択 (/index.htmlなど)
    • IP address : 選択なし
  5. Cookie 値の設定(以下の値を選択)
    • Name : premium-user-cookie
    • Value : some-secret-cookie-value
    • Attributes : 空白
  6. テストの実行
    • “Test function” をクリックすると、以下のような uriOutput の値が表示されます。

Test case 2: premium cookie の値なしのユースケース

上記からCookieの値を除外し、premium cookie の値なしのユースケースをテストします。この場合は約30%の確率で uri の値が /waitingroom.html になることが確認できます(この値は originHitRate の値を変更することで0-100%の値で変更することが可能です)。

CloudFront のDistributionへの適用

上記のテストなどで設定値の確認が完了したら、CloudFront への適用を行います。

Step 1: CloudFront Functions のステージの変更

  1. Functions のページから Publish タブを選択します。
  2. Publish function ボタンを押下し、コードをデプロイします。

Functionをパブリッシュし動作の確認が完了したら、CloudFront Functions をディストリビューションへの関連付けをおこないます。

Step 2: Functionの関連付け

  1. Add association ボタンを押下します。
    1. Distribution : 関連付けを行いたい CloudFront のディストリビューションIDを選びます。
    2. Event type : Viewer Request を選択します
    3. Cache behavior : Default(*) もしくは適切なビヘイビアを選択します。
    4. Add association ボタンを押下します。
  2. 以上が完了したらCloudFrontへのデプロイの完了を待ちます。

デプロイが完了したらCloudFront Function が適用されるので、利用が可能となります。

CloudFormation Template.

CloudFront の設定をお持ちでない場合などでは、以下の CloudFormation テンプレートを利用することで確認できます。このテンプレートでは以下の設定を行います。

  • Origin Access Identity を有効化したS3バケット作成
  • 上記の S3 バケットを利用したCloudFront Distribution の作成
  • CloudFront Functions の作成

以下の CloudFormation をローカルに保存して、実行してください。

AWSTemplateFormatVersion: "2010-09-09"
Description:
  Creating CloudFront with OAI enabled S3 Origin

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "S3 and CloudFront Configuration"
        Parameters:
          - BucketName
      - Label:
          default: "CloudFront Functions Name"
        Parameters:
          - CFFunctionName

    ParameterLabels:
      BucketName:
        default: "BucketName"
      CFFunctionName:
        default: "CFFunctionName"


Parameters:
  BucketName:
    Type: String
  CFFunctionName:
    Type: String

Resources:
  Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Ref BucketName

  CloudFrontOriginAccessIdentity:
    Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity"
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Sub "access-identity-${Bucket}"

  BucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref Bucket
      PolicyDocument:
        Statement:
        - Action: "s3:GetObject"
          Effect: Allow
          Resource: !Sub "arn:aws:s3:::${Bucket}/*"
          Principal:
            CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId

  WaitingRoomFunction2020:
    Type: AWS::CloudFront::Function
    Properties:
      Name: !Ref CFFunctionName
      AutoPublish: true
      FunctionCode: |
        /*
         * A flag indicating whether the origin is ready to accept traffic.
         * Unlike Lambda@Edge, CloudFront Functions doesn't support network call.
         * So if you want to change this value, you need to modify then re-deploy
         * this function.
         */
        var originAcceptingTraffic = true;

        /*
         * The origin hit rate (a value between 0 and 1) specifies a percentage of
         * users that go directly to the origin, while the rest go to
         * a "waiting room." Premium users always go to the origin.  if you want to
         * change this value, you need to modify then re-deploy this function.
         */
        var originHitRate = 0.3;

        /*
         * Waiting Room Redirect URL
         */

        var FullClose = `https://FullCLOSE SITE` // Change the redirect URL to your choice
        
        function handler(event) {
            var request = event.request;
            var uri = event.request.uri;
            var cookies = event.request.cookies;
            var premiumUserCookieValue = 'some-secret-cookie-value';

            if(!originAcceptingTraffic) {
                console.log("Origin is not accepting any traffic. " +
                            "All requests go to the Full close waiting room.");
                var response = {
                         statusCode: 302,
                         statusDescription: 'Found',
                         headers:
                                 { "location": { "value": FullClose } }
                             }
                return response;
            }

            if(cookies.hasOwnProperty("premium-user-cookie") && cookies["premium-user-cookie"].value === premiumUserCookieValue){
                console.log(`Verified Premium user cookie, this request goes to Origin cause it has Cookie with a valid secret value of "${premiumUserCookieValue}".`);
                return request;
              }

            // Lotterly to check go to origin
            if (Math.random() >= originHitRate) {
                console.log("An unlucky user goes to the waiting room.");
                request.uri = '/waitingroom.html';
                return request;
            }
            console.log("A lucky user goes to the origin.");
            return request;
        };
      FunctionConfig:
        Comment: waitingroom-functions
        Runtime: cloudfront-js-1.0


  CloudFrontDistribution:
    Type: "AWS::CloudFront::Distribution"
    Properties:
      DistributionConfig:
        PriceClass: PriceClass_All
        Origins:
        - DomainName: !GetAtt Bucket.RegionalDomainName
          Id: !Sub "S3origin-${BucketName}"
          S3OriginConfig:
            OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          TargetOriginId: !Sub "S3origin-${BucketName}"
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
          - GET
          - HEAD
          CachedMethods:
          - GET
          - HEAD
          DefaultTTL: 600
          MaxTTL: 600
          MinTTL: 600
          Compress: true
          ForwardedValues:
            Cookies:
              Forward: none
            QueryString: false
          FunctionAssociations:
          - EventType: viewer-request
            FunctionARN:
              Fn::GetAtt:
                - WaitingRoomFunction2020
                - FunctionARN
        HttpVersion: http2
        Enabled: true

Outputs:
  BucketName:
    Value: !Ref Bucket
  DomainName:
    Value: !GetAtt CloudFrontDistribution.DomainName
  DistributionID:
      Value: !Ref CloudFrontDistribution

テストの実行

デプロイが完了したら、cURLなどのツールを利用することで確認することができます。

$ curl -i https://XXXXXXXXXX.cloudfront.net
HTTP/2 200 
content-type: text/html
content-length: 14
last-modified: Mon, 02 Aug 2021 04:10:48 GMT
accept-ranges: bytes
server: AmazonS3
date: Mon, 02 Aug 2021 05:03:53 GMT
etag: "25e8f2fd2871c8423bbe4e254066cd98"
x-cache: Hit from cloudfront
via: 1.1 4004d5f75919e4406a8e631c774796f5.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-C4
x-amz-cf-id: Rkh-XCJx409E1r7kvjJ7ka_WKwKIzWAV1LKn9D1-WasSXR9rYJejbQ==

Please wait...(Waiting Room)

Conclusion

CloudFront Functions はチケットの予約など定常時よりも多くのリクエストが瞬間的に発生するユースケースにおいて流量制御を行いたい場合には非常に有効なソリューションを提供することができます。ここではトラフィックの誘導を行う例を説明していますが、他のユースケースにおいても同様に CloudFront でCloudFront Functions または Lambda@Edge を使うことで様々なコンピューティングを行うことができます。ぜひ、ここでご紹介したような流量制御以外のユースケースでも利用していただき、CloudFront Functions 、Lambda@Edgeの利用をご検討ください。

このブログの著者

中谷 喜久 (Yoshihisa Nakatani)
Solutions Architect

藤原 吉規 (Yoshinori Fujiwara)
Solutions Architect

森 啓 (Akira Mori)
Solutions Architect