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
そこで今回は
- 既存のDynamoDBを使って、ユニットリゾルバーを作成
- 既存のDynamoDBを使って、パイプラインリゾルバーを作成
- Noneデータソースのリゾルバーを作成
の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": {
"Type": "String",
"Description": "ServiceRoleArnOfDynamoDB"
}
...
}
Resourcesの下に、DataSourceを追加
今回はDynamoDBの Board
テーブルをDataSourceとして追加します。
なお、 Ref
や AWS::AppSync::DataSource
、 DynamoDBConfig
の詳細については、以下のCloudFrontの公式ドキュメントに記載があります。
{
"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": {
"Type": "AWS::AppSync::DataSource",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"Name": "BoardDataSource",
"Type": "AMAZON_DYNAMODB",
"ServiceRoleArn": {
"Ref": "BoardDynamoDBServiceRoleArn"
},
"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": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"DataSourceName": {
"Fn::GetAtt": [
"DataSourceOfExistsDynamoDB",
"Name"
]
},
"TypeName": "Query",
"FieldName": "listBoards",
"RequestMappingTemplateS3Location": {
"Fn::Sub": [
"s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.listBoards.req.vtl",
{
"S3DeploymentBucket": {
"Ref": "S3DeploymentBucket"
},
"S3DeploymentRootKey": {
"Ref": "S3DeploymentRootKey"
}
}
]
},
"ResponseMappingTemplateS3Location": {
"Fn::Sub": [
"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",
"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のドキュメントは以下です。
{
"AWSTemplateFormatVersion": "2010-09-09",
...
"Parameters": {
...
"AuthorDynamoDBServiceRoleArn": {
"Type": "String",
"Description": "DynamoDBServiceRoleArn of Author table"
},
"BlogDynamoDBServiceRoleArn": {
"Type": "String",
"Description": "DynamoDBServiceRoleArn of Blog table"
}
},
"Resources": {
"AuthorTable": {
"Type": "AWS::AppSync::DataSource",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"Name": "AuthorDataSource",
"Type": "AMAZON_DYNAMODB",
"ServiceRoleArn": {
"Ref": "AuthorDynamoDBServiceRoleArn"
},
"DynamoDBConfig": {
"TableName": "Author",
"AwsRegion" : {
"Ref": "AWS::Region"
}
}
}
},
"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",
"PipelineConfig": {
"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"
}
}
]
}
}
},
"FunctionBlog": {
"Type": "AWS::AppSync::FunctionConfiguration",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"Name": "GetBlog",
"Description": "Get Blog Table",
"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"
}
}
]
}
}
},
"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"
}
},
"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