本記事について
- AWS CloudFormationを用いて、色々なアーキテクチャを構築していきます。テンプレートのコピペ元としてご活用いただければ幸いです
- 最新テンプレートはGitHubに配置しています
- 本記事では、前回作成した「S3 + CloudFront + OACによる静的ウェブサイトのホスティング」に、Cognitoを用いた認証機能を付けます。これによって、許可されたユーザーのみがサイトを閲覧できるようにします
構築するアーキテクチャ
- まず、S3とCloudFrontとOACを作成し、静的ウェブサイトとしてホスティングができるようにします。これらの設定は前回と全く同じです
- 次に、認証用のCognito UserPoolと、それに付随してCognitoドメインとアプリケーションクライアントも作成します
- アプリケーションクライアントでは、作成したCognitoドメインを使用するようにホストされたUIを設定します。これによって、ユーザープールのサインアップとサインイン用のウェブページが自動的に使えるようになります
- 今回、設定はほぼデフォルトのままとし、認証方法も簡単なIDとパスワードによるものを使います
- ユーザーがCloudFront経由でサイトにアクセスしたら、Cognito認証を呼び出すようにする必要があります
- このために、Lambda関数 (Lambda@Edge) を作成します。Lambda@Edgeはus-east-1 (バージニア北部) リージョンでしかサポートされていないため、これだけは別のテンプレートで作成します
- Lambda関数内で、Cognito認証ページへリダイレクトしたり、認証処理を記載する必要があります。これを簡単に行うために、cognito-at-edgeというものを使います。詳細は後述
作成するリソース
- S3バケット関連
- AWS::S3::Bucket
- AWS::S3::BucketPolicy
- CloudFront関連
- AWS::CloudFront::Distribution
- AWS::CloudFront::OriginAccessControl
- Cognito認証関連
- AWS::Cognito::UserPool
- AWS::Cognito::UserPoolDomain
- AWS::Cognito::UserPoolClient
- Cognito認証関連 (cognito-at-edge)
- AWS::IAM::Role
- AWS::Lambda::Function
デプロイ方法
必要な準備
- AWSアカウントとアクセスキーの設定
- サーバーレス環境のため、特にVPCなどは不要です。事前の環境構築は不要です
S3 + CloudFront + OACによる静的ウェブサイトの作成
- まずは前回と同様に基本となる静的ウェブサイトを作成します
- その後、適当なindex.htmlをアップロードしてサイトにアクセスできることを確認します
Region=ap-northeast-1
OrganizationName=iwatake2222
SystemName=sample-05
aws cloudformation deploy \
--region "${Region}" \
--stack-name "${SystemName}"-s3-cloudfront-cognito \
--template-file ./s3-cloudfront-cognito.yaml \
--parameter-overrides \
OrganizationName="${OrganizationName}" \
SystemName="${SystemName}"
aws cloudformation describe-stacks --stack-name "${SystemName}"-s3-cloudfront-cognito
echo hello > index.html
aws s3 cp index.html s3://"${OrganizationName}-${SystemName}-bucket"
Lambda@Edgeの作成
- 次に、Lambda@Edgeを作成します
- Lambda関数のコードはテンプレート内に直接、'Hello from Lambda!'という文字列を返す簡単なものを書いています。これは後でcognito-at-edgeに差し替えます
- デプロイ先はus-east-1リージョンとなります
RegionForLambdaEdge=us-east-1
OrganizationName=iwatake2222
SystemName=sample-05
aws cloudformation deploy \
--region "${RegionForLambdaEdge}" \
--stack-name "${SystemName}"-s3-cloudfront-cognito-lambda-edge \
--template-file ./s3-cloudfront-cognito-lambda-edge.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
OrganizationName="${OrganizationName}" \
SystemName="${SystemName}"
Lambda@Edgeに対してCloudFrontをトリガーとして設定する
- CloudFrontに対して、
viewer request
が発生したらLambda@Edgeを呼ぶように設定します- この設定はCloudFormationからだと出来なかったのでAWS Consoleから行います
- AWS Console -> us-east-1リージョンに移動 -> Lambda -> 作成したLambda関数 (sample-05-lambda-edge) を選択
-
トリガーを追加
->ソース
=CloudFront
を選択 ->Deploy to Lambda@Edge
をクリック -
Configure new CloudFront trigger
を選択し、以下の設定を行う-
Distribution
= 作成したDistribution (sample-05-distribution) -
Cache behavior
= * -
CloudFront event
= Viewer request <- 大事 -
Confirm deploy to Lambda@Edge
= チェックをつける -
デプロイ
をクリック
-
- 動作確認
- 図のように、バージョンが選ばれた状態で、CloudFrontがトリガーとして指定されていればOKです
- 再度CloudFrontのページにアクセスし、ページの内容がLambda@Edgeで返却している文字列「Hello from Lambda!」になっていればOKです
- 反映されるのに少し時間がかかるかもしれません
- 先ほど動作確認で同じサイトにアクセスするため、キャッシュが残っている可能性があります。表示が先ほどと変わらない場合は、キャッシュをクリアするか、別のブラウザやプライベートモードで試してみてください
- ブラウザを開発者モードにして、リロードボタンを長押しや右クリックするとキャッシュクリアしたうえでリロードできます
- 削除時の注意
- CloudFormationスタックやLambda関数を削除する前には、CloudFrontをトリガーから外す必要があります
- トリガーから外す操作をしても、それが反映されてエラーなく削除できるようになるまで10分程度かかりました
- CloudFormationスタックやLambda関数を削除する前には、CloudFrontをトリガーから外す必要があります
cognito-at-edgeのデプロイ
- 先ほど作成したLambda@Edgeは、hello world的な文字列を返すだけのものです。この関数の中身をcognito-at-edgeに差し替えます
- 手順は基本的にはhttps://github.com/awslabs/cognito-at-edge のGet Startedの通りです
- 作業用PC上で下記コマンドで、cognito-at-edgeを取得します。また、index.jsも作成しておきます
mkdir -p cognito-at-edge && cd cognito-at-edge
npm install cognito-at-edge
nano index.js
cd ..
index.js
const { Authenticator } = require('cognito-at-edge');
const authenticator = new Authenticator({
// Replace these parameter values with those of your own environment
region: 'ap-northeast-1', // user pool region
userPoolId: 'ap-northeast-1_tyo1a1FHH', // user pool ID
userPoolAppId: '63gcbm2jmskokurt5ku9fhejc6', // user pool app client ID
userPoolDomain: 'iwatake2222-sample-05.auth.ap-northeast-1.amazoncognito.com', // user pool domain
cookiePath: '/',
});
exports.handler = async (request) => authenticator.handle(request);
- index.jsの
region
、userPoolId
、userPoolAppId
、userPoolDomain
は、自分の環境に応じて編集してください- 今回使うテンプレートでは、下記コマンドによって必要な情報を出力しています
- "OutputValue" の値をコピペしてください。「"」が入らないように注意してください
- AWS Console上でCognitoの設定を確認しに行くのでも大丈夫です
- 今回使うテンプレートでは、下記コマンドによって必要な情報を出力しています
aws cloudformation describe-stacks --stack-name "${SystemName}"-s3-cloudfront-cognito
- 設定が完了したcognito-at-edgeをzipに固めてデプロイします
cd cognito-at-edge
zip -r ../cognito-at-edge.zip ./*
cd ..
RegionForLambdaEdge=us-east-1
aws lambda update-function-code --region "${RegionForLambdaEdge}" --function-name "${SystemName}-lambda-edge" --zip-file fileb://cognito-at-edge.zip
- 新しいバージョンのコードをアップロードしたので、このバージョンのコードに対して再度CloudFrontをトリガーとして設定します
- AWS Console -> us-east-1リージョンに移動 -> Lambda -> 作成したLambda関数 (sample-05-lambda-edge) を選択
- 新しいバージョンになったため、トリガーが設定されていない状態のはずです
-
トリガーを追加
->ソース
=CloudFront
を選択 ->Deploy to Lambda@Edge
をクリック -
Use existing CloudFront trigger on this function
を選択し、先ほど作成したトリガーを選び、デプロイをクリック- 先ほど作成したトリガーを使うので、今回は設定不要です
- AWS Console -> us-east-1リージョンに移動 -> Lambda -> 作成したLambda関数 (sample-05-lambda-edge) を選択
- 動作確認
- 再度CloudFrontのページにアクセスし、図のようにサインイン画面が表示されていればOKです
- 反映されるのに少し時間がかかるかもしれません
- 先ほど動作確認で同じサイトにアクセスしたため、キャッシュが残っている可能性があります。サインイン画面が現れない場合は、キャッシュをクリアするか、別のブラウザやプライベートモードで試してみてください
- ブラウザを開発者モードにして、リロードボタンを長押しや右クリックするとキャッシュクリアしたうえでリロードできます
- うまくいかない場合は、index.jsの設定を間違えていたり、zip化する場所をまちがえている可能性があります
- また、下記の場所でLambdaのログを確認できます
- AWS Console -> CloudFrontをデプロイしたリージョン(東京)に移動 -> CloudWatch -> ロググループ -> /aws/lambda/us-east-1.${Lambda@Edge名}
- 再度CloudFrontのページにアクセスし、図のようにサインイン画面が表示されていればOKです
全体の動作確認
- 現状はユーザーが登録されていない状態なので、サインインできません
- 以下の2つの方法でユーザー登録できます。ここでは2つ目の方法を試してみます
- AWS Console -> Cognito -> UserPoolからユーザーを登録
- ブラウザ上で表示されているサインイン画面からサインアップ (ユーザー登録) し、その後そのユーザーを承認する
- ユーザー登録
- 表示されているサインイン画面の
Sign up
をクリックします -
Sign up
画面で、適当なユーザー名とパスワードで登録します - その後、「An error was encountered with the requested page.」という画面が表示されますが無視して大丈夫です
- 恐らく、今登録したユーザー情報で自動的にサインインしようとしていると思うのですが、今の時点だとこのユーザーはまだ使えません。そのため失敗しているのだと思われます
- 逆に、ここで失敗してくれないと、新規のユーザーを登録すれば誰でも閲覧可能になってしまいます
- 恐らく、今登録したユーザー情報で自動的にサインインしようとしていると思うのですが、今の時点だとこのユーザーはまだ使えません。そのため失敗しているのだと思われます
- 表示されているサインイン画面の
- ユーザーの承認
- AWS Console -> Cognito -> 作成したユーザープール (sample-05-user-pool) を選択します
- ユーザータブを見ると、先ほど登録したユーザーがいますが「未確認」の状態です
- このユーザーをクリックし、ユーザー情報を開きます
-
アクション
->アカウントの確認
をクリックし、このユーザーを承認 (Confirm) してアクセス権を付与します
- 再度ブラウザからサイトにアクセスし、サインイン画面を表示します
- ここで、先ほど登録したユーザー名とパスワードを入力してサインインします
- S3にアップロードしたindex.html (「hello」) が表示されればOKです
テンプレート
静的ウェブサイト (S3とCloudFrontとOAC) と Cognitoを作成するテンプレート
s3-cloudfront-cognito.yaml
s3-cloudfront-cognito.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: |
Create an S3 bucket and CloudFront (OAC) for hosting a static website with Cognito authentification
Parameters:
OrganizationName:
Description: Organization Name
Type: String
SystemName:
Description: System Name
Type: String
Resources:
#-----------------------------------------------------------------------------
# S3 bucket
#-----------------------------------------------------------------------------
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${OrganizationName}-${SystemName}-bucket
Tags:
- Key: Name
Value: !Sub ${OrganizationName}-${SystemName}-bucket
S3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- s3:GetObject
Effect: Allow
Resource:
- !Sub arn:aws:s3:::${S3Bucket}/*
Principal:
Service: cloudfront.amazonaws.com
Condition:
StringEquals:
AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
#-----------------------------------------------------------------------------
# CloudFront
#-----------------------------------------------------------------------------
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: !Sub ${SystemName}-distribution
Origins:
- DomainName: !GetAtt S3Bucket.RegionalDomainName
Id: S3Origin
OriginAccessControlId: !GetAtt OAC.Id
S3OriginConfig:
OriginAccessIdentity: ''
Enabled: true
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: S3Origin
Compress: true
ForwardedValues:
QueryString: false
Cookies:
Forward: none
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized (Recommended for S3)
ViewerProtocolPolicy: redirect-to-https
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
PriceClass: PriceClass_All
OAC:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Description: Access Control
Name: !Sub ${SystemName}-oac
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
#-----------------------------------------------------------------------------
# Cognito User pool
#-----------------------------------------------------------------------------
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: !Sub ${SystemName}-user-pool
CognitoUserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: !Sub ${OrganizationName}-${SystemName}
UserPoolId: !Ref CognitoUserPool
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: !Sub ${SystemName}-client
UserPoolId: !Ref CognitoUserPool
CallbackURLs:
- !Join ["", [https://, !GetAtt CloudFrontDistribution.DomainName]]
SupportedIdentityProviders:
- COGNITO
AllowedOAuthFlows:
- code
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthScopes:
- openid
Outputs:
CloudFrontDomainName:
Value: !GetAtt CloudFrontDistribution.DomainName
Export:
Name: !Sub ${SystemName}-domain-name
UserPool:
Value: !Ref CognitoUserPool
Export:
Name: !Sub ${SystemName}-user-pool
UserPoolClient:
Value: !Ref CognitoUserPoolClient
Export:
Name: !Sub ${SystemName}-user-pool-client
UserPoolDomain:
Value: !Ref CognitoUserPoolDomain
Export:
Name: !Sub ${SystemName}-user-pool-domain
Lambda@Edgeを作成するテンプレート
s3-cloudfront-cognito-lambda-edge.yaml
s3-cloudfront-cognito-lambda-edge.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: |
Create an S3 bucket and CloudFront (OAC) for hosting a static website with Cognito authentification
Parameters:
SystemName:
Description: System Name
Type: String
Resources:
#-----------------------------------------------------------------------------
# IAM roles
#-----------------------------------------------------------------------------
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${SystemName}-lambda-edge-role
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
- edgelambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: !Sub ${SystemName}-lambda-edge-policy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- lambda:GetFunction
- lambda:EnableReplication*
- iam:CreateServiceLinkedRole
- cloudfront:CreateDistribution
- cloudfront:UpdateDistribution
Resource: '*'
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${SystemName}-lambda-edge
Role: !GetAtt LambdaRole.Arn
Runtime: nodejs18.x
Handler: index.handler
Code:
ZipFile: |
exports.handler = async (event) => {
const response = {
status: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
トラブルシューティング
しばらく経過後、アクセスできなくなった
- IDトークンの期限が切れた後(デフォルトだと30分後)、トップindex.html以外にアクセスするとこのような症状が発生します
- 例えば、https://ooo.cloudfront.net/aaa/xxx/index.html
- 詳しく原因を見ると、リダイレクトループが発生しているようでした
- Cookieを削除することで一時的に対応できます。が、毎回面倒です
- cognito-at-edgeに対して、Cookieのパスを指定することで根本的に解決できるようです
- https://github.com/awslabs/cognito-at-edge/issues/69
- index.jsに以下のパラメータを追加してください
const authenticator = new Authenticator({
...,
cookiePath: '/',
});