別のAmplifyプロジェクトで作成したDynamoDBのテーブルなど、既存のDynamoDBテーブルをAmplifyから使用する備忘録。(ここで記載しているほぼ全ての技術に関して使い始めて日が浅いので、間違いなどありましたらご指摘お願いします。)
参考記事
この2つの記事でほとんどわかるが、いくつかハマったので別途メモる。
- Connecting Amplify AppSync to an imported DynamoDB Table
- AWS AppSync + Amplify JavaScript + CustomResourcesで、既存のDynamoDBなどをDatasourceとしたリゾルバーを作成する
ちなみに、本件に関するAmplifyの公式ドキュメントは、2021/08/03現在、まるで役に立たなかった。
前提
- 別のAmplifyプロジェクトで構築したDynamoDBのテーブルに新規作成するAmplifyプロジェクトから直接アクセスする
- 影響しているかわからないが、とりあえず両方のAmplifyでユーザ認証不要にするため、apiのauthをAPI keyにしている
(この状態だと、AmplifyのGraphQLスキーマ記述に@auth
が使用できなくなっている) - AmplifyやGraphQLの基礎はわかっている(Amplify公式チュートリアル、GraphQL基本)
- ユニットリゾルバを使用する。参考記事2ではパイプラインリゾルバでの方法も説明されている
手順
手順はすてべ既存のテーブルにアクセスする側のAmplifyプロジェクトのものでamplify init
済みとする。
DynamoDBテーブルをインポート
2021/08/05追記:唯一公式ドキュメントに書いてあるこのステップだが、やらなくてもデータの取得はできた。何に必要になるのか今のところ不明。Amplify env毎に切り替えられないようなので、できればやりたくない。
-
amplify import storage
を実行する。 -
DynamoDB table - NoSQL Database
を選択 - テーブルを選択
MacBook-Pro.local:amplify-js-app% amplify import storage
? Please select from one of the below mentioned services: DynamoDB table - NoSQL Database
✔ Select the DynamoDB Table you want to import: · Todo-xxxxxxxxxxxxxxxxxx-dev
✅ DynamoDB Table 'Todo-xxxxxxxxxxxxxxxxxx-dev' was successfully imported.
Next steps:
- This resource can now be accessed from REST APIs (‘amplify add api’) and Functions (‘amplify add function’)
スキーマを定義
既存のテーブルと同じスキーマを定義する(完全に同じでなくても良い)。
amplify/backend/api/myapi/schema.graphql
のようにAmplify CLIが自動生成しているファイルを下記の要領でスキーマを記述する。
このファイルがまだない場合は、amplify add api
を実行してGraphQLを選んでプロジェクトにAPIを追加する。
DynamoDBテーブルを作成しないので@model
ディレクティブは付けない。
要は、Amplifyが生成するamplify/backend/api/myapi/build/schema.graphql
を自分で記述するわけなので、既存のAmplifyプロジェクトでAmplifyが作成したそのファイルが参考になる。
type Query {
listTodos(limit: Int, nextToken: String): TodoList
}
type Todo {
id: ID!
description: String!
}
type TodoList {
items: [Todo]
}
ここではリストを取得するQueryを定義しているが、ページ上部で紹介している参考記事1では、インデックスを使って特定のアイテムを取得する方法を紹介している。
リゾルバを定義
参考記事1, 2ではシンプルなリゾルバ定義を使用しているが、本記事ではシンプルなモデルを@model
で定義したときにAmplifyがbuildディレクトリに作成する.req.vtlファイルの中身をまんまコピーする。(若干不要な部分はあるかもしれない・・・)
難しそうに見えるが、最終的にはJSONでAppSyncのリゾルバーテンプレートを出力すれば良く、単純なものだとただのJSONになる。
下記の例では、Velocityという言語で入力パラメータに基づいてJSONを作成している。良く見てみると大して難しくない。
#set( $limit = $util.defaultIfNull($context.args.limit, 100) )
#set( $ListRequest = {
"version": "2018-05-29",
"limit": $limit
} )
#if( $context.args.nextToken )
#set( $ListRequest.nextToken = $context.args.nextToken )
#end
#if( $context.args.filter )
#set( $ListRequest.filter = $util.parseJson("$util.transform.toDynamoDBFilterExpression($ctx.args.filter)") )
#end
#if( !$util.isNull($modelQueryExpression)
&& !$util.isNullOrEmpty($modelQueryExpression.expression) )
$util.qr($ListRequest.put("operation", "Query"))
$util.qr($ListRequest.put("query", $modelQueryExpression))
#if( !$util.isNull($ctx.args.sortDirection) && $ctx.args.sortDirection == "DESC" )
#set( $ListRequest.scanIndexForward = false )
#else
#set( $ListRequest.scanIndexForward = true )
#end
#else
$util.qr($ListRequest.put("operation", "Scan"))
#end
$util.toJson($ListRequest)
$util.toJson($ctx.result)
自分でリゾルバテンプレートを記述する際は下記あたりが参考になる。
CloudFormationテンプレートでリソースを定義
環境毎の変数を準備
もし、接続先のDynamoDBが別のAmplifyプロジェクトで作成されたものならば、テーブル名は
{任意のテーブル名}-{AppSyncのAPI ID}-{Amplify環境名}
となっているはずだ。
「Amplify環境名」は利用元のAmplify環境を同じ名前にしてしまえば${env}
で対応できるが、「AppSyncのAPI ID」は設定で指定するしかない。
下記の様に使用する環境の設定に、categories.api.{API名}.{任意の変数名}
を追加する。
ここで指定する OtherAppSyncApiId
は実際に参照したいテーブル名を見れば確認できるし、AppSyncのコンソールでも確認できる。
{
"devkanji": {
"awscloudformation": {
...
},
"categories": {
"api": {
"myapi": {
"OtherAppSyncApiId": "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}
}
これで準備ができたので amplify/backend/api/myapi/stacks/CustomResource.json
に下記を追記する。
DyanmoDBアクセス用Roleの追加
下記のようにResources
の下にRole
を追加する。
参考記事2では既存のRoleを使用しているが、ここでは自動的に作成されるようにする。
{
"Resources": {
...
"DynamoDBDataSourceRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"RoleName": "DynamoDBDataSource",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Policies": [
{
"PolicyName": "DynamoDBFullAccess",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:*"
],
"Resource": "arn:aws:dynamodb:ap-northeast-1:000000000000:table/Todo-xxxxxxxxxxxxxxxx-dev",
}
]
}
}
]
}
},
...
}
}
ここでは、Policies
の中のResource
にアクセスしたいテーブルのARNを設定する。
上記例では固定値を使用したが、実際には環境ごとに自動的に切り替わるようにFn::Subで、変数を利用する。
前のステップで準備した変数OtherAppSyncId
はここで利用する。
まず、CustomResource.jsonファイルにこのパラメータを使用する宣言をする。
{
...
"Parameters": {
"AppSyncApiId": { // これは元々あるが、このAmplifyが作っているAppSyncのAPI ID
"Type": "String",
"Description": "The id of the AppSync API associated with this project."
},
...
"OtherAppSyncApiId": { // これを追加する
"Type": "String",
"Description": "The AppSync API ID which is different for each environment"
}
},
"Resources": {
...
}
}
そして下記のようにResource
の部分で変数を使用する。${AWS::Region}
${AWS::AccountId}
${env}
は既に定義されているのですぐに使える。
"Resource": [
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/Todo-${OtherAppSyncApiId}-${env}",
{
"env": {
"Ref": "env"
},
"OtherAppSyncApiId": {
"Ref": "OtherAppSyncApiId"
}
}
]
}
]
追記
RoleName
が同じ名前のが存在するとエラーになるので、"RoleName":
の1行も下記に置き換えて環境名でサフィックスします。
"RoleName": {
"Fn::Sub": [
"DynamoDBDataSource-${env}",
{
"env": {
"Ref": "env"
}
}
]
},
DataSourceの追加
下記のようにResources
の下にDataSource
を追加して、どのテーブルにアクセスするかを定義する。
{
"Resources": {
...
"DynamoDBDataSource": {
"Type": "AWS::AppSync::DataSource",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"Name": "TodoDataset", // 任意の名称
"Type": "AMAZON_DYNAMODB",
"ServiceRoleArn": {
"Fn::GetAtt": [
"DynamoDBDataSourceRole",
"Arn"
]
},
"DynamoDBConfig": {
"TableName": "Todo-xxxxxxxxxxxxxxxx-dev",
"AwsRegion": "ap-northeast-1"
}
}
},
...
}
}
ここではDynamoDBConfig.TableName
でアクセスするテーブル名を指定している。
このテーブル名はAmplifyがimport storage
で付けた名前ではなく実際のDynamoDBでの名前になる。
また、ここでもTableName
とAwsRegion
に変数を利用できる。
"TableName": {
"Fn::Sub": [
"Todo-${OtherAppSyncApiId}-${env}",
{
"env": {
"Ref": "env"
},
"OtherAppSyncApiId": {
"Ref": "OtherAppSyncApiId"
}
}
]
},
"AwsRegion": { "Fn::Sub": "${AWS::Region}" }
リゾルバを登録
下記のようにResources
の下にResolver
を追加して、先に作成したリゾルバーを登録する。
-
DataSourceName
で先に定義したDataSourceを指定 -
FieldName
でschema.graphqlのQueryで定義した名前を指定 -
RequestMappingTemplateS3Location
とResponseMappingTemplateS3Location
でそれぞれのリゾルバファイル名を指定
{
"Resources": {
...
"QueryListTodosResolver": {
"Type": "AWS::AppSync::Resolver",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"DataSourceName": { // 参考記事1の書き方だとうまく行かなかったとこ
"Fn::GetAtt": [
"DynamoDBDataSource",
"Name"
]
},
"TypeName": "Query",
"FieldName": "listTodos", // schema.graphqlのQueryで定義した名前
"RequestMappingTemplateS3Location": {
"Fn::Sub": [
"s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.listTodos.req.vtl",
{
"S3DeploymentBucket": {
"Ref": "S3DeploymentBucket"
},
"S3DeploymentRootKey": {
"Ref": "S3DeploymentRootKey"
}
}
]
},
"ResponseMappingTemplateS3Location": {
"Fn::Sub": [
"s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.listTodos.res.vtl",
{
"S3DeploymentBucket": {
"Ref": "S3DeploymentBucket"
},
"S3DeploymentRootKey": {
"Ref": "S3DeploymentRootKey"
}
}
]
}
}
}
...
}
}
環境に反映
amplify push -y
で環境に反映する。
オプションを変更したい場合は-y
を取ってっ実行。
動作確認
AppSyncコンソールのQueriesから確認
入力
query MyQuery {
listTodos(limit: 3) {
items {
description
id
}
}
}
結果
{
"data": {
"listTodos": {
"items": [
{
"description": "洗濯をする",
"id": "8d24bed5-923a-4f5f-b37c-b15e5bc3b1e1"
},
{
"description": "納豆を買いに行く",
"id": "d17ffdca-8fd9-43e1-80cd-55a5a2b56b39"
},
{
"description": "猫に餌をあげる",
"id": "d9a4a26f-762f-4bee-928e-140a0b1b3819"
}
]
}
}
}
以上。
解決していない課題
- 双方または片方に認証を入れた場合の扱い
- データ作成者のユーザIDを既存テーブルにどうやって入れるのか
- subscriptionで既存テーブルの更新を検知できるのか