12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

既存DynamoDBをAmplifyで使うメモ

Last updated at Posted at 2021-08-04

別のAmplifyプロジェクトで作成したDynamoDBのテーブルなど、既存のDynamoDBテーブルをAmplifyから使用する備忘録。(ここで記載しているほぼ全ての技術に関して使い始めて日が浅いので、間違いなどありましたらご指摘お願いします。)

参考記事

この2つの記事でほとんどわかるが、いくつかハマったので別途メモる。

  1. Connecting Amplify AppSync to an imported DynamoDB Table
  2. 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を作成している。良く見てみると大して難しくない。

amplify/backend/api/myapi/resolvers/Query.listTodos.req.vtl
#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)
amplify/backend/api/myapi/resolvers/Query.listTodos.res.vtl
$util.toJson($ctx.result)

自分でリゾルバテンプレートを記述する際は下記あたりが参考になる。

CloudFormationテンプレートでリソースを定義

環境毎の変数を準備

もし、接続先のDynamoDBが別のAmplifyプロジェクトで作成されたものならば、テーブル名は
{任意のテーブル名}-{AppSyncのAPI ID}-{Amplify環境名} となっているはずだ。
「Amplify環境名」は利用元のAmplify環境を同じ名前にしてしまえば${env}で対応できるが、「AppSyncのAPI ID」は設定で指定するしかない。

下記の様に使用する環境の設定に、categories.api.{API名}.{任意の変数名}を追加する。
ここで指定する OtherAppSyncApiId は実際に参照したいテーブル名を見れば確認できるし、AppSyncのコンソールでも確認できる。

amplify/team-provider-info.json
{
  "devkanji": {
    "awscloudformation": {
      ...
    },
    "categories": {
      "api": {
        "myapi": {
          "OtherAppSyncApiId": "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
        }
      }
    }
  }
}

この情報のソース

これで準備ができたので amplify/backend/api/myapi/stacks/CustomResource.json に下記を追記する。

DyanmoDBアクセス用Roleの追加

下記のようにResourcesの下にRoleを追加する。
参考記事2では既存のRoleを使用しているが、ここでは自動的に作成されるようにする。

amplify/backend/api/myapi/stacks/CustomResource.json
{
  "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ファイルにこのパラメータを使用する宣言をする。

amplify/backend/api/myapi/stacks/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での名前になる。

また、ここでもTableNameAwsRegionに変数を利用できる。

"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で定義した名前を指定
  • RequestMappingTemplateS3LocationResponseMappingTemplateS3Locationでそれぞれのリゾルバファイル名を指定
{
  "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で既存テーブルの更新を検知できるのか
12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?