小西秀和です。
以前、次の記事でAWS Amplify Hosting(AWS Amplify Console)の構築方法について紹介しました。
AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify Console)
しかし、AWS Amplify Hosting(AWS Amplify Console)では基本認証や証明書追加の機能はありますが、IP制限の機能がサポートされていません。
そのため、今回は内部がAmazon S3とAmazon CloudFrontで構成されていると推測されるAWS Amplify Hostingをカスタムオリジンと見なし、Amazon CloudFront、AWS WAF、Lambda@Edgeを使用してIP制限機能の追加と基本認証機能のオーバーライドを試してみたいと思います。
補足ですが、AWS Amplifyのコンポーネントは最近ではAWS Amplify CLI、AWS Amplify Libraries、AWS Amplify Hosting、AWS Amplify Studioに分類され、AWS Amplify HostingにはAWS Amplify CLIによるホスティングとAWS Amplify Consoleによるホスティングを含むような概念になっているようです。
参考:Web アプリケーション開発のいろはと AWS Amplify
ここでは以前の記事と同様にAWS Amplify Consoleの機能について言及しますが、AWS Amplifyによるホスティング機能をまとめてAWS Amplify Hostingと呼ぶようになっている傾向が強いため、用語としては「AWS Amplify Hosting」を使用しています。
※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。
今回の記事の内容は次のような構成になっています。
本記事で試す構成図
今回の構成ではAmazon S3とAmazon CloudFrontで構成されていると推測されるAWS Amplify Hostingをカスタムオリジンと見なし、Amazon CloudFront、AWS WAF、Lambda@Edgeと関連付けるので、Amazon CloudFrontが2段の構成になります。
また、AWS Amplify HostingのURLに直接アクセスされる可能性があるため、AWS Amplify Hostingで設定した基本認証のID・パスワードをBase64エンコードした文字列を追加するAmazon CloudFront側からカスタムヘッダーで送信することで、AWS Amplify Hostingへの直接アクセスを基本認証でブロックして、AWS CloudFormationで追加するAmazon CloudFrontからの通信を許可します。
一方でAmazon CloudFront側の基本認証はLambda@Edgeで実装し、AWS Amplify Hostingの基本認証情報とは別にID・パスワードを設定します。
Amazon CloudFront側のIP制限はAWS WAF Web ACLに入力したIPアドレスを許可するルールを作成して実現します。
今回の構成ではさらにAWS Certificate Manager(ACM)でSSL/TLS証明書も追加するAmazon CloudFrontに関連付けています。
AWS CloudFormationテンプレートとパラメータの例
AWS CloudFormationテンプレート(Amazon CloudFront、AWS WAF、Lambda@Edge、ACMをカスタムオリジンに関連付け)
このAWS CloudFormationテンプレートは、us-east-1リージョンにデプロイすることでAmazon CloudFront、AWS WAF、Lambda@Edge、ACMをカスタムオリジン(AWS Amplify Hosting、Amazon EC2など)に関連付けるものです。
CloudFrontOriginCustomHeaderName
にAuthorization
、CloudFrontOriginCustomHeaderValue
にBasic <基本認証用Base64文字列>
を入力することでカスタムヘッダーを使用してAmazon CloudFrontの背後にあるカスタムオリジン(AWS Amplify Hosting、Amazon EC2など)で設定してある基本認証を通過します。
CloudFrontOriginCustomHeaderName
とCloudFrontOriginCustomHeaderValue
を変更すれば、基本認証以外のカスタムヘッダーを使用したカスタムオリジンの認証にも幅広く使用できるようにしています。
一方でAmazon CloudFront側のLambda@Edgeを使用した基本認証は、LambdaEdgeBasicAuthID
にID、LambdaEdgeBasicAuthPW
にパスワードを入力して設定します。
Amazon CloudFront側のIP制限は、WAFWebACLAllowIPList
へプレフィックス表記の許可IPアドレスをカンマ区切りで入力します。
入力パラメータ例
CloudFrontCustomOriginDomain: "demo.xxxxxxxxxxxxxx.amplifyapp.com" CloudFrontOriginCustomHeaderName: "Authorization" CloudFrontOriginCustomHeaderValue: "Basic SWFtOkltbWVyc2VkSW5BV1M=" # ID: Iam, Password: ImmersedInAWSの場合 CloudFrontDistributionID: "" CloudFrontCachePolicyName: "CachingDisabled" CloudFrontOriginRequestPolicyName: "NONE" CloudFrontResponseHeaderPolicyName: "CORS-with-preflight-and-SecurityHeadersPolicy" WAFWebACLResourcePrefix: "WebHostIPRestrictionsForCustomOrigin" WAFWebACLAllowIPList: "XXX.XXX.XXX.XXX/16,YYY.YYY.YYY.YYY/24,ZZZ.ZZZ.ZZZ.ZZZ/32" LambdaEdgeBasicAuthFuncName: "WebHostBasicAuthLambdaEdgeForCustomOrigin" LambdaEdgeBasicAuthID: "Iam" LambdaEdgeBasicAuthPW: "Nobody" ACMCustomDomainName: "cfn-acm-edge-waf-cfnt-custom-origin.h-o2k.com" ACMHostedZoneId: "ZZZZZZZZZZZZZZZZZZZZZ"
※CloudFrontOriginCustomHeaderValueに入力する基本認証用のBase64文字列は次のUnix系コマンドで<ID>、<パスワード>にそれぞれ値を入れて実行すると取得できます。
$ echo -n '<ID>:<パスワード>' | base64
テンプレート本体
ファイル名:WebHostCFnAddCloudfrontWafEdgeAndAcmToCustomOrigin.yml
AWSTemplateFormatVersion: '2010-09-09' Description: 'CFn Template for a stack that creates ACM, Lambda@Edge, WAF, and Custom Origin+CloudFront Hosting.' Parameters: CloudFrontCustomOriginDomain: #【CloudFront用】Amazon CloudFrontに設定するCustomOriginのURL Type: String Default: "demo.xxxxxxxxxxxxxx.amplifyapp.com" CloudFrontOriginCustomHeaderName: #【CloudFront用】Amazon CloudFrontのオリジンカスタムヘッダーに設定するヘッダー名 Type: String Default: "Authorization" # 基本認証を想定してデフォルトはAuthorization CloudFrontOriginCustomHeaderValue: #【CloudFront用】Amazon CloudFrontのオリジンカスタムヘッダーに設定するヘッダー値 Type: String Default: "Basic SWFtOkltbWVyc2VkSW5BV1M=" # 基本認証を想定して「Basic <Base64文字列>」。例は「echo -n "Iam:ImmersedInAWS" | base64」を実行した場合のBase64文字列。 Description: 'The following command creates a base64 string: echo -n "ID:Password" | base64' CloudFrontCachePolicyName: #【CloudFront用】Amazon CloudFrontのキャッシュポリシーの指定。マネージドポリシーからの選択式。 Type: String Default: CachingDisabled AllowedValues: - NONE - Amplify - CachingDisabled - CachingOptimized - CachingOptimizedForUncompressedObjects - Elemental-MediaPackage CloudFrontOriginRequestPolicyName: #【CloudFront用】Amazon CloudFrontのオリジンリクエストポリシー。マネージドポリシーからの選択式。 Type: String Default: NONE AllowedValues: - NONE - AllViewer - AllViewerAndCloudFrontHeaders-2022-06 - AllViewerExceptHostHeader - CORS-CustomOrigin - CORS-S3Origin - Elemental-MediaTailor-PersonalizedManifests - UserAgentRefererHeaders CloudFrontResponseHeaderPolicyName: #【CloudFront用】Amazon CloudFrontのレスポンスヘッダーポリシー。マネージドポリシーからの選択式。 Type: String Default: CORS-with-preflight-and-SecurityHeadersPolicy AllowedValues: - NONE - SimpleCORS - CORS-With-Preflight - SecurityHeadersPolicy - CORS-and-SecurityHeadersPolicy - CORS-with-preflight-and-SecurityHeadersPolicy ACMCustomDomainName: #【ACM用】ACM証明書を発行する対象ドメイン名 Type: String Default: cfn-acm-edge-waf-cfnt-custom-origin.h-o2k.com ACMHostedZoneId: #【ACM用】ACM証明書を発行する対象ドメイン名を管理するRoute53のホストゾーンID Type: String Default: ZZZZZZZZZZZZZZZZZZZZZ WAFWebACLResourcePrefix: #【WAFWebACL用】AWS WAF WebACLのリソースにつけるPrefix Type: String Default: WebHostIPRestrictionsForCustomOrigin WAFWebACLAllowIPList: #【WAFWebACL用】AWS WAF WebACLのルールでIP制限をするCIDR #Type: CommaDelimitedList Type: String Default: "XXX.XXX.XXX.XXX/16,YYY.YYY.YYY.YYY/24,ZZZ.ZZZ.ZZZ.ZZZ/32" LambdaEdgeBasicAuthFuncName: #【Lambda@Edge用】基本認証をするLambda@Edgeの関数名 Type: String Default: WebHostBasicAuthLambdaEdgeForCustomOrigin LambdaEdgeBasicAuthID: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するID。AWS Secrets Managerシークレットとして保管する。 Type: String Default: Iam LambdaEdgeBasicAuthPW: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するパスワード。AWS Secrets Managerシークレットとして保管する。 Type: String Default: Nobody CloudFrontDistributionID: #【共通】各リソースの関連付けに使用するAmazon CloudFrontのDistribution ID。 #ALambda@Edgeで使用するAWS Secrets Managerシークレットの一意識別で使用する。 Type: String #※スタックの初回Create処理でAmazon CloudFrontを作成してから、2回目のUpdate処理で入力する。 Default: "" Mappings: CloudFrontCachePolicyIds: NONE: Id: "" Amplify: Id: 2e54312d-136d-493c-8eb9-b001f22f67d2 CachingDisabled: Id: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad CachingOptimized: Id: 658327ea-f89d-4fab-a63d-7e88639e58f6 CachingOptimizedForUncompressedObjects: Id: b2884449-e4de-46a7-ac36-70bc7f1ddd6d Elemental-MediaPackage: Id: 08627262-05a9-4f76-9ded-b50ca2e3a84f CloudFrontOriginRequestPolicyIds: NONE: Id: "" AllViewer: Id: 216adef6-5c7f-47e4-b989-5492eafa07d3 AllViewerAndCloudFrontHeaders-2022-06: Id: 33f36d7e-f396-46d9-90e0-52428a34d9dc AllViewerExceptHostHeader: Id: b689b0a8-53d0-40ab-baf2-68738e2966ac CORS-CustomOrigin: Id: 59781a5b-3903-41f3-afcb-af62929ccde1 CORS-S3Origin: Id: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf Elemental-MediaTailor-PersonalizedManifests: Id: 775133bc-15f2-49f9-abea-afb2e0bf67d2 UserAgentRefererHeaders: Id: acba4595-bd28-49b8-b9fe-13317c0390fa CloudFrontResponseHeaderPolicyIds: NONE: Id: "" CORS-and-SecurityHeadersPolicy: Id: e61eb60c-9c35-4d20-a928-2b84e02af89c CORS-With-Preflight: Id: 5cc3b908-e619-4b99-88e5-2cf7f45965bd CORS-with-preflight-and-SecurityHeadersPolicy: Id: eaab4381-ed33-4a86-88ca-d9558dc6cd63 SecurityHeadersPolicy: Id: 67f7725c-6f97-4210-82d7-5512b31e9d03 SimpleCORS: Id: 60669652-455b-4ae9-85a4-c4c02393f86c Conditions: IsAllowIPList: !Not [!Equals [!Ref WAFWebACLAllowIPList, ""]] IsCloudFrontDistributionID: #入力パラメータにAmazon CloudFrontのDistribution IDがある場合はTrueでLambda@Edgeを作成、ない場合はFalseでLambda@Edge作成をスキップ。 !Not [!Equals [!Ref CloudFrontDistributionID, ""]] Resources: #CloudFrontをエイリアスレコードとしてRoute53ホストゾーンに登録するRoute53レコードセット Route53RecordSetGroup: Type: AWS::Route53::RecordSetGroup DependsOn: - CloudFrontDistribution Properties: HostedZoneId: !Ref ACMHostedZoneId RecordSets: - Name: !Ref ACMCustomDomainName Type: A #CloudFrontをエイリアスレコードとして登録する場合はエイリアスターゲットのHostedZoneIdが次の固定値となる #参考:https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-route53.html AliasTarget: HostedZoneId: Z2FDTNDATAQYW2 DNSName: !GetAtt CloudFrontDistribution.DomainName #ACM証明書の発行 CertificateManagerCertificate: Type: AWS::CertificateManager::Certificate Properties: DomainName: !Ref ACMCustomDomainName DomainValidationOptions: - DomainName: !Ref ACMCustomDomainName HostedZoneId: !Ref ACMHostedZoneId ValidationMethod: DNS #カスタムドメインの検証はRoute53のDNSを使用する方法で実施する #AWS WAF WebACLの作成 WAFv2WebACL: Type: AWS::WAFv2::WebACL Properties: Name: !Sub "${WAFWebACLResourcePrefix}-WebACL" Scope: CLOUDFRONT DefaultAction: Block: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Sub "${WAFWebACLResourcePrefix}-WebACL-Metric" Rules: - Name: !Sub "${WAFWebACLResourcePrefix}-WebACL-Rule" Action: Allow: {} Priority: 0 Statement: IPSetReferenceStatement: Arn: !GetAtt WAFv2IPSet.Arn VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Sub "${WAFWebACLResourcePrefix}-WebACL-Rule-Metric" WAFv2IPSet: Type: AWS::WAFv2::IPSet Properties: Name: !Sub "${WAFWebACLResourcePrefix}-IPSet" Scope: CLOUDFRONT IPAddressVersion: IPV4 Addresses: !If [IsAllowIPList, !Split [ ",", !Ref WAFWebACLAllowIPList ], []] #Lambda@Edgeの作成 LambdaEdgeBasicAuth: Type: AWS::Lambda::Function DependsOn: - SecretsManagerSecret - LambdaEdgeBasicAuthRole Properties: FunctionName: !Ref LambdaEdgeBasicAuthFuncName Runtime: python3.8 MemorySize: 128 #Lambda@Edgeのクォータ最大値を設定 Timeout: 5 #Lambda@Edgeのクォータ最大値を設定 Role: !GetAtt LambdaEdgeBasicAuthRole.Arn Handler: index.lambda_handler Code: ZipFile: | # 基本認証を実施するLambda@Edgeの関数 import json import boto3 import base64 # ID、パスワードを保管し、認証に使用するAWS Secrets Managerを扱うクライアントを作成 asm_client = boto3.client('secretsmanager', region_name='us-east-1') # エラーの場合のレスポンスを定義 err_response = { 'status': '401', 'statusDescription': 'Unauthorized', 'body': 'Authentication Failed.', 'headers': { 'www-authenticate': [ { 'key': 'WWW-Authenticate', 'value': 'Basic realm="Basic Authentication"' } ] } } def lambda_handler(event, context): try: print('event:') print(event) # イベントからCloudFrontのリクエストを取得 request = event['Records'][0]['cf']['request'] # イベントからCloudFrontのDistribution IDを取得 cf_dist_id = event['Records'][0]['cf']['config']['distributionId'] # リクエストからヘッダーを取得 headers = request['headers'] if (headers.get('authorization') != None): # ヘッダーにauthorizationがあれば認証を試行する # headers['authorization'][0]['value']の中身は「Basic <base64でエンコードされた[ID:Password]の文字列>」 # authorizationの内容を分解してID、パスワードを取り出す target_credentials_str = headers['authorization'][0]['value'].split( " ") target_credentials = base64.b64decode( target_credentials_str[1]).decode().split(":") target_id = target_credentials[0] target_pw = target_credentials[1] # 取得したCloudFrontのDistribution IDおよび基本認証で入力されたIDでシークレットを取得する response = asm_client.get_secret_value( SecretId='CloudFrontBasicAuth/' + cf_dist_id + '/' + str(target_id) ) # シークレットが取得でき、格納されている文字列が基本認証で入力されたパスワードと一致すれば認証成功としてリクエストを返却する。 # それ以外の場合はエラーレスポンスを返す。 if (response.get('SecretString') != None): secret_string = json.loads(response['SecretString']) if (secret_string.get('Password') == target_pw): return request else: return err_response else: return err_response else: return err_response except Exception as e: print("Exception:") print(e) return err_response LambdaEdgeBasicAuthVersion: #呼出元スタックでAmazon CloudFrontと関連付けるためのLambdaEdgeバージョンを作成する Type: AWS::Lambda::Version DependsOn: - LambdaEdgeBasicAuth Properties: FunctionName: !Ref LambdaEdgeBasicAuth Description: 'Basic Auth Lambda Edge for Amazon CloudFront' LambdaEdgeBasicAuthRole: #基本認証用Lambda@Edgeに適用するIAMロールとIAMポリシーの設定 Type: AWS::IAM::Role Properties: RoleName: !Sub 'IAMRole-${LambdaEdgeBasicAuthFuncName}' Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - edgelambda.amazonaws.com - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: !Sub 'IAMPolicy-${LambdaEdgeBasicAuthFuncName}' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - iam:CreateServiceLinkedRole Resource: - '*' - Effect: Allow Action: - lambda:GetFunction - lambda:EnableReplication Resource: - !Sub 'arn:aws:lambda:us-east-1:${AWS::AccountId}:function:${LambdaEdgeBasicAuthFuncName}' - Effect: Allow Action: - cloudfront:UpdateDistribution Resource: !If - IsCloudFrontDistributionID - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistributionID}' - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/*' - PolicyName: SecretsManagerGetSecretValue PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: !If - IsCloudFrontDistributionID - !Sub 'arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:CloudFrontBasicAuth/${CloudFrontDistributionID}/*' - !Sub 'arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:CloudFrontBasicAuth/DUMMY/*' SecretsManagerSecret: #基本認証に使用するID、パスワードを格納するAWS Secrets Managerシークレットの作成 Type: 'AWS::SecretsManager::Secret' Properties: Name: !If - IsCloudFrontDistributionID - !Sub CloudFrontBasicAuth/${CloudFrontDistributionID}/${LambdaEdgeBasicAuthID} - !Sub CloudFrontBasicAuth/DUMMY/${LambdaEdgeBasicAuthID} SecretString: !Sub '{"Password":"${LambdaEdgeBasicAuthPW}"}' Description: "SecretsManagerSecret of LambdaEdgeBasicAuth" CloudFrontDistribution: Type: AWS::CloudFront::Distribution DependsOn: #ACM証明書が発行されてからCloudFrontに関連付けるためDependsOnを設定 - CertificateManagerCertificate #AWS WAF WebACLが作成されてからCloudFrontに関連付けるためDependsOnを設定 - WAFv2WebACL #基本認証用Lambda@Edgeバージョンが作成されてからCloudFrontに関連付けるためDependsOnを設定 - LambdaEdgeBasicAuth Properties: DistributionConfig: #CloudFrontのエイリアスにACM証明書を発行したドメイン名を設定する Aliases: - !Ref ACMCustomDomainName #CloudFrontにACM証明書を設定する ViewerCertificate: SslSupportMethod: sni-only MinimumProtocolVersion: TLSv1.2_2021 AcmCertificateArn: !Ref CertificateManagerCertificate #返却値はACM証明書のARN WebACLId: !GetAtt WAFv2WebACL.Arn HttpVersion: http2 Origins: - DomainName: !Ref CloudFrontCustomOriginDomain Id: StaticWebsiteHostingCustomOrigin CustomOriginConfig: OriginProtocolPolicy: "https-only" OriginCustomHeaders: - HeaderName: !Ref CloudFrontOriginCustomHeaderName HeaderValue: !Ref CloudFrontOriginCustomHeaderValue Enabled: true DefaultCacheBehavior: AllowedMethods: [GET, HEAD, OPTIONS] TargetOriginId: StaticWebsiteHostingCustomOrigin #オリジンをターゲットに指定する ForwardedValues: QueryString: false ViewerProtocolPolicy: redirect-to-https CachePolicyId: !FindInMap [ CloudFrontCachePolicyIds, !Ref CloudFrontCachePolicyName , Id ] OriginRequestPolicyId: !FindInMap [ CloudFrontOriginRequestPolicyIds, !Ref CloudFrontOriginRequestPolicyName , Id ] ResponseHeadersPolicyId: !FindInMap [ CloudFrontResponseHeaderPolicyIds, !Ref CloudFrontResponseHeaderPolicyName , Id ] #DefaultTTL: 86400 #MaxTTL: 31536000 #MinTTL: 60 #Compress: true LambdaFunctionAssociations: !If - IsCloudFrontDistributionID - [ { EventType: viewer-request, IncludeBody: "true", LambdaFunctionARN: !Ref LambdaEdgeBasicAuthVersion } ] - !Ref AWS::NoValue DefaultRootObject: index.html CustomErrorResponses: - { ErrorCachingMinTTL: 10, ErrorCode: 400, ResponseCode: 200, ResponsePagePath: /, } - { ErrorCachingMinTTL: 10, ErrorCode: 404, ResponseCode: 200, ResponsePagePath: /, } Outputs: Region: Value: !Ref AWS::Region ACMCustomDomainURL: Value: !Join ["", ["https://", !Ref ACMCustomDomainName]] Description: "Web hosting URL with Certificate" CloudFrontDistributionID: Value: !Ref CloudFrontDistribution CloudFrontDomainName: Value: !GetAtt CloudFrontDistribution.DomainName CloudFrontSecureURL: Value: !Join ["", ["https://", !GetAtt CloudFrontDistribution.DomainName]] LambdaEdgeCompleteSetupSkiped: Value: !If [IsCloudFrontDistributionID, "false", "true"]
構築手順
次の記事で説明してある手順を参考にAWS Amplify Hostingによる静的ウェブサイトを作成し、基本認証のID、パスワードを設定する。
AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify Console)
以下の情報はAWS CloudFormationテンプレートのパラメータとして使用するのでメモしておく。
・作成したAWS Amplify Hostingのドメイン(例:<App Name>.xxxxxxxxxxxxxx.amplifyapp.com
)
・Unix系OSで次のコマンドを実行して出力されるBase64文字列(<ID>、<パスワード>にはAWS Amplify Hostingに設定した基本認証のID、パスワードを入力する)
echo -n '<ID>:<パスワード>' | base64
us-east-1リージョンで「Amazon CloudFront、AWS WAF、Lambda@Edgeをカスタムオリジンに関連付け」のAWS CloudFormationテンプレートにカスタムオリジン(AWS Amplify Hosting)のドメインや基本認証用Base64文字列などをパラメータに入力した上で作成(初回実行)のデプロイをする。
※初回実行ではCloudFrontDistributionID
は入力しない。
初回作成後にOutput
フィールドへCloudFrontDistributionID
が出力されるのでメモしておく。us-east-1リージョンで「Amazon CloudFront、AWS WAF、Lambda@Edgeをカスタムオリジンに関連付け」のAWS CloudFormationテンプレートにCloudFrontDistributionIDのパラメータを入力した上で更新(2回目実行)のデプロイをする。
入力されたCloudFrontDistributionID
に基づいてLambda@Edgeで使用するAWS Secrets Managerシークレットや権限が変更されてAmazon CloudFront側の基本認証が設定される。結果確認
正しく実行されていればパラメータで指定した許可IPアドレスからACM証明書を発行する対象ドメインにhttpsでアクセスすると基本認証のダイアログが表示され、Amazon CloudFront側の基本認証としてパラメータで指定したIDとパスワードで認証を通過します。
CloudFrontOriginCustomHeaderName
へAuthorization
、CloudFrontOriginCustomHeaderValue
へBasic <基本認証用Base64文字列>
を入力していれば、Amazon CloudFront側からカスタムオリジン(AWS Amplify Hosting)には前述した基本認証用Base64文字列がAuthorization
のカスタムヘッダーで送信され、カスタムオリジン側の基本認証を通過します。
WAFWebACLAllowIPList
パラメータで指定した許可IPアドレス以外からアクセスすると「403 ERROR」でアクセス拒否されます。
正しく動作しない場合は呼出元AWS CloudFormationスタックのイベント内容、AWS CloudTrailのログから原因を特定して不具合を修正します。
削除手順
AWS CloudFormationではLambda@EdgeバージョンとAmazon CloudFrontの相互関係のある関連付け解除を順序制御することはしないので一度にすべてのスタックを削除することはできません。
そのため、削除する場合は次の手順のようにAmazon CloudFrontとLambda@Edgeバージョンの関連付けを解除した後、AWS CloudFormationスタックを削除する必要があります。
- AWS CloudFormationスタックのパラメータでAmazon CloudFrontのDistribution IDを空にしてUpdate処理をする。
呼出元AWS CloudFormationスタックのパラメータ「CloudFrontDistributionID」を空にしてスタックを更新する。 - AWS CloudFormationスタックを削除する
- AWS CloudFormationスタックの削除が失敗するようであれば、Lambda@Edgeを残してAWS CloudFormationスタックを削除し、後からLambda@Edgeバージョンを個別に削除する。
参考:
Web アプリケーション開発のいろはと AWS Amplify
Route 53 template snippets - AWS CloudFormation
Tech Blog with related articles referenced
まとめ
今回はus-east-1リージョンへ作成したAmazon CloudFront、AWS WAF、Lambda@EdgeへAWS Amplify Hostingをカスタムオリジンとして関連付けてIP制限機能の追加と基本認証機能のオーバーライドを試しました。
結果として今回の構成で追加したAmazon CloudFront側のSSL/TLS証明書(AWS Certificate Manager)・基本認証(Lambda@Edge)・IP制限(AWS WAF)が機能し、Authorization
カスタムヘッダーで設定した基本認証用Base64文字列によってカスタムオリジン(AWS Amplify Hosting)の基本認証を通過してHTMLコンテンツが表示されることを確認できました。
また、今回の構成はAWS Amplify Hosting(AWS Amplify Console)の内部にあると想定されるAmazon CloudFrontとAWS CloudFormationによって追加したAmazon CloudFrontで2段のAmazon CloudFront構成のため、レスポンスは遅くなりましたが、特に不具合はなくHTMLコンテンツが表示されました(※動作保証ではありません。特にJavaScriptやバックエンドとのAPI通信などが絡んでくると不具合が出る可能性はあります。)。
ただ、本来であればAWS Amplify Hosting側でAWS WAFのIP制限などの機能がサポートされることが理想的です。
今後もAWS Amplify Hostingの改善を楽しみにしながら、AWS CloudFormation、AWS Amplify、AWS CDKなどのデプロイ関連サービスや静的ウェブホスティングに関するアップデートについて様々なパターンを試してみたいと考えています。