CloudFormationがCognito対応したので試してみた(後編)

こんにちは、抜歯直後で口内環境最悪なエンジニアの最上です。
前編に引き続き、今回はCloudFormationでUserPoolとFederatedIdentityを作ります!

前編を読まれていない方は、前編:シンプルにUserPoolを作るもどうぞ! tech.mti.co.jp

何を作るのか

CloudFormationでこの辺を作る必要があります。リンクはそれぞれの公式ドキュメントです。

結構量が多くなりますね。IAM Roleは、手で作って割り当てることもできますが、今回はCloudFormationで全部作っちゃうことにしました。

実践

コード

こんな感じになりました。多い!

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "UserPoolName": {
      "Description": "UserPool name.",
      "Type": "String"
    },
    "isUserCreatedByAdminOnly": {
      "Description": "is user account created by admin only?",
      "Type": "String",
      "AllowedValues": ["true", "false"],
      "Default": "true"
    },
    "IdentityPoolName": {
      "Description": "IdentityPoolName can contain numbers, letters, underscores and spaces.",
      "Type": "String",
      "MaxLength": 128
    }
  },
  "Resources": {
    "UserPool": {
      "Type": "AWS::Cognito::UserPool",
      "Properties": {
        "AdminCreateUserConfig": {
          "AllowAdminCreateUserOnly": {
            "Ref": "isUserCreatedByAdminOnly"
          },
          "InviteMessageTemplate": {
            "EmailMessage": "ID: {username}\nPassword: {####}",
            "EmailSubject": "Your Temporary Password"
          }
        },
        "AutoVerifiedAttributes": ["email"],
        "MfaConfiguration": "OFF",
        "Policies": {
          "PasswordPolicy": {
            "MinimumLength": 8,
            "RequireLowercase": true,
            "RequireUppercase": true,
            "RequireNumbers": true,
            "RequireSymbols": false
          }
        },
        "UserPoolName": {
          "Ref": "UserPoolName"
        },
        "Schema": [{
          "Name": "email",
          "Required": true,
          "Mutable": true
        }, {
          "Name": "name",
          "Required": true,
          "Mutable": true
        }]
      }
    },
    "UserPoolApp": {
      "DependsOn": "UserPool",
      "Type": "AWS::Cognito::UserPoolClient",
      "Properties": {
        "ClientName": {
          "Ref": "UserPoolName"
        },
        "GenerateSecret": false,
        "RefreshTokenValidity": 30,
        "UserPoolId": {
          "Ref": "UserPool"
        }
      }
    },
    "IdentityPool": {
      "DependsOn": "UserPoolApp",
      "Type": "AWS::Cognito::IdentityPool",
      "Properties": {
        "IdentityPoolName": {
          "Ref": "IdentityPoolName"
        },
        "AllowUnauthenticatedIdentities": false,
        "CognitoIdentityProviders": [{
          "ClientId": {
            "Ref": "UserPoolApp"
          },
          "ProviderName": {
            "Fn::Join": ["", ["cognito-idp.", {
              "Ref": "AWS::Region"
            }, ".amazonaws.com/", {
              "Ref": "UserPool"
            }]]
          },
          "ServerSideTokenCheck": true
        }]
      }
    },
    "AuthRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "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": "IdentityPool"
                }
              },
              "ForAnyValue:StringLike": {
                "cognito-identity.amazonaws.com:amr": "authenticated"
              }
            }
          }]
        },
        "Policies": [{
          "PolicyName": "root",
          "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [{
              "Effect": "Allow",
              "Action": [
                "mobileanalytics:PutEvents",
                "cognito-sync:*",
                "cognito-identity:*"
              ],
              "Resource": [
                "*"
              ]
            }]
          }
        }],
        "RoleName": {
          "Fn::Join": ["_", ["Cognito", {
            "Ref": "UserPoolName"
          }, "Auth", "Role"]]
        }
      }
    },
    "UnauthRole": {
      "DependsOn": "IdentityPool",
      "Type": "AWS::IAM::Role",
      "Properties": {
        "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": "IdentityPool"
                }
              },
              "ForAnyValue:StringLike": {
                "cognito-identity.amazonaws.com:amr": "unauthenticated"
              }
            }
          }]
        },
        "Policies": [{
          "PolicyName": "root",
          "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [{
              "Effect": "Allow",
              "Action": [
                "mobileanalytics:PutEvents",
                "cognito-sync:*",
                "cognito-identity:*"
              ],
              "Resource": [
                "*"
              ]
            }]
          }
        }],
        "RoleName": {
          "Fn::Join": ["_", ["Cognito", {
            "Ref": "UserPoolName"
          }, "Unauth", "Role"]]
        }
      }
    },
    "IdentityPoolRole": {
      "DependsOn": ["AuthRole", "UnauthRole", "IdentityPool"],
      "Type": "AWS::Cognito::IdentityPoolRoleAttachment",
      "Properties": {
        "IdentityPoolId": {
          "Ref": "IdentityPool"
        },
        "Roles": {
          "authenticated": {
            "Fn::GetAtt": ["AuthRole", "Arn"]
          },
          "unauthenticated": {
            "Fn::GetAtt": ["UnauthRole", "Arn"]
          }
        }
      }
    }
  },
  "Outputs": {
    "UserPoolID": {
      "Description": "UserPoolID",
      "Value": {
        "Ref": "UserPool"
      }
    },
    "ClientID": {
      "Description": "AppClientID",
      "Value": {
        "Ref": "UserPoolApp"
      }
    },
    "IdentityPoolID": {
      "Description": "IdentityPoolID",
      "Value": {
        "Ref": "IdentityPool"
      }
    }
  }
}

