小西秀和です。
以前、次の記事で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などのデプロイ関連サービスや静的ウェブホスティングに関するアップデートについて様々なパターンを試してみたいと考えています。