AWS Amplify JavaScriptを使ってAWS AppSync APIを作成する場合、 amplify add api
した直後はDynamoDBのテーブルが新規作成されます。
既存のDynamoDBを使いたい場合は、 amplify push
でAPIをデプロイ後にAppSync Consoleにて内容を編集することもできます。
ただ、手作業になるため
- 同一環境の再現
- 作業ミスの防止
などは難しいです。
コードベースでカスタマイズする方法を探したところ、CustomResourcesを使えば良さそうでした。
RFC: Custom data sources, resolvers, and resources with GraphQL API category. · Issue #574 · aws-amplify/amplify-cli
そこで今回は
の3パターンを試してみた時のメモを残します。
なお、multi-env下で各環境固有のパラメータもCustomResourceに設定しようとしましたが、現状ではできないようです。以下のissueが対応されれば、将来的にはできるようになるかもしれません。
How can you define custom environment-specific variables? · Issue #1366 · aws-amplify/amplify-cli
また、CustomResourcesはCloudFormationの設定ファイルを書く感じとなります。
ただ、CloudFormationの設定ファイルはJSONとYAMLの両方をサポートしていますが、Amplifyで作成する場合はJSONのみサポートしています。
そのため、YAMLで記述したCustomResourceを使おうとすると、以下のエラーが発生します。
Yaml is not yet supported. Please convert the CloudFormation stack ExistsDynamoDB.yaml to json.
目次
環境
また、AppSync API環境は、以下の方法で作成したものとします。
amplify init
$ amplify init Note: It is recommended to run this command from the root of your app directory ? Enter a name for the project infra_by_amplify ? 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 CREATE_IN_PROGRESS infrabyamplify-dev-xxx AWS::CloudFormation::Stack ... User Initiated CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket ... CREATE_IN_PROGRESS AuthRole AWS::IAM::Role ... CREATE_IN_PROGRESS UnauthRole AWS::IAM::Role ...
amplify add api
$ amplify add api ? Please select from one of the below mentioned services GraphQL ? Provide API name: InfraAPI ? 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 # Schemaなしは選べなかったので、やむを得ず MyType というtypeを作成 ? Provide a custom type name MyType
既存のDynamoDBを使って、ユニットリゾルバーを作成
AppSyncのリゾルバーには
の2つがあります。
システムの概要とアーキテクチャ - AWS AppSync
まずは、既存のDynamoDBをDataSourceとしたユニットリゾルバーを作成してみます。
今回は Board
という既存のDynamoDBを使います。スキーマとデータは以下の通りです。
key | author | content |
---|---|---|
1 | baz | egg |
schema.graphqlの変更
まずは、デフォルトで作成されたSchemaを変更します。
project_root/amplify/backend/api/infraAPI/schema.graphql
を開き、 Query listBoards
用のSchemaに変更します。今回はDynamoDBからデータを取得するQueryを定義します。
なお、DynamoDBは既に存在しているため、 @model
ディレクティブは不要です。
type Board { key: String author: String content: String } type BoardConnection { items: [Board] nextToken: String } type Query { listBoards(limit: Int, nextToken: String): BoardConnection }
stacksに、CustomResourcesを追加
続いて、
- 既存のDynamoDBをDatasourceとして使う
- Schemaに対するユニットリゾルバーを作成する
を行うため、 project_root/amplify/backend/api/infraAPI/stacks
の中に ExistsDynamoDB.json
ファイルを作成します。
なお、同じディレクトリには CustomResources.json
があります。このファイルに追記しても良いですし、別ファイルとして作成しても良いです。
今回は、 CustomResources.json
をベースに必要な項目を追加した ExistsDynamoDB.json
となります。
Parametersの下に ServiceRoleARN 用の項目を追加
まずは、使い回ししやすくするため、DynamoDBを操作するためのServiceRoleARNを外部から渡せるようにします。
Parameters
の下に、BoardDynamoDBServiceRoleArn
を追加します。
{ "AWSTemplateFormatVersion": "2010-09-09", ... "Parameters": { // 以下を追加 "BoardDynamoDBServiceRoleArn": { // 外部ファイルから文字列で受け取るため、 "String" を指定 "Type": "String", "Description": "ServiceRoleArnOfDynamoDB" } ... }
Resourcesの下に、DataSourceを追加
今回はDynamoDBの Board
テーブルをDataSourceとして追加します。
なお、 Ref
や AWS::AppSync::DataSource
、 DynamoDBConfig
の詳細については、以下のCloudFrontの公式ドキュメントに記載があります。
- Ref - AWS CloudFormation
- AWS::AppSync::DataSource - AWS CloudFormation
- AWS AppSync DataSource DynamoDBConfig - AWS CloudFormation
{ "AWSTemplateFormatVersion": "2010-09-09", ... "Parameters": { "AppSyncApiId": { "Type": "String", "Description": "The id of the AppSync API associated with this project." }, ... "BoardDynamoDBServiceRoleArn": { "Type": "String", "Description": "ServiceRoleArnOfDynamoDB" } }, "Resources": { // 以下を追加 "DataSourceOfExistsDynamoDB": { // DataSourceの定義をするので、固定値 "Type": "AWS::AppSync::DataSource", "Properties": { // どこのAPIに紐付けるか // "Ref": "AppSyncApiId"で、上のParametersで指定 AppSyncApiId の値を渡す "ApiId": { "Ref": "AppSyncApiId" }, // 任意の名前、AppSync Console - Data Source のNameとなる "Name": "BoardDataSource", // DynamoDBを使うので固定 "Type": "AMAZON_DYNAMODB", // DynamoDBを使うので必須 // 上のParametersで指定した BoardDynamoDBServiceRoleArn の値を渡す "ServiceRoleArn": { "Ref": "BoardDynamoDBServiceRoleArn" }, // DynamoDBの設定 "DynamoDBConfig": { "TableName": "Board", "AwsRegion" : { "Ref": "AWS::Region" } } } }, }
Resourcesの下に、ユニットリゾルバーを追加
DataSourceができたので、次はQueryとDataSourceをつなぐリゾルバーを追加します。
関係するCloudFormationのドキュメントはこちらです。
{ "AWSTemplateFormatVersion": "2010-09-09", ... "Parameters": { "AppSyncApiId": { ... }, ... "S3DeploymentBucket": { ... }, "S3DeploymentRootKey": { ... }, ... }, "Resources": { "DataSourceOfExistsDynamoDB": { ... "Name": "BoardDataSource", ... } } }, // 以下を追加 "ListBoardsResolver": { // リゾルバーを追加するので固定値 "Type": "AWS::AppSync::Resolver", "Properties": { // リゾルバーを作成するAPI "ApiId": { "Ref": "AppSyncApiId" }, // リゾルバーのDataSource。名前を参照するため、先ほど追加した "DataSourceOfExistsDynamoDB" の "Name" を使う "DataSourceName": { "Fn::GetAtt": [ "DataSourceOfExistsDynamoDB", "Name" ] }, // リゾルバーのType。今回はQuery用のリゾルバー "TypeName": "Query", // リゾルバーをAttachするQuery名。Schemaに書いたものを指定 "FieldName": "listBoards", // リクエストマッピングテンプレートの指定。直接書くこともできるが、今回はS3にテンプレートをアップロードして、そちらを参照する // 書き方は定形 "RequestMappingTemplateS3Location": { "Fn::Sub": [ // 後で resolvers ディレクトリに用意するリクエストマッピングテンプレートのファイル名を指定 "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.listBoards.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, // 同じく、レスポンスマッピングテンプレートを指定 "ResponseMappingTemplateS3Location": { "Fn::Sub": [ // 同様に、 resolvers ディレクトリに用意するリクエストマッピングテンプレートのファイル名を指定 "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.listBoards.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } ...
resolversに、マッピングテンプレートを作成
次に、 project_root/amplify/backend/api/infraAPI/resolvers
の中に、リクエスト/レスポンスマッピングテンプレートを作成します。
リクエストマッピングテンプレートを作成
stacks/ExistsDynamoDB.json
で指定したファイル名 Query.listBoards.req.vtl
にてリクエストマッピングテンプレートを作成します。
今回はスキャンのテンプレートを作成します。
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-dynamodb.html#aws-appsync-resolver-mapping-template-reference-dynamodb-scan
{ "version": "2017-02-28", "operation": "Scan", "limit": $util.defaultIfNull($ctx.args.limit, 20), "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null)), }
レスポンスマッピングテンプレートを作成
同じく、 stacks/ExistsDynamoDB.json
で指定したファイル名 Query.listBoards.res.vtl
にてレスポンスマッピングテンプレートを作成します。
今回は取得した結果をそのまま返します。
$util.toJson($context.result)
parameters.jsonの追加
最後に、ServiceRoleARN をCustomResourcesに渡すため、今回は project_root/amplify/backend/api/InfraAPI/paramters.json
に追加します。
{ "AppSyncApiName": "InfraAPI", "DynamoDBBillingMode": "PAY_PER_REQUEST", // Boardテーブルを操作可能なServiceRoleのARNを追加 "BoardDynamoDBServiceRoleArn": "arn:aws:iam::xxx:role/service-role/appsync-xxx-Board", }
amplify push
ここまでで作業が終わったため、AppSync APIを作成します。
$ amplify push Current Environment: dev | Category | Resource name | Operation | Provider plugin | | -------- | ------------- | --------- | ----------------- | | Api | InfraAPI | Create | awscloudformation | ? Are you sure you want to continue? Yes GraphQL schema compiled successfully. Edit your schema at path/to/infra_by_amplify/amplify/backend/api/InfraAPI/schema.graphql or place .graphql files in a directory at path/to/infra_by_amplify/amplify/backend/api/InfraAPI/schema # APIを新しく作成する ? Do you want to generate code for your newly created GraphQL API Yes # JavaScript用のコードを生成する ? Choose the code generation language target javascript # 生成するコードを置くディレクトリなどを指定する ? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js # JavaScript用のQueryコードを自動生成する ? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes # ネストはデフォルト(2)のまま ? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
pushが終わると、AppSync APIが新規作成されています。AppSync Consoleで自分が書いた内容と一致するか確認します。
動作確認
最後に、AppSync ConsoleのQueriesを使って動作確認をします。
Queriesに
query listBoards { listBoards { items { key author content } nextToken } }
と記載し、
{ "data": { "listBoards": { "items": [ { "key": "1ad5dcf8-ca8b-4eba-9193-5c3c37371644", "author": "baz", "content": "egg" } ], "nextToken": null } } }
となれば、無事に作成できています。
既存のDynamoDBを使って、パイプラインリゾルバーを作成
続いて、既存のDynamoDBを使って、パイプラインリゾルバーを作成してみます。
パイプラインリゾルバーをAppSync Console上で作成するのは以前試しました。 AWS AppSyncのPipeline Resolverを使って、複数のDynamoDBの値をマージして返すAPIを作成してみた - メモ的な思考的な
そこで今回は、上記記事と同じ内容でパイプラインリゾルバを作成します。
なお、DynamoDBの状況は以下の通りです。
Blog table
id (key) | title | author_id |
---|---|---|
1 | ham | 100 |
2 | spam | 200 |
Author table
Blog tableの author_id
に紐づく id
を持つテーブルです。
id (key) | name |
---|---|
100 | foo |
200 | bar |
schema.graphqlへの追記
type BlogWithAuthor { id: String! title: String author_id: String author_name: String } type Query { ... getByPipeline(id: String!): BlogWithAuthor }
stacks/PipelineResolver.jsonの作成
ユニットリゾルバーとは別のファイル PipelineResolver.json
に、CustomResources を追加していきます。
ユニットリゾルバーとは異なる部分をメインにコメントを入れてあります。また、関係するCloudFormationのドキュメントは以下です。
- AWS AppSync リゾルバー PipelineConfig - AWS CloudFormation
- AWS::AppSync::FunctionConfiguration - AWS CloudFormation
{ "AWSTemplateFormatVersion": "2010-09-09", ... "Parameters": { ... // DynamoDB "Author" を操作するための ServiceRoleArn を渡すためのパラメータ "AuthorDynamoDBServiceRoleArn": { "Type": "String", "Description": "DynamoDBServiceRoleArn of Author table" }, // DynamoDB "Blog" を操作するための ServiceRoleArn を渡すためのパラメータ "BlogDynamoDBServiceRoleArn": { "Type": "String", "Description": "DynamoDBServiceRoleArn of Blog table" } }, "Resources": { // 既存のDynamoDBテーブルその1 "AuthorTable": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "AuthorDataSource", "Type": "AMAZON_DYNAMODB", "ServiceRoleArn": { "Ref": "AuthorDynamoDBServiceRoleArn" }, "DynamoDBConfig": { "TableName": "Author", "AwsRegion" : { "Ref": "AWS::Region" } } } }, // 既存のDynamoDBテーブルその1 "BlogTable": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "BlogDataSource", "Type": "AMAZON_DYNAMODB", "ServiceRoleArn": { "Ref": "BlogDynamoDBServiceRoleArn" }, "DynamoDBConfig": { "TableName": "Blog", "AwsRegion" : { "Ref": "AWS::Region" } } } }, "PipeLineResolverOfAuthorAndBlog": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "TypeName": "Query", "FieldName": "getByPipeline", // パイプラインリゾルバーの場合、 `Kind: "PIPELINE"` が必要 "Kind": "PIPELINE", // パイプラインリゾルバーの設定 "PipelineConfig": { // パイプラインリゾルバーで使用するFunctionの "FunctionId" を、使用順で定義する "Functions": [ { "Fn::GetAtt" : [ "FunctionBlog" , "FunctionId" ] }, { "Fn::GetAtt" : [ "FunctionBlogWithAuthor" , "FunctionId" ] } ] }, "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.PipeLineResolverOfAuthorAndBlog.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.PipeLineResolverOfAuthorAndBlog.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } }, // Blogテーブルからデータを取得するFunction "FunctionBlog": { // Functionの定義なので固定 "Type": "AWS::AppSync::FunctionConfiguration", "Properties": { // Functionを含むAppSync APIのID "ApiId": { "Ref": "AppSyncApiId" }, // Functionの名前 "Name": "GetBlog", "Description": "Get Blog Table", // FunctionのDataSource "DataSourceName": { "Fn::GetAtt" : [ "BlogTable" , "Name" ] }, "FunctionVersion": "2018-05-29", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.GetBlog.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.GetBlog.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } }, // Authorテーブルからデータを取得するFunction "FunctionBlogWithAuthor": { "Type": "AWS::AppSync::FunctionConfiguration", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "GetBlogWithAuthor", "Description": "Merge Blog and Author", "DataSourceName": { "Fn::GetAtt" : [ "AuthorTable" , "Name" ] }, "FunctionVersion": "2018-05-29", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.GetBlogWithAuthor.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.GetBlogWithAuthor.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } }, ... }
マッピングテンプレートを作成
今回は、1つのパイプラインリゾルバーと、2つのFunctionを作成したため、それぞれのマッピングテンプレートを作成します。
パイプラインリゾルバー用
resolvers
ディレクトリの中に、Before mapping template Query.PipeLineResolverOfAuthorAndBlog.req.vtl
を作成します。Queryの引数として id
を受け取ります。
#set($result = { "id": $ctx.args.id }) $util.toJson($result)
after mapping template Query.PipeLineResolverOfAuthorAndBlog.res.vtl
はこちら。Functionの結果をそのまま返します。
$util.toJson($ctx.result)
GetBlog Function用
リクエストマッピングテンプレート Query.GetBlog.req.vtl
です。今回は GetItem
を使います。
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-dynamodb.html#aws-appsync-resolver-mapping-template-reference-dynamodb-getitem
また、 $context
の短縮形 $ctx
を使っています。
{ "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), } }
レスポンスマッピングテンプレート Query.GetBlog.res.vtl
です。エラーがあればエラーを返します。
## Raise a GraphQL field error in case of a datasource invocation error #if($ctx.error) $util.error($ctx.error.message, $ctx.error.type) #end ## Pass back the result from DynamoDB. ** $util.toJson($ctx.result)
GetBlogWithAuthor Function用
リクエストマッピングテンプレート Query.GetBlogWithAuthor.req.vtl
です。
{ "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.prev.result.author_id), } }
リクエストマッピングテンプレート Query.GetBlogWithAuthor.res.vtl
です。
今回はGetBlogで取得したデータに追加していますが、GetBlogWithAuthorのデータにGetBlogのデータをマージすることもできます。
## Raise a GraphQL field error in case of a datasource invocation error #if($ctx.error) $util.error($ctx.error.message, $ctx.error.type) #end ## Pass back the result from DynamoDB. ** ## GetBlogの結果に、author_nameとして今回取得したAuthorテーブルのnameの値をマージ $util.qr($ctx.prev.result.put("author_name", $ctx.result.name)) ## マージしたデータを返す $util.toJson($ctx.prev.result)
parameters.jsonへの追加
今回もDynamoDBの操作用ServiceRoleを設定していますので、 parameters.json
へも追加します。
{ // 追加 "AuthorDynamoDBServiceRoleArn": "arn:aws:iam::xxx:role/service-role/appsync-xxx-Author", "BlogDynamoDBServiceRoleArn": "arn:aws:iam::xxx:role/service-role/appsync-xxx-Blog" }
以上で作成が終わりました。
動作確認
ユニットリゾルバーと同様に、AppSync ConsoleのQueriesにて動作確認をします。
query GetByPipeline($id: String!) { getByPipeline(id: $id) { id title author_id author_name } }
QUERY VARIABLES
にも以下を追加します。
{ "id": "1" }
実行すると、以下の結果が得られます。
{ "data": { "getByPipeline": { "id": "1", "title": "ham", "author_id": "100", "author_name": "foo" } } }
Noneデータソースのリゾルバーを作成
今まで、既存DynamoDBをDatasourceとして使ってみました。
ただ、AppSyncではDynamoDB以外にもDatasourceとして扱えるものがあり、それらもCustomResourceで生成できます。
リゾルバーのマッピングテンプレートリファレンス - AWS AppSync
今回は、ローカルリゾルバーに向いている None データソースのリゾルバーを作成してみます。
チュートリアル : ローカルリゾルバー - AWS AppSync
schema.graphql の追加
NoneデータソースのQueryを追加します。
type NoneResponse { comment: String } type Query { ... getNoneDatasource: NoneResponse }
stacks/ResolverWithNoneDatasource.json の追加
{ "AWSTemplateFormatVersion": "2010-09-09", ... "Resources": { "NoneSourceOfGetContext": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "NoneSource", // TypeをNoneにする "Type": "NONE" } }, "GetContextResolver": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": { "Fn::GetAtt": [ "NoneSourceOfGetContext", "Name" ] }, "TypeName": "Query", "FieldName": "getNoneDatasource", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.getNoneDatasource.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.getNoneDatasource.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } }, ... }
リクエスト/レスポンスマッピングテンプレートを作成
リクエストマッピングテンプレート
resolvers/Query.getNoneDatasource.req.vtl
を作成します。
{ "version": "2017-02-28", "payload": { "comment": "Hello, world!" } }
レスポンスマッピングテンプレート
resolvers/Query.getNoneDatasource.res.vtl
を作成します。
$utils.toJson($context.result)
動作確認
こちらもAppSync Consoleで動作を確認します。
query GetNoneDatasource { getNoneDatasource { comment } }
を実行すると
{ "data": { "getNoneDatasource": { "comment": "Hello, world!" } } }
の結果が得られました。
ソースコード
Githubに上げました。ディレクトリ infra_by_amplify
の中が今回のファイルです。
https://github.com/thinkAmi-sandbox/AWS_AppSync_Amplify-sample