CloudFront FunctionsでIPアドレス制限をする CloudFormation templateの書き方例

はじめに
========
CloudFront Functionsは CloudFront上で実行できる小さなFunctionで、Lambda@Edge よりも軽量な処理をちょっと行いたいときに便利そうです。
例えば、あるURLに対してIPアドレス制限をする、という用途が考えられます。
ググると色々記事もあります（例: [CloudFront FunctionsでIP制限を試してみた](https://dev.classmethod.jp/articles/cloudfront-functions-ip-control/)）。
この方法は、IPアドレスを変更するたびにCloudFront Functionをデプロイしなおさないといけないので、社内のIPアドレスなどほぼ変化しないようなケースでしか使いづらいです。が、WAFを使う方法に比べて、お手軽に安くできるというメリットはあるかなと思います。


今回は CloudFormation でどう書いたら良いのか調べてみたので、書き残しておきます。

CloudFormation templateの書き方例
============
例としては以下のようになります。
ちょっと説明を書いておきます。

- `www.example.com` への CloudFront に対する設定
- `/faq/` ページは `123.45.67.89` というIPからのみアクセスを許可
    - 他のページは、全てアクセス許可
- `CachePolicy` は適当なので、用途に応じて変える必要がある
- `CacheBehaviors` を追加していくことで、違うPathへの違う制限をすることができる


```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: CloudFront with CloudFunction example

Parameters:
    OriginName: 
      Type: String
      Default: www.example.com
    
    AllowIPRegexp:
      Type: String
      Default: "^(123[.]45[.]67[.]89)$"

Outputs:
  CDNUrl:
    Description: CDN URL
    Value: !Sub "https://${CloudFrontDistrib.DomainName}/"

Resources:
  DefaultCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties: 
      CachePolicyConfig: 
        Name: SimpleConfig
        DefaultTTL: 3600
        MinTTL: 600
        MaxTTL: 86400
        ParametersInCacheKeyAndForwardedToOrigin:
          CookiesConfig:
            CookieBehavior: all
          EnableAcceptEncodingGzip: true
          HeadersConfig:
            HeaderBehavior: none
          QueryStringsConfig:
            QueryStringBehavior: all

  IPRestrictionFunction:
    Type: AWS::CloudFront::Function
    Properties:
      Name: IPRestriction
      AutoPublish: true
      FunctionConfig:
        Comment: IP-Restriction-Function
        Runtime: cloudfront-js-1.0
      FunctionCode: !Sub |
        function handler(event) {
          var request = event.request;
          var clientIP = event.viewer.ip;
          var regexp = new RegExp("${AllowIPRegexp}");

          if (!regexp.test(clientIP)) {
            var response = {
              statusCode: 403,
              statusDescription: "Forbidden",
              // debug用に余計なHeaderを追加してみる
              headers: {
                "x-cloudfrontfunction-version": {value: "IPRestriction-Ver1"},
                "x-ip-denied-from-cloudfront": {value: clientIP},
              }
            };
            return response;
          }

          return request;
        }
      
  CloudFrontDistrib:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          -
            DomainName: !Ref OriginName
            Id: OriginGW
            CustomOriginConfig:
              OriginProtocolPolicy: "match-viewer"
              OriginSSLProtocols: [ "TLSv1.2" ]
        Comment: CloudFront distribution
        Enabled: true
        DefaultCacheBehavior:
          CachePolicyId: !Ref DefaultCachePolicy
          TargetOriginId: OriginGW
          ViewerProtocolPolicy: "redirect-to-https"
        CacheBehaviors:
          -
            PathPattern: "/faq/"
            TargetOriginId: OriginGW
            CachePolicyId: !Ref DefaultCachePolicy
            ViewerProtocolPolicy: "redirect-to-https"
            FunctionAssociations:
              - 
                EventType: viewer-request
                FunctionARN: !GetAtt IPRestrictionFunction.FunctionMetadata.FunctionARN
```

さいごに
=======
最初のデプロイは数分程度かかりましたが、CloudFront FunctionのUpdateは1分くらいで終わるので、割と開発もやりやすかったです。