AWS Amplifyで、カスタムカテゴリを作って、カスタムリソースを追加してみた

今まで、Amplifyが用意する以外のリソースを使いたい場合は、 <project_root>/amplify/backend/api/<API name>/stacks ディレクトリの中に、CloudFormation(CFn)ファイルを作成して対応してきました。
AWS AppSync + Amplify JavaScript + CustomResourcesで、既存のDynamoDBなどをDatasourceとしたリゾルバーを作成する - メモ的な思考的な

 
それ以外の方法がないかを探したところ、以下のissueコメントに、カスタムカテゴリを作っても追加できる旨が記載されていました。
Support for custom Resources/CloudFormation templates · Issue #80 · aws-amplify/amplify-cli

そこで、issueに書かれた方法で試してみたため、メモを残します。

 
ちなみに、「カスタムカテゴリ」と表現するのが正しいのかは微妙ですが、

$ amplify --help


| Category      |
| ------------- |
| analytics     |
...

と、元々あるAnalyticsやAPIはカテゴリ(Category)と呼ぶようです。

そのため、この記事ではそれにならって、今回自作するカテゴリのことをカスタムカテゴリと呼ぶことにします。

 
目次

 

環境

 
今回は amplify init & amplify api add で、APIモジュールを追加した後の環境で、

  • CognitoのIDプールを作成し、amplify init で作成した承認済/未認証ロールを紐付ける
  • AppSync GraphQLへのアクセスを許可するIAMポリシーを作成し、未承認ロールに割り当てる

というカスタムリソースを書いてみます。

amplify init & amplify api add は以下のような感じです。

# 初期化
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project CustomCategory
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using none
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run-script build
? Start Command: npm run-script start
Using default provider  awscloudformation

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use default

# APIの追加
$ amplify api add
? Please select from one of the below mentioned services GraphQL
? Provide API name: CustomCategoryAPI
? Choose an authorization type for the API API key
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? No
? Provide a custom type name MyType

 

カスタムカテゴリを追加 

ディレクトリの作成

<project_root>/amplify/backend/ ディレクトリの下に、 identity ディレクトリを作成します。

 

CFnテンプレートの作成

今回はIDプールとRoleのマッピングを行うため、

  • AWS::Cognito::IdentityPool
  • AWS::Cognito::IdentityPoolRoleAttachment

の2つのResourceが必要です。

そのため、このResourceが含まれるファイルを template.json として、上記の identity ディレクトリに作成します。

 

IDプールの作成

まずは AWS::Cognito::IdentityPool のリソースを作成します。

amplifyのenvとIDプールの名称をパラメータとして受け取り、CoginitoのIDプールを作成します。

AllowUnauthenticatedIdentities: true としてありますが、IDプールの 認証されていない ID に対してアクセスを有効にする にチェックを入れてみたかっただけで、特に深い意味はありません。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "env": {
      "Type": "String",
    },
    "IdentityPoolName": {
      "Type": "String"
    },
