この記事は CodeChrysalis Advent Calendar 2019 の記事です。
Serverless FrameworkでDynamoDBを構築することについての記事もぜひ御覧ください。
はじめに
Serverless Framework はYAMLでサーバサイドの環境構築ができる、Infrastructure As Code の真骨頂です。確かにAWS等のコンソールでもポチポチクリックしながら環境構築はできますが、
- 変更の管理ができない
- 画面でポチポチクリックするのが本当に面倒(Lazy...)
なのでServerless Framework の CLI でコマンド一発で全てが構築されるのはとても気持ちが良い。今回はその中でもAWSのCognitoをどのように構築するかを紹介します。
この説明の前提は、認証プロバイダはCognito Identity Poolとし、Cognito User Poolと連携して、API GatewayにアクセスさせるときにデフォルトのAWS authorizerを使用したいという状況とします。
まずはYAML
早速YAMLを紹介します。
serverless.yml
service: cc-cognito
custom:
stage: ${opt:stage, self:provider.stage}
provider:
name: aws
stage: dev
profile: cc-admin
region: ap-northeast-1
iamRoleStatements:
- Effect: Allow
Action:
- cognito-idp:ListUsers
- cognito-idp:AdminListGroupsForUser
Resource: "arn:aws:cognito-idp:ap-northeast-1:*:*"
resources:
# IAM
- ${file(resources/iam-roles.yml)}
# Cognito
- ${file(resources/cognito-user-pool.yml)}
- ${file(resources/cognito-identity-pool.yml)}
cognito-user-pool.yml
Resources:
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
AutoVerifiedAttributes:
- email
EmailConfiguration:
EmailSendingAccount: "DEVELOPER"
SourceArn: "arn:aws:ses:[ここにArn]:identity/[ここにemailAddress]"
EmailVerificationMessage: "任意のEmail本文"
EmailVerificationSubject: "任意のEmailタイトル"
MfaConfiguration: "OFF"
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
UserPoolName: ${self:custom.stage}-CognitoUserPool
UsernameAttributes:
- email
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: ${self:custom.stage}-CognitoUserPoolClient
ExplicitAuthFlows:
- ADMIN_USER_PASSWORD_AUTH
GenerateSecret: false
UserPoolId:
Ref: CognitoUserPool
Outputs:
UserPoolId:
Value:
Ref: CognitoUserPool
Export:
Name: ${self:custom.stage}-UserPoolId
UserPoolClientId:
Value:
Ref: CognitoUserPoolClient
Export:
Name: ${self:custom.stage}-UserPoolClientId
cognito-identity-pool.yml
Resources:
CognitoUserIdentityPool:
Type: AWS::Cognito::IdentityPool
Properties:
IdentityPoolName: ${self:custom.stage}CognitoUserIdentityPool
AllowUnauthenticatedIdentities: false
CognitoIdentityProviders:
- ClientId:
Ref: CognitoUserPoolClient
ProviderName:
Fn::GetAtt: [ "CognitoUserPool", "ProviderName" ]
CognitoUserIdentityPoolRoleAttachment:
Type: AWS::Cognito::IdentityPoolRoleAttachment
Properties:
IdentityPoolId:
Ref: CognitoUserIdentityPool
Roles:
authenticated:
Fn::GetAtt: [CognitoUserAuthRole, Arn]
Outputs:
CognitoUserIdentityPool:
Value:
Ref: CognitoUserIdentityPool
Export:
Name: ${self:custom.stage}-CognitoUserIdentityPool
iam-roles.yml
Resources:
CognitoUserAuthRole:
Type: AWS::IAM::Role
Properties:
Path: /
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Federated: "cognito-identity.amazonaws.com"
Action:
- "sts:AssumeRoleWithWebIdentity"
Condition:
StringEquals:
"cognito-identity.amazonaws.com:aud":
Ref: CognitoUserIdentityPool
"ForAnyValue:StringLike":
"cognito-identity.amazonaws.com:amr": authenticated
Policies:
- PolicyName: "CognitoAuthorizedPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "mobileanalytics:PutEvents"
- "cognito-sync:*"
- "cognito-identity:*"
Resource: "*"
- Effect: "Allow"
Action:
- "execute-api:Invoke"
Resource:
# Allow users to invoke hello GET API
- Fn::Join:
- ""
- - "arn:aws:execute-api:"
- Ref: AWS::Region
- ":"
- Ref: AWS::AccountId
- ":"
- "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
- "/*/GET/hello"
# Allow users to invoke hello POST API
- Fn::Join:
- ""
- - "arn:aws:execute-api:"
- Ref: AWS::Region
- ":"
- Ref: AWS::AccountId
- ":"
- "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
- "/*/POST/hello"
関係性を解説
Cognito User Pool
ユーザーID(Emailや電話番号とすることもできる)とパスワードを保管して、サインアップやログイン等を司ることができます。MFA(多要素認証)も使うことができます。
ログインに成功すると、何に対してアクセスできるのかを後述するCognito Identity Poolに判断させます。
Cognito Identity Provider
今回でいうとIdentityを提供してくれる、すなわちCognito Identity ProviderであるCognito User Poolから提供された情報に基づき、どのIAMロールを付加するかを判断します。
これによって例えばログインしてきたユーザーはどのエンドポイントにアクセスできるかを切り分けることができます。管理者とユーザーそれぞれでユーザープールを作り、それぞれで異なるIAMロールを割り当てることで実現できます。
IAM
IAMはどんなAWS製品でも避けては通れない認識なので皆さんご存知なのではないでしょうか。権限を付与するためのものです。今回でいうと、Cognito Identity Poolによってログインしてきたユーザーに割り当てるIAMを作ります。
YAMLの要素を解説
serverless.yml
Serverless Framework で DynamoDB を構築する
の続きで、マイクロサービスを前提としますので、まずはserverless.yml
です。
再掲
service: cc-auth
custom:
stage: ${opt:stage, self:provider.stage}
provider:
name: aws
stage: dev
profile: cc-admin
region: ap-northeast-1
iamRoleStatements:
- Effect: Allow
Action:
- cognito-idp:ListUsers
- cognito-idp:AdminListGroupsForUser
Resource: "arn:aws:cognito-idp:ap-northeast-1:*:*"
resources:
# IAM
- ${file(resources/iam-roles.yml)}
# Cognito
- ${file(resources/cognito-user-pool.yml)}
- ${file(resources/cognito-identity-pool.yml)}
ここでresources
に渡したのは3つのファイル参照です。Cognitoは認証を司るため、認証まわりを一つのサービスと見立ててマイクロサービス化しています。逆にCognito User Pool, Cognito Identity Pool, IAMは認証の部品なので一つのマイクロサービスに包みました。が、それぞれ別の製品なので別のファイルで管理するようにしました。
他のプロパティについてはServerless Framework で DynamoDB を構築する
を参照してください。
Cognito User Pool
再掲
Resources:
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
AutoVerifiedAttributes:
- email
EmailConfiguration:
EmailSendingAccount: "DEVELOPER"
SourceArn: "arn:aws:ses:[ここにArn]:identity/[ここにemailAddress]"
EmailVerificationMessage: "任意のEmail本文"
EmailVerificationSubject: "任意のEmailタイトル"
MfaConfiguration: "OFF"
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
UserPoolName: ${self:custom.stage}-CognitoUserPool
UsernameAttributes:
- email
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: ${self:custom.stage}-CognitoUserPoolClient
ExplicitAuthFlows:
- ADMIN_USER_PASSWORD_AUTH
GenerateSecret: false
UserPoolId:
Ref: CognitoUserPool
Outputs:
UserPoolId:
Value:
Ref: CognitoUserPool
Export:
Name: ${self:custom.stage}-UserPoolId
UserPoolClientId:
Value:
Ref: CognitoUserPoolClient
Export:
Name: ${self:custom.stage}-UserPoolClientId
基本的にはAWS CloudFormationの文法に基づきます。
Type: AWS::Cognito::UserPool
でこれからCognito User Poolを作るという宣言になります。
AutoVerifiedAttributes
はユーザーがサインアップするときに何を以て確認するかです。選択肢はEmailかSMSがあります。今回はEmailを選択しました。
EmailConfiguration:
EmailSendingAccount: "DEVELOPER"
SourceArn: "arn:aws:ses:[ここにArn]:identity/[ここにemailAddress]"
ここでは任意のEmailアドレスから確認のEmailを送信したいときの設定をしています。AWSのデフォルトのEmailアドレスから送信するのでよければこの設定は不要です。
もしこの機能を利用する場合は、事前にAWS SESで任意のEmailアドレスを登録し、サンドボックスを解除する必要があります。
EmailVerificationMessage
は任意のEmail本文を使うための設定、EmailVerificationSubject
は任意のタイトルを使うための設定です。
MfaConfiguration
は多要素認証を利用するかどうかです。今回はOFFにしています。
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
これはパスワードの強固さの規定です。上から順番に、最低何文字か、小文字を必要とするか、数字を必要とするか、記号を必要とするか、大文字を必要とするかの設定です。
UserPoolName
はユーザープールの名前を何にするかです。Serverless Framework で DynamoDB を構築する
でご紹介したcustom
を用いることにより、開発環境と本番環境の名前を分けています。
UsernameAttributes:
- email
これはCognitoにどんなデータをもたせるかです。実はCognitoもデータベースのようにいろいろな情報をもたせることができます。が、Cognitoに持たせるかデータベースに持たせるかは設計次第です。
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: ${self:custom.stage}-CognitoUserPoolClient
ExplicitAuthFlows:
- ADMIN_USER_PASSWORD_AUTH
UserPoolId:
Ref: CognitoUserPool
Cognito User Pool Clientの設定です。ClientName
は任意の名前を設定します。ここでもcustom
プロパティを使って本番環境と開発環境で名前を区別しています。
ExplicitAuthFlowsはCognito Clientでサポートする認証フローです。設定したい認証フローに応じて値を変えます。ドキュメントはこちらにあります。
UserPoolId
はどのPoolに帰属するClientかを設定します。Ref関数を使って先ほど設定したPoolと関連付けました。
Cognito Identity Pool
再掲
Resources:
CognitoUserIdentityPool:
Type: AWS::Cognito::IdentityPool
Properties:
IdentityPoolName: ${self:custom.stage}CognitoUserIdentityPool
AllowUnauthenticatedIdentities: false
CognitoIdentityProviders:
- ClientId:
Ref: CognitoUserPoolClient
ProviderName:
Fn::GetAtt: [ "CognitoUserPool", "ProviderName" ]
CognitoUserIdentityPoolRoleAttachment:
Type: AWS::Cognito::IdentityPoolRoleAttachment
Properties:
IdentityPoolId:
Ref: CognitoUserIdentityPool
Roles:
authenticated:
Fn::GetAtt: [CognitoUserAuthRole, Arn]
Outputs:
CognitoUserIdentityPool:
Value:
Ref: CognitoUserIdentityPool
Export:
Name: ${self:custom.stage}-CognitoUserIdentityPool
Cognito Identity Poolを作成するときのTypeはAWS::Cognito::IdentityPool
です。IdentityPoolName
は名前なので任意の名前とします。
AllowUnauthenticatedIdentities
は認証されていないユーザーを許可するかどうかですが、サインインしないとリソースにアクセスさせない場合はfalse
とします。
CognitoIdentityProviders
は認証情報は何が提供してくれるのかを定義するところです。ClientIdはRef関数でCognitoUserPoolClient
を、ProviderName
はFn::GetAtt
でCognitoUserPool
を参照していますが、これはcognito-user-pool.yml
でOutputsを設定しているからです。Outputsに設定したものは同じアカウントのCloudFormation上で作成されたものは読み取ることができます。
CognitoUserIdentityPoolRoleAttachment
では、どのIdentity PoolでどのIAMロールを割り当てるかを決めるものです。IAMロールは後述しますが、ここではつい先程設定したCognitoUserIdentityPool
にはCognitoUserAuthRole
のロールを割り当てる設定となっています。
IAM
再掲
Resources:
CognitoUserAuthRole:
Type: AWS::IAM::Role
Properties:
Path: /
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Federated: "cognito-identity.amazonaws.com"
Action:
- "sts:AssumeRoleWithWebIdentity"
Condition:
StringEquals:
"cognito-identity.amazonaws.com:aud":
Ref: CognitoUserIdentityPool
"ForAnyValue:StringLike":
"cognito-identity.amazonaws.com:amr": authenticated
Policies:
- PolicyName: "CognitoAuthorizedPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "mobileanalytics:PutEvents"
- "cognito-sync:*"
- "cognito-identity:*"
Resource: "*"
- Effect: "Allow"
Action:
- "execute-api:Invoke"
Resource:
# Allow users to invoke hello GET API
- Fn::Join:
- ""
- - "arn:aws:execute-api:"
- Ref: AWS::Region
- ":"
- Ref: AWS::AccountId
- ":"
- "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
- "/*/GET/hello"
# Allow users to invoke hello POST API
- Fn::Join:
- ""
- - "arn:aws:execute-api:"
- Ref: AWS::Region
- ":"
- Ref: AWS::AccountId
- ":"
- "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
- "/*/POST/hello"
まずAssumeRolePolicyDocument
で誰がどのようにユーザーにIAMロールを割り当てるのかを設定します。Principal
がAWSのリソース(この例ではPolicies
に記載されているLambda)に対してアクセスするリクエストを発行する主体になります。ここではCognito Identity Poolとするため、"cognito-identity.amazonaws.com"
としています。
Action:
- "sts:AssumeRoleWithWebIdentity"
アプリにサインアップしたユーザーにIAMロールを割り当てるためにAWS STSのsts:AssumeRoleWithWebIdentity
を使います。IAMユーザーに登録しなくともAWSのリソースに一時的にアクセスすることを許すためのものです。
Condition:
StringEquals:
"cognito-identity.amazonaws.com:aud":
Ref: CognitoUserIdentityPool
"ForAnyValue:StringLike":
"cognito-identity.amazonaws.com:amr": authenticated
これはIAMロールを割り当てるための条件です。認証を司るものがCognitoUserIdentityPool
であって、かつその結果がauthenticated
のもの、つまりログインされているものだけとしています。
Policies:
- PolicyName: "CognitoAuthorizedPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "mobileanalytics:PutEvents"
- "cognito-sync:*"
- "cognito-identity:*"
Resource: "*"
- Effect: "Allow"
Action:
- "execute-api:Invoke"
Resource:
# Allow users to invoke hello GET API
- Fn::Join:
- ""
- - "arn:aws:execute-api:"
- Ref: AWS::Region
- ":"
- Ref: AWS::AccountId
- ":"
- "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
- "/*/GET/hello"
# Allow users to invoke hello POST API
- Fn::Join:
- ""
- - "arn:aws:execute-api:"
- Ref: AWS::Region
- ":"
- Ref: AWS::AccountId
- ":"
- "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
- "/*/POST/hello"
そしてPolicies
には具体的にどんなAWSのリソースにアクセスすることを許すのかの設定です。今回はLambdaをAPI GATEWAYを介して呼び出すアーキテクチャのための認証であると仮定して、API GATEWAYのコールを許可しています。
"Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
の部分はすでにデプロイされているAPI Gatewayのリソースを取得していて、API Gatewayの当該パスにアクセスすることを許すという記載です。
デプロイの順番
IAMでAPI Gatewayを参照していると書きましたが、IAMはCognito Identity Poolに参照されています。つまり少なくともIAMとCognito Identity PoolのデプロイはAPI Gatewayの後でなければならないということです。Serverless Frameworkでマイクロサービスをデプロイするときはその順番も大事になります。
さいごに
Serverless Frameworkでデータベース、認証を構築する紹介をしました。次はAPI GATEWAYを紹介したいと思います。