Parameters

Parametersには、前編に加えてIdentityPoolの名前を渡せるようにしました。ここで注意したいのは、IdentityPoolNameに使える文字が制限されているということです。本来であれば、ここにAllowedPatternのプロパティを追加して、正規表現でバリデーションするのがよいでしょう(めんどくさくてやってないです、ごめんなさい)。

Resources

Resourcesを1つづつ見ていきます。UserPoolは前編でやったので割愛しますね。

UserPoolApp(UserPoolClient)

DependsOnにUserPoolを設定しています。プロパティとしてUserPoolIdを指定する必要があるため、UserPoolが作成されてから出ないとRefが効かないため、DependsOnは必須となります。今回は、JavaScriptから利用されることを想定して、GenerateSecretはfalseとしています。

IdentityPool

AllowUnauthenticatedIdentitiesをfalseにして、認証済みユーザーと未認証ユーザーのIAMRoleをわけるようにします。認証と連携するのであれば、ここはfalseになるはずです。CognitoIdentityProvidersのProviderNameは難しそうに見えますが、cognito-idp.[region].amazonaws.com/[UserPoolId]になるようにしているだけです。

外部ID連携をするのであれば、CognitoIdentityProvidersの代わりにSupportedLoginProvidersを設定すればよいようです。しかし、ここの説明がなさすぎて、ぼんやりとしか設定方法がわかりません。Keyの名前どうすんの?とかいろいろ思うところはありますが、これから拡充されていくといいですね。

AuthRole/UnauthRole(IAM Role)

IAM Roleを作成しています。このまま流用するのであれば、Policiesが、各ユーザーに割り当てられるPolicyになるので、そこを修正していただければと思います。

IdentityPoolRole(IdentityPoolRoleAttachment)

この部分のドキュメントのSyntax(JSON)のRolesが、配列になっていますが、ここで思いっきりハマりました。ここはArrayではなくObjectで書けばいいようです。ドキュメントちょっと間違えてますね〜。

Outputs

UserPoolsとFederated Identitiesを利用する際に必要になる3つのIDを出力するようにしました。

ちなみに

IAMを作ることになるので、aws-cliを使ってスタックを作成する場合は、--capabilities CAPABILITY_NAMED_IAMオプションが必要になります。ご注意を。

おわりに

手でポチポチ作ると、行ったり来たりしてIDを探さないといけなかったです。しかし、CloudFormationを使うと一発で必要なID3つが手に入れられるのが、控えめに言っても最高ですね。

おわり。