...
  },
  "Resources": {
    "IdentityPool": {
      "Type": "AWS::Cognito::IdentityPool",
      "Properties": {
        "IdentityPoolName": {
          "Fn::Join": [
            "__",
            [
              {
                "Ref": "IdentityPoolName"
              },
              {
                "Ref": "env"
              }
            ]
          ]
        },
        "AllowUnauthenticatedIdentities": "true"
      }
    },

 

amplify initで作成されたものをnested-cloudformation-stack.ymlで確認し、ロールを割り当て

AWS::Cognito::IdentityPoolRoleAttachment を設定するには、

  • 認証されていないロール
  • 認証されたロール

が必要です。

今回は、Amplifyの動きと同じく、amplify init で作成される認証されていないロールと認証されたロールを、自作のIDプールに割り当ててみます。

CFn内でamplify initで作成されるロールを参照する方法を探したところ、 <project_root>/amplify/backend/awscloudformation/nested-cloudformation-stack.yml に記載されているリソースを使えばよさそうでした。

今回は、

  • AuthRole
  • UnauthRole

のArnを Fn::GetAtt で参照します。

 
ただ、CFnのテンプレート内では参照できなかったため、

  • CFnテンプレート (template.json) は Parameter で外部から値を受け取る
  • CFnテンプレートに値を渡すためのファイル parameters.json を作成し、その中で Fn::GetAtt を使ってロールのArnを取得・設定する

としました。

関係する部分を抜粋したのは以下です。

template.json

{
  "AWSTemplateFormatVersion": "2010-09-09",
  ...
  "Parameters": {
    ...
    "AuthRoleArn": {
      "Type": "String"
    },
    "UnauthRoleArn": {
      "Type": "String"
    }
  },
  "Resources": {
    ...
    "IdentityPoolRoleMap": {
      "Type": "AWS::Cognito::IdentityPoolRoleAttachment",
      "Properties": {
        "IdentityPoolId": {
          "Ref": "IdentityPool"
        },
        "Roles": {
          "unauthenticated": {
            "Ref": "UnauthRoleArn"
          },
          "authenticated": {
            "Ref": "AuthRoleArn"
          }
        }
      },
      "DependsOn": "IdentityPool"
    }
  },
...
}

 
parameters.json

{
  "IdentityPoolName": "CustomIdentityPool",
  "AuthRoleArn": {
    "Fn::GetAtt": [
      "AuthRole",
      "Arn"
    ]
  },
  "UnauthRoleArn": {
    "Fn::GetAtt": [
      "UnauthRole",
      "Arn"
    ]
  }
}

 

amplify env checkout

このままの状態で amplify push しようとしても、まだAmplify CLIにはAPIしか認識されていません。

$ amplify status

Current Environment: develop

| Category | Resource name     | Operation | Provider plugin   |
| -------- | ----------------- | --------- | ----------------- |
| Api      | CustomCategoryAPI | Create    | awscloudformation |

 
そのため、amplify env checkout を行い、Amplify CLIに認識してもらいます。

# 環境の確認
$ amplify env list

| Environments |
| ------------ |
| *develop     |


# チェックアウト
$ amplify env checkout develop


# 認識された
$ amplify status

Current Environment: develop

| Category | Resource name      | Operation | Provider plugin   |
| -------- | ------------------ | --------- | ----------------- |
| Api      | CustomCategoryAPI  | Create    | awscloudformation |
| Identity | CustomIdentityPool | Create    | awscloudformation |

 
また、amplify push などの情報を持っている <project_root>/amplify/backend/amplify-meta.json も更新されました。

{
    "providers": {
        "awscloudformation": {
            ...
        }
    },
    "api": {
        "CustomCategoryAPI": {
            ...
            }
        }
    },
    // 追加された
    "identity": {
        "CustomIdentityPool": {
            "providerPlugin": "awscloudformation"
        }
    }
}

 

amplify pushして状況確認

カスタムカテゴリが認識されたため、amplify push すると、成功しました。

$ amplify push

Current Environment: develop

| Category | Resource name      | Operation | Provider plugin   |
| -------- | ------------------ | --------- | ----------------- |
| Api      | CustomCategoryAPI  | Create    | awscloudformation |
| Identity | CustomIdentityPool | Create    | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully.

? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
...
✔ All resources are updated in the cloud

GraphQL endpoint: https://host.appsync-api.region.amazonaws.com/graphql
GraphQL API KEY: xxx

 
CognitoのIDプールを確認したところ、たしかに作成されていました。

f:id:thinkAmi:20190730221051p:plain:w450

 

IAMポリシーの作成と未承認ロールへの割り当て

まずは、AppSync GraphQLへのアクセスを許可するIAMポリシーを作成します。

IAMポリシーでGraphQLへのアクセスを許可するには "arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiID}/types/Mutation/* のようなResourceに対して設定する必要があります。

ただ、この中の apiID については、カスタムカテゴリの中では取得できませんでした。一方、APIカテゴリの中では取得できました。

 
そのため、 <project_root>/amplify/backend/api/<API name>/stacks/ ディレクトリの中に IAMRole.json を作成します。

この中で

  • パラメータ AppSyncApiId を参照し、AppSyncのAPI IDを取得
  • パラメータ UnauthRoleName を参照し、未承認ロールに対しポリシーを割り当て

を行います。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "AppSync GraphQL Policy",
  "Metadata": {},
  "Parameters": {
    "AppSyncApiId": {
      "Type": "String",
      "Description": "The id of the AppSync API associated with this project."
    },
    "env": {
      "Type": "String",
      "Description": "The environment name. e.g. Dev, Test, or Production",
      "Default": "NONE"
    },
    "UnauthRoleName": {
      "Type": "String"
    }
  },
  "Resources": {
    "AppSyncGraphQLPolicy": {
      "Type": "AWS::IAM::ManagedPolicy",
      "Properties": {
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "appsync:GraphQL"
              ],
              "Resource": [
                {
                  "Fn::Sub": [
                    "arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiID}/types/Query/*",
                    {
                      "apiID": {
                        "Ref": "AppSyncApiId"
                      }
                    }
                  ]
                },
                {
                  "Fn::Sub": [
                    "arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiID}/types/Mutation/*",
                    {
                      "apiID": {
                        "Ref": "AppSyncApiId"
                      }
                    }
                  ]
                }
              ]
            }
          ]
        },
        "ManagedPolicyName": {
          "Fn::Join": [
            "-",
            [
              "AppSyncGraphQLPolicy",
              {
                "Ref": "env"
              }
            ]
          ]
        },
        "Roles": [
          {
            "Ref": "UnauthRoleName"
          }
        ]
      }
    }
  },
  "Outputs": {
    "EmptyOutput": {
      "Description": "An empty output. You may delete this if you have at least one resource above.",
      "Value": ""
    }
  }
}

 

結果確認

未認証のロールに、AppSyncのGraphQLアクセスを許可するIAMポリシー AppSyncGraphQLPolicy-develop が割り当てられていました。

Cognito IDプールでの未認証ロール

f:id:thinkAmi:20190730222341p:plain:w450

ロールに割り当てられたポリシー

f:id:thinkAmi:20190730222416p:plain:w450

 

カスタムカテゴリを使わず、APIのカスタムリソースとして作成

ちなみに、今回作成したCognito IDプールのCFnファイルですが、カスタムカテゴリを使わなくても、APIカテゴリのカスタムリソースとすることも可能です。

<project_root>/amplify/backend/api/<API name>/build/cloud-formation-template.json を見ても、APIのカスタムリソースのパラメータが各リソースを参照できています。

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "An auto-generated nested stack.",
    "Metadata": {},
    "Parameters": {
        ...
        "AuthRoleArn": {
            "Type": "String"
        },
        "UnauthRoleArn": {
            "Type": "String"
        }
    },
    "Resources": {
        "GraphQLAPI": {
            "Type": "AWS::AppSync::GraphQLApi",
            ...
        },
        "GraphQLAPIKey": {
            "Type": "AWS::AppSync::ApiKey",
            ...
        },
        "GraphQLSchema": {
            "Type": "AWS::AppSync::GraphQLSchema",
            ...
        },
        "MyType": {
            "Type": "AWS::CloudFormation::Stack",
            ...
        },
        "CustomResourcesjson": {
            "Type": "AWS::CloudFormation::Stack",
            ...
        },
        "IAMRolejson": {
            "Type": "AWS::CloudFormation::Stack",
            ...
        },
        "templatejson": {
            "Type": "AWS::CloudFormation::Stack",
            "Properties": {
                "Parameters": {
                    "env": {
                        "Ref": "env"
                    },
                    "StackIdentityPoolName": {
                        "Ref": "StackIdentityPoolName"
                    },
                    "AuthRoleArn": {
                        "Ref": "AuthRoleArn"
                    },
                    "UnauthRoleArn": {
                        "Ref": "UnauthRoleArn"
                    }
                },

 

まとめ

以上より、AWS Amplifyでカスタムリソースを作る場合は

  • <project_root>/amplify/backend/awscloudformation/nested-cloudformation-stack.yml
  • <project_root>/amplify/backend/api/<API name>/build/cloud-formation-template.json

などを見て使えるリソースを探しつつ、

  • APIの中のstacksの中にCFnファイルを作成
  • カスタムカテゴリを作成し、その中にCFnファイルを作成

のどちらを行い、必要なパラメータは parameters.json に記載すれば良いことが分かりました。

 

ソースコード

Githubに上げました。ディレクトcustom_category 以下が今回のファイルです。
https://github.com/thinkAmi-sandbox/AWS_AppSync_Amplify-sample