AWS AppSyncにて、同じ内容でMutationした場合に、Subscriptionがどうなるかを試してみた

AWS AppSyncでは、Mutationした時の通知をSubscriptionで受け取れます。
リアルタイムデータ - AWS AppSync

ただ、DynamoDBのレコードと同じ内容でMutationした場合でも、Subscriptionがどのように動作するのか気になったため、試してみた時のメモを残します。

 
目次

 

環境

  • AWS AppSync
  • aws-amplify 1.1.32

 

Schema

今回用意したSchemaです。Mutationに

  • createTodo
  • updateTodo

の2つを用意しています。

今回は、onCreateTodoのSubscriptionがupdateTodoのMutationに反応しないかを確認するため、create/updateと名称を分けています。

ただし、GraphQL的にはSQLのInsert/Updateのような区別はありません。

type Todo {
    title: String!
    content: String
}

input TodoInput {
    title: String!
    content: String
}

type Mutation {
    createTodo(input: TodoInput!): Todo
    updateTodo(input: TodoInput!): Todo
}

type Subscription {
    onCreateTodo(title: String, content: String): Todo
        @aws_subscribe(mutations: ["createTodo"])
    onUpdateTodo(title: String, content: String): Todo
        @aws_subscribe(mutations: ["updateTodo"])
}

 

ゾルバー

createTodo/updateTodoとも、リゾルバーのリクエストテンプレート/レスポンステンプレートは同じです。

リクエストテンプレート
{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "title": { "S" : "${context.args.input.title}" }
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input)
}

 

レスポンステンプレート
$util.toJson($context.result)

 

Amplifyを使ったアプリ
index.html

Mutation/Subscriptionするためのボタンを用意しています。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Amplify Framework</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="app">
  <div>Subscribe: Create (title="foo", content="bar")
    <button id="subscribe-create">Subscribe (Create)</button>
  </div>
  <div>Subscribe: Update (title="foo", content="bar")
    <button id="subscribe-update">Subscribe (Update)</button>
  </div>

  <hr>

  <div>データ作成
    <label for="title">タイトル</label>
    <input type="text" id="title" value="foo" />
    <label for="content">内容</label>
    <input type="text" id="content" value="bar" />
  </div>

  <div>Create button
    <button id="create">Create</button>
  </div>
  <div>Update button
    <button id="update">Update</button>
  </div>


</div>
<script src="main.bundle.js"></script>
</body>
</html>

 

app.js

ボタンのclickイベントで

  • HTMLの内容でMutation
  • Mutationに対するSubscription

が実行されるようにしています。

 
なお、Subscriptionについては、 unsubscribe() を使っていないため、

  • title = "foo"
  • content = "bar"

な内容でMutaitonされると、毎回反応するようにしています。

import Amplify, { API } from 'aws-amplify';
import awsconfig from './aws-exports';
import * as mutations from "./graphql/mutations";
import * as subscriptions from "./graphql/subscriptions";


// データ作成
const createButton = document.getElementById('create');
createButton.addEventListener('click', () => {
  callMutation(mutations.createTodo, 'create');
});

// データ更新
const updateButton = document.getElementById('update');
updateButton.addEventListener('click', () => {
  callMutation(mutations.updateTodo, 'update');
});


// Mutation関数以外はすべて共通
const callMutation = (func, funcType) => {
  Amplify.configure(awsconfig);
  API.graphql(
    {
      query: func,
      authMode: 'API_KEY',
      variables: {
        input: {
          title: document.getElementById('title').value,
          content: document.getElementById('content').value,
        }
      }
    }
  ).then((data) => {
    console.log(`${funcType}: target data.`);
    console.log(data);
  })
    .catch((e) => {
      console.log(`mutation(${funcType}) error`);
      console.log(e)
    });
};


// --------------------------
// Subscriptions
// --------------------------

// CreateをSubscribe
const subscribeCreateButton = document.getElementById('subscribe-create');
subscribeCreateButton.addEventListener('click', () => {
  callSubscription(subscriptions.onCreateTodo, 'subscribe(Create)');
});


// UpdateをSubscribe
const subscribeUpdateButton = document.getElementById('subscribe-update');
subscribeUpdateButton.addEventListener('click', () => {
  callSubscription(subscriptions.onUpdateTodo, 'subscribe(Update)');
});


// Subscription関数以外は共通
const callSubscription = (func, funcType) => {
  Amplify.configure(awsconfig);

  console.log(`subscribe(${funcType}): start...`);

  const client = API.graphql(
    {
      query: func,
      authMode: 'API_KEY',
      variables: {
        title: "foo",
        content: "bar"
      }
    }
  ).subscribe({
    next: (data) => {
      console.log(`subscribed: ${funcType}`);
      console.log(data);
    },
    error: (err) => {
      console.log(err);
      console.log(`sub error (${funcType})`);
    },
    close: () => console.log('sub done')
  })
};

 

動作確認

準備:Subscription開始
  • createTodo
  • updateTodo

のMutationに対してSubscriptionを実行しておきます。

f:id:thinkAmi:20190726215322p:plain:w450

 

新規登録時

Mutation createTodo() の実行ボタンを押したしたところ、想定通り、Subscription onCreateTodo() だけが反応しました。

f:id:thinkAmi:20190726215411p:plain:w450

 
上記を展開した内容はこちら。

f:id:thinkAmi:20190726215515p:plain:w450

 

同じ内容で更新時

同じ内容のまま、再度 Mutation createTodo() の実行ボタンを押したところ、Subscription onCreateTodo() が再度反応しました。

f:id:thinkAmi:20190726215733p:plain:w450

 
これより、特に設定をしなければ、同じ内容で更新した場合もSubscriptionで通知を受け取ることができるようです。

 

参考:createTodoは、同じキーで呼び出すとエラーにしたい

上記で見た通り、Mutation createTodo はDynamoDBに存在するキーで実行してもエラーになりません。

ただ、新規登録用のMutationなど、DynamoDBに存在するキーで実行した時はエラーにしたい場合、リゾルバーのリクエストテンプレートに condition を追加して制御します。

今回は attribute_not_exists を使って制御します。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Functions

{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "title": { "S" : "${context.args.input.title}" }
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input),
## 以降を追加
  "condition": {
      "expression": "attribute_not_exists(#title)",
      "expressionNames": {
          "#title": "title"
    }
  }
}

 
また、DynamoDBの状態は以下とします。

f:id:thinkAmi:20190726220957p:plain:w300

 
この時に、

  • title = "foo"
  • content = "ham"

にて、Mutation createTodo するとエラー DynamoDB:ConditionalCheckFailedException になります。

f:id:thinkAmi:20190726221157p:plain:w450

 
ただし、

  • title = "foo"
  • content = "bar"

の場合には、エラーとはなりません。

f:id:thinkAmi:20190726221316p:plain:w450

 
もう少し考える必要がありそうなものの、今回はこれくらいにします。

 

ソースコード

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

AWS Amplify CLIで、.graphqlconfig.ymlを使って、schema.jsonやqueries.jsonなどの保存場所を変更する

Amplify CLIamplify push することで、必要に応じてAppSyncのスキーマファイル ( <project_root>/amplify/backend/api/"api_name"/schema.graphql ) を元に、Amplify Framework向けのソースコードを生成してくれます。

$ amplify push

Current Environment: dev

| Category | Resource name    | Operation | Provider plugin   |
| -------- | ---------------- | --------- | ----------------- |
| Api      | graphqlconfigApi | 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

...
✔ Downloaded the schema
✔ Generated GraphQL operations successfully and saved at src/graphql
✔ All resources are updated in the cloud

 
デフォルトでは、 src/graphql ディレクトリに作成されます。

ただ、amplify push後に、自動生成ファイルの保存先を変更しようと思ったため、その時のことをメモします。

 

環境

  • Amplify CLI 1.8.5

 

.graphqlconfig.ymlを使った修正

.graphqlconfig.yml ファイルについて、公式ドキュメントでは

When a project is configured to generate code with codegen, it stores all the configuration .graphqlconfig.yml file in the root folder of your project.

https://aws-amplify.github.io/docs/cli-toolchain/graphql#codegen

と記載されています。

ただ、どの項目を修正することで保管場所が変更になるか分からなかったため、いろいろと試してみました。

 

queries.json/mutations.json/subscriptions.jsonの移動

GraphQLに対応したファイル

を移動するには、項目 docsFilePath を修正します。

試しに、docsFilePathsrc_2nd/graphql へと変更し、 schema.graphql を修正した後、 amplify push してみました。

$ amplify push

Current Environment: dev

| Category | Resource name    | Operation | Provider plugin   |
| -------- | ---------------- | --------- | ----------------- |
| Api      | graphqlconfigApi | Update    | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully.

? Do you want to update code for your updated GraphQL API Yes
? Do you want to generate GraphQL statements (queries, mutations and subscription) based on your schema types? This will overwrite your current graphql queries, mutations and subs
criptions Yes

...
✔ Downloaded the schema
✔ Generated GraphQL operations successfully and saved at src_2nd/graphql
✔ All resources are updated in the cloud

 
すると、

の3ファイルが src_2nd/graphql の中に作成されました。
https://github.com/thinkAmi-sandbox/AWS_AppSync_Amplify-sample/commit/d4658252211362d35296110fdef8b19b507d4e08#diff-65fd0b65d8da646fd955c3e7deaee9b8

 

shema.jsonの移動

shema.json の場合は、項目 schemaPath を修正します。あとは amplify push することで、新しい場所に作成されます。
https://github.com/thinkAmi-sandbox/AWS_AppSync_Amplify-sample/commit/6efb091d1a9d2891cccefe56565c6e05a99019c4#diff-65fd0b65d8da646fd955c3e7deaee9b8

 
ちなみに、 Amplify CLIのバージョンが古い場合はschema.jsonの移動はできません。

 
もし移動したい場合は、最新のAmplify CLIにアップデートする必要があります(手元だと、1.8.5で移動可能でした)。

 

ソースコード

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

AWS AppSync + Amplifyで、Subscriptionに引数を追加し、一部のみ通知を受け取ってみた

AWS AppSyncでは、Subscriptionを使うことでMutationに対する通知を受け取ることができます。
リアルタイムデータ - AWS AppSync

また、AWS AppSyncにAWS Amplifyを組み合わせた場合でも、Amplify FrameworkのSubscriptionが使えます。
Subscription | AWS Amplify | API

 
ドキュメントを読んでいたところ、Subscriptionに引数を追加することで、一部のMutationに対してのみ通知を受け取ることもできそうでした。

そこで、AWS AppSync + Amplifyで試してみたため、その時のメモを残します。

 
目次

 

環境

 

Schemaと確認内容

今回用意したAmplifyの schema.graphql はこちらです。

type Blog
        @model(queries: {get: "getBlog"}, mutations: {create: "createBlog"},
            subscriptions: { onCreate: [
                "none",
                "titleOnly(title: String!)",
                "all(title: String!, content: String)",
            ]})
        @key(fields: ["title"]) {
    title: String!
    content: String
}

 
これをデプロイすると、AppSync Console上のSchemaは以下のような感じになります。

type Blog {
    title: String!
    content: String
}

input CreateBlogInput {
    title: String!
    content: String
}

type Mutation {
    createBlog(input: CreateBlogInput!): Blog
}

type Query {
    getBlog(title: String!): Blog
}

type Subscription {
    none: Blog
        @aws_subscribe(mutations: ["createBlog"])
    titleOnly(title: String!): Blog
        @aws_subscribe(mutations: ["createBlog"])
    all(title: String!, content: String): Blog
        @aws_subscribe(mutations: ["createBlog"])
}

 
今回は、このSchemaに対し、3つのSubscription

  • none (引数なし)
  • titleOnly (キーtitleのみの引数)
  • all (キーtitle、およびそれ以外の項目(content)も引数に含む)

を実行した後にMutation createBlog を実行することで、各Subscriptionがどのように動作するかを確認します。

 
動作確認用の環境として、以下の index.html と src/app.js を用意しました。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Amplify Framework</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="app">
  <div>フィルターなし
    <button id="none">Non Filter</button>
  </div>
  <div>タイトルだけ (title=foo)
    <button id="title_only">Title Only</button>
  </div>
  <div>すべて (title=foo, content=bar)
    <button id="all">All Filter</button>
  </div>


  <div>データ作成
    <label for="title">タイトル</label>
    <input type="text" id="title" placeholder="my title" />
    <label for="content">内容</label>
    <input type="text" id="content" placeholder="my content" />
  </div>

  <div>
    <button id="create">Create</button>
  </div>


</div>
<script src="main.bundle.js"></script>
</body>
</html>

 

app.js

subscriptionを実行する時の引数は、それぞれ以下を指定しました。

subscriptionsのメソッド 引数
none なし
titleOnly title = 'foo'
all title = 'foo' , content = 'bar'

また、一度通知を受け取ったら再度受け取らないよう、 unsubscribe() メソッドを使って通知を中止しています。
https://aws-amplify.github.io/docs/js/api#subscriptions

 

import Amplify, { API } from 'aws-amplify';
import awsconfig from './aws-exports';
import * as mutations from "./graphql/mutations";
import * as subscriptions from "./graphql/subscriptions";


// データ作成
const creteButton = document.getElementById('create');
creteButton.addEventListener('click', () => {
  Amplify.configure(awsconfig);
  API.graphql(
    {
      query: mutations.createBlog,
      authMode: 'API_KEY',
      variables: {
        input: {
          title: document.getElementById('title').value,
          content: document.getElementById('content').value,
        }
      }
    }
  ).then((data) => {
    console.log('created: target data.');
    console.log(data);
  })
    .catch((e) => {
      console.log('mutation error');
      console.log(e)
    });
});


// --------------------------
// Subscriptions
// --------------------------

// non filter
const nonFilterButton = document.getElementById('none');
nonFilterButton.addEventListener('click', () => {
  Amplify.configure(awsconfig);

  console.log('subscribe(none): start...');

  const client = API.graphql(
    {
      query: subscriptions.none,
      authMode: 'API_KEY',
    }
  ).subscribe({
    next: (data) => {
      console.log('subscribed: none');
      console.log(data);
      // 一回通知を受け取ったら、それ以降は受け取らないようにする
      client.unsubscribe();
      console.log('finish: none');
    },
    error: (err) => {
      console.log(err);
      client.unsubscribe();
      console.log('sub error (none)');
    },
    close: () => console.log('sub done')
  })
});


// title filter
const titleFilterButton = document.getElementById('title_only');
titleFilterButton.addEventListener('click', () => {
  Amplify.configure(awsconfig);

  console.log('subscribe(title): start...');

  const client = API.graphql(
    {
      query: subscriptions.titleOnly,
      authMode: 'API_KEY',
      variables: {
        title: "foo"
      }
    }
  ).subscribe({
    next: (data) => {
      console.log('subscribed: title');
      console.log(data);
      client.unsubscribe();
      console.log('finish: title');
    },
    error: (err) => {
      console.log(err);
      client.unsubscribe();
      console.log('sub error (title)');
    },
    close: () => console.log('sub done')
  })
});


const allButton = document.getElementById('all');
allButton.addEventListener('click', () => {
  Amplify.configure(awsconfig);

  console.log('subscribe(all): start...');

  const client = API.graphql(
    {
      query: subscriptions.all,
      authMode: 'API_KEY',
      variables: {
        title: "foo",
        content: "bar"
      }
    }
  ).subscribe({
    next: (data) => {
      console.log('subscribed: all');
      console.log(data);
      client.unsubscribe();
      console.log('finish: all');
    },
    error: (err) => {
      console.log(err);
      client.unsubscribe();
      console.log('sub error (all)');
    },
    close: () => console.log('sub done')
  })
});

 

動作確認

どのSubscriptionの引数にも一致しないtitleのMutationを実行

title = 'ham' , content = 'spam' のMutationを実行した時は

  • 引数なしの none()

のSubscriptionで通知を受け取りました。

f:id:thinkAmi:20190721204206p:plain:w450

 

一部の引数ありのSubscriptionに一致する、titleが "foo" のMutationを実行

title = 'foo' , content = 'spam' のMutationを実行した時は、

  • 引数なしの none()
  • 引数 title = 'foo' を設定した titleOnly()

の2つのSubscriptionで通知を受け取りました。

f:id:thinkAmi:20190721204836p:plain:w450

 

引数ありのSubscriptionすべてに一致する、titleが "foo" & contentが "bar" のMutationを実行

title = 'foo' , content = 'bar' のMutationを実行した時は、すべてのSubscriptionで通知を受け取りました。

f:id:thinkAmi:20190721205009p:plain:w450

 

まとめ

以上より、Mutationでの更新内容とSubscriptionの引数値がすべて一致した場合に、通知を受け取れるようでした。

例えば

const client = API.graphql(
  {
    query: subscriptions.all,
    authMode: 'API_KEY',
    variables: {
      title: "foo",
      content: "bar"
    }
  }
)

は、title == "foo" && content == "bar" のMutationのみ通知を受け取るようです。

 

ソースコード

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

AWS AppSync + Amplifyで、AWS_IAM認証を使って、認証されていないユーザーに対してQueryを許可してみた

AWS AppSyncを使ってGraphQL APIを公開する場合、 Default authorization mode が必須なため、何らかの方法での認証を行う必要があります。
セキュリティ - AWS AppSync

認証されていないユーザーでもAPIを使えるようにしたいと考えた場合、簡単なのはAPI_KEY認証です。

ただし、APIで共通のAPIキーを使い、またAPIキーの有効期限があるなどの問題があり、ドキュメントにも

API キーは、最大 365 日間有効に設定可能で、該当日からさらに最大 365 日、既存の有効期限を延長できます。API キーは、パブリック API の公開が安全であるユースケース、または開発目的での使用が推奨されます。

と書かれています。

 
他の認証方法のうち、OPENID_CONNECT認証とAMAZON_COGNITO_USER_POOLS認証については、何らかの形でログインが必要になります。

残るはAWS_IAM認証ですが、

  • 認証されていないユーザーがアクセスする場合、Amazon CognitoのIDプールの 認証されていないロール が適用される
  • Amazon Cognitoには、 認証されていない ID に対してアクセスを有効にする という設定がある

ことから、これを使えばよいのではと考えました。

 
そこで、AWS_IAM認証を使って、認証されていないユーザーに対してQueryを許可してみた時のメモを残します。

 
なお、記載に誤りがありましたら、ご指摘ください。

 
ちなみに、現在のAmplify CLI (1.8.2)でAppSync APIを作成する場合、AWS_IAM認証の設定を行えません。
Add AWS_IAM via Identity Pool as an authorization option when adding a GraphQL API · Issue #1386 · aws-amplify/amplify-cli

そのため、Amplify CLIで可能な限り作業しつつ、足りないことはAWSの各Consoleで設定する形にします。

 
目次

 

環境

  • @aws-amplify/cli 1.8.2
  • 使用するAmplifyモジュール

 
また、今回Queryを実行する対象のDynamoDBテーブル Todo は以下とします。

項目 備考
title String 必須
content String

 

Amplifyモジュールの追加

Amplify CLIを使って、必要なモジュールを追加していきます。

 

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 unauthaccesswithIAM
? 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

 

Authモジュールの追加

ポイントは、Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM)」を Yes とすることです。これにより、IDプールの 認証されていない ID に対してアクセスを有効にする がチェックされます。

$ amplify auth add
Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 Do you want to use the default authentication and security configuration? Manual configuration
 Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or oth
er content, Analytics, and more)
 Please provide a friendly name for your resource that will be used to label this category in the project: UnauthDynamoDBResource
 Please enter a name for your identity pool. UnauthDynamoDBRIdentityPool

 # 認証されていないIDに対してアクセスを有効にするにチェックを入れる
 Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) Yes

 Do you want to enable 3rd party authentication providers in your identity pool? No
 Please provide a name for your user pool: UnauthDynamoDBRUserPool
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Multifactor authentication (MFA) user login options: OFF
 Email based user registration/forgot password: Enabled (Requires per-user email entry at registration)
 Please specify an email verification subject: Your verification code
 Please specify an email verification message: Your verification code is {####}
 Do you want to override the default password policy for this User Pool? No
 Warning: you will not be able to edit these selections. 
 What attributes are required for signing up? (Press <space> to select, <a> to toggle all, <i> to invert selection)Email
 Specify the app's refresh token expiration period (in days): 30
 Do you want to specify the user attributes this app can read and write? No
 Do you want to enable any of the following capabilities? (Press <space> to select, <a> to toggle all, <i> to invert selection)
 Do you want to use an OAuth flow? No
? Do you want to configure Lambda Triggers for Cognito? No

 

APIモジュールの追加

今回は、Amplify CLIの実行後に、その内容を修正・追加する形をとります。

なお、前述の通り 「Choose an authorization type for the API」は

  • API_KEY認証
  • AMAZON_COGNITO_USER_POOLS認証

のどちらかしか選択できないため、ひとまず動作確認が楽な API key を選んでおきます。

$ amplify api add
? Please select from one of the below mentioned services GraphQL
? Provide API name: UnauthDynamoDBAPI

# API_KEY認証にする
? 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 Todo

 
なお、<root>/amplify/backend/api/UnauthDynamoDBAPI/build ディレクトリにある

  • stacks/Todo.json
  • resolvers/

などは、amplify push すると上書きされてしまうため、どこか別のところに保存しておくと参照できるので便利です。

 

schema.graphqlの修正

Amplify CLIで作成されたschema.graphqlでは、@model ディレクティブがあるためにQuery/Mutation/Subscriptionができてしまいます。

そのため、今回必要なものだけに修正します。Queryの他に、きちんとアクセス制限されるかを確認するためにMutationも用意します。

type Todo {
    title: String!
    content: String
}

input CreateTodoInput {
    title: String!
    content: String
}


type Query {
    getTodo(title: String!): Todo
}

type Mutation {
    createTodo(input: CreateTodoInput!): Todo
}

 

stacks/Todo.jsonの修正

こちらも今回不要なものがいくつもあるため、CloudFormationのドキュメントを参照しつつ、修正を加えます。

AmplifyにおけるCloudFormationの設定は、今のところJSONしか対応していないため、行数が多くなってしまうのが残念です。
Cloudformation template format. · Issue #1286 · aws-amplify/amplify-cli

# YAMLで書いた時のエラー
Yaml is not yet supported. Please convert the CloudFormation stack ExistsDynamoDB.yaml to json.

 

Parametersまで

必要なパラメータだけに絞ります。

なお、AppSyncAPIのIDは、 AppSyncApiId というパラメータ名で受け取れます。
How to resolve GetAttGraphQLAPIApiId in CustomResources.json · Issue #1109 · aws-amplify/amplify-cli

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Todo stack",
    "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"
        },
        "DynamoDBBillingMode": {
            "Type": "String",
            "Description": "Configure @model types to create DynamoDB tables with PAY_PER_REQUEST or PROVISIONED billing modes.",
            "Default": "PAY_PER_REQUEST",
            "AllowedValues": [
                "PAY_PER_REQUEST",
                "PROVISIONED"
            ]
        },
        "S3DeploymentBucket": {
            "Type": "String",
            "Description": "The S3 bucket containing all deployment assets for the project."
        },
        "S3DeploymentRootKey": {
            "Type": "String",
            "Description": "An S3 key relative to the S3DeploymentBucket that points to the root of the deployment directory."
        }
    },
    "Resources": {
...

 

DynamoDBテーブルの作成

まずは、今回使用するDynamoDBテーブルの作成をResourcesに記述します。

 
今回は、必須設定の

  • TableName
  • KeySchema
  • AttributeDefinitions
  • BillingMode

のみ行い、後はデフォルトのままです。

"TodoTable": {
    "Type": "AWS::DynamoDB::Table",
    "Properties": {
        "TableName": {
            "Fn::Join": [
                "-",
                [
                    "TodoUnauth",
                    {
                        "Ref": "env"
                    }
                ]
            ]
        },
        "KeySchema": [
            {
                "AttributeName": "title",
                "KeyType": "HASH"
            }
        ],
        "AttributeDefinitions": [
            {
                "AttributeName": "title",
                "AttributeType": "S"
            }
        ],
        "BillingMode": {
            "Fn::If": [
                "ShouldUsePayPerRequestBilling",
                "PAY_PER_REQUEST",
                {
                    "Ref": "AWS::NoValue"
                }
            ]
        }
    }
},

 
また、 Conditions に、 ShouldUsePayPerRequestBilling の設定をします。

    },  // Resources の閉じカッコ
    "Conditions": {
        "ShouldUsePayPerRequestBilling": {
            "Fn::Equals": [
                {
                    "Ref": "DynamoDBBillingMode"
                },
                "PAY_PER_REQUEST"
            ]
        }
    }
}

 

DynamoDBを操作するIAMロールを作成

AppSyncのDataSourceを指定する場合、 SerciceRoleArn (データソースがリソースへの接続に使用するIAMロール) が必要になるため、IAMロールを作成します。

 
ポイントとしては、AppSyncでの利用で誤った操作をされないよう、PoliciesのActionは dynamodb:GetItem だけ許可しておきます。

また、AppSyncで利用するためのAssumeRole設定を、AssumeRolePolicyDocumentに記載します。
IAMロール徹底理解 〜 AssumeRoleの正体 | DevelopersIO

"AssumeRolePolicyDocument": {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "appsync.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
},

 
全体は以下です。

"TodoIAMRole": {
    "Type": "AWS::IAM::Role",
    "Properties": {
        "RoleName": {
            "Fn::Join": [
                "-",
                [
                    "TodoTableUnauthAccessRole",
                    {
                        "Ref": "env"
                    }
                ]
            ]
        },
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "appsync.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        },
        "Policies": [
            {
                "PolicyName": "DynamoDBAccess",
                "PolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Action": [
                                "dynamodb:GetItem"
                            ],
                            "Resource": [
                                {
                                    "Fn::Sub": [
                                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}",
                                        {
                                            "tablename": {
                                                "Ref": "TodoTable"
                                            }
                                        }
                                    ]
                                },
                                {
                                    "Fn::Sub": [
                                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*",
                                        {
                                            "tablename": {
                                                "Ref": "TodoTable"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            }
        ]
    }
},

 

AppSyncのDataSourceを指定

ここまでで、DynamoDBテーブル & 操作するIAMロールができましたので、次はAppSyncまわりの設定です。

まずはDataSourceを設定します。
AWS::AppSync::DataSource - AWS CloudFormation

 
ポイントとしては ServiceRoleArn に、先ほど作成した TodoIAMRoleArn を指定することです。

"ServiceRoleArn": {
    "Fn::GetAtt": ["TodoIAMRole", "Arn"]
},

 
全体です。

"TodoDataSource": {
    "Type": "AWS::AppSync::DataSource",
    "Properties": {
        "ApiId": {"Ref": "AppSyncApiId"},
        "Name": "TodoTable",
        "Type": "AMAZON_DYNAMODB",
        "ServiceRoleArn": {
            "Fn::GetAtt": ["TodoIAMRole", "Arn"]
        },
        "DynamoDBConfig": {
            "AwsRegion": {
                "Ref": "AWS::Region"
            },
            "TableName": {"Ref": "TodoTable"}
        }
    }
},

 

ゾルバーの設定

必要なのはQueryリゾルバーだけですが、今回AppSync Consoleからも確認できるようにMutaitionリゾルバーも用意します。
AWS::AppSync::Resolver - AWS CloudFormation

 
ポイントは

  • DataSourceNameに、上記で作成したDataSourceのNameを指定
    • "Fn::GetAtt": ["TodoDataSource", "Name"]
  • FieldNameは、GraphQLのQuery/Mutationの名前と合わせる

あたりです。

"GetTodoResolver": {
    "Type": "AWS::AppSync::Resolver",
    "Properties": {
        "ApiId": {"Ref": "AppSyncApiId"},
        "DataSourceName": {
            "Fn::GetAtt": [
                "TodoDataSource",
                "Name"
            ]
        },
        "FieldName": "getTodo",
        "TypeName": "Query",
        "RequestMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Query",
                                "getTodo",
                                "req",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        },
        "ResponseMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Query",
                                "getTodo",
                                "res",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        }
    }
},

"CreateTodoResolver": {
    "Type": "AWS::AppSync::Resolver",
    "Properties": {
        "ApiId": {"Ref": "AppSyncApiId"},
        "DataSourceName": {
            "Fn::GetAtt": [
                "TodoDataSource",
                "Name"
            ]
        },
        "FieldName": "createTodo",
        "TypeName": "Mutation",
        "RequestMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Mutation",
                                "createTodo",
                                "req",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        },
        "ResponseMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Mutation",
                                "createTodo",
                                "res",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        }
    }
}

 

IDプールの「認証されていないロール」にアタッチするポリシーを作成

ここまででDynamoDBとAppSyncの設定が終わりました。

最後に、IDプールの 認証されていないロール にアタッチする管理ポリシーを作成します。このポリシーは、後で「認証されていないロール」に手動でアタッチします。Amplify CLIAWS_IAM認証に対応していないためです。

 
ポイントとしては、Statementで

を行うことです。

"UnauthAccessPolicy": {
    "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"
                                    }
                                }
                            ]
                        }
                    ]
                }
            ]
        },
        "ManagedPolicyName": {
            "Fn::Sub": [
                "MyUnauthAccessPolicy-${apiID}",
                {
                    "apiID": {
                        "Ref": "AppSyncApiId"
                    }
                }
            ]
        }
    }
},

 

リクエスト/レスポンスマッピングテンプレートの作成

ゾルバで指定した、リクエスト/レスポンスマッピングテンプレートを <root>/amplify/backend/api/UnauthDynamoDBAPI/resolvers ディレクトリに作成します。

 

Query.getTodo.req.vtl

operationをTodoIAMRoleで指定した GetItem にして、リクエストテンプレートを作成します。

{
  "version": "2017-02-28",
  "operation": "GetItem",
  "key": {
    "title": $util.dynamodb.toDynamoDBJson($ctx.args.title)
  }
}

 

Query.getTodo.res.vtl

結果をそのまま返します。

$util.toJson($context.result)

 

Mutation用のマッピングテンプレート

今回の動作確認用のMutation用マッピングテンプレートも用意します。

Mutation.createTodo.req.vtl

## [Start] Prepare DynamoDB PutItem Request. **
$util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $util.time.nowISO8601())))
$util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601())))
$util.qr($context.args.input.put("__typename", "Todo"))
{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "title": { "S" : "${context.args.input.title}" }
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input),
  "condition": {
      "expression": "attribute_not_exists(#title)",
      "expressionNames": {
          "#title": "title"
    }
  }
}
## [End] Prepare DynamoDB PutItem Request. **

 
Mutation.createTodo.res.vtl

$util.toJson($context.result)

 

pushして動作確認

ここまでで設定ができたため、

$ amplify push

します。

その後、AppSync ConsoleのQueriesで、MutationとQueryが成功することを確認します。

 

Mutation
mutation CreateTodo($params: CreateTodoInput!) {
  createTodo(input: $params) {
    title
    content
  }
}

 
QUERY_VALUESです。

{
  "params": {
    "title": "Hello, title!",
    "content": "Hello, content!"
  }
}

 
実行すると以下が表示されます。

{
  "data": {
    "createTodo": {
      "title": "Hello, title!",
      "content": "Hello, content!"
    }
  }
}

 

Query

Mutationと同様です。

query GetTodo($title: String!) {
  getTodo(title: $title) {
    title
    content
  }
}

 
QUERY_VALUESです。

{
  "params": {
    "title": "Hello, title!",
    "content": "Hello, content!"
  },
  "title": "Hello, title!"
}

 
結果です。

{
  "data": {
    "createTodo": {
      "title": "Hello, title!",
      "content": "Hello, content!"
    }
  }
}

 

AWS_IAM認証化

ここまではAPY_KEY認証でしたので、AWS_IAM認証へと切り替えます。

 

AppSync Consoleでの認証切り替え

Settings にある Default authorization mode を、 AWS Identity and Access Management (IAM) へと変更し、 Save をクリックします。

 

IAMロールで、認証されていないロールにポリシーをアタッチ

まずは、CognitoのIDプールにて、編集画面から 認証されていないロール のロール名を確認します(今回の場合、 unauthaccess-dev-20190720202229-unauthRole )。

f:id:thinkAmi:20190721115435p:plain:w300

 
次に、IAMのロールで確認したロール名を選択し、stacks/Todo.jsonUnauthAccessPolicy セクションで作成したポリシー (MyUnauthAccessPolicy-<AppSync API ID>)をアタッチします。

f:id:thinkAmi:20190721115618p:plain:w200

 

動作確認

Amplify Framework JavaScriptを使って、ボタンを押したらQuery/Mutationを実行する動作確認アプリを作成します。

以下ではindex.htmlとsrc/app.jsのみ掲載します。

  • package.json
  • webpack.config.js

は、Amplify公式チュートリアルを流用したものを用意します。
Amplify JavaScript - Getting Started

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Amplify Framework</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="app">
    <button id="query">Run Query</button>
</div>
<div>
    <input type="text" id="title" placeholder="title">
    <button id="mutation">Run Mutation</button>
</div>
<script src="main.bundle.js"></script>
</body>
</html>

 

src/app.js
import Amplify, {API} from 'aws-amplify';
import awsconfig from './aws-exports';
import * as queries from "./graphql/queries";
import * as mutations from "./graphql/mutations";


const queryButton = document.getElementById('query');
queryButton.addEventListener('click', () => {
  console.log('Run Query!');
  Amplify.configure(awsconfig);
  API.graphql(
    {
      query: queries.getTodo,
      authMode: 'AWS_IAM',
      variables: {
        title: "Hello, title!"
      }
    }
  ).then((data) => {
    console.log(data);
  })
    .catch((e) => {
      console.log('error!');
      console.log(e)
    });
});


const mutationButton = document.getElementById('mutation');
mutationButton.addEventListener('click', () => {
  console.log('Run Mutation!');

  const title = document.getElementById('title').value;
  Amplify.configure(awsconfig);
  API.graphql(
    {
      query: mutations.createTodo,
      authMode: 'AWS_IAM',
      variables: {
        input: {
          title: title,
          content: "hello"
        }
      }
    }
  ).then((data) => {
    console.log(data);
  })
    .catch((e) => {
      console.log('error!');
      console.log(e)
    });
});

 

結果

$ npm start してアプリを起動後、動作を確認してみます。

すると、

  • Queryは成功
  • MutationはHTTP 401 エラー

となり、認証されていないユーザーに対してQueryのみ許可できました。

f:id:thinkAmi:20190721111936p:plain:w450

 

ソースコード

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

AWS Amplify CLIの amplify auth push の挙動について

AWS Amplify CLI のREADMEを見ると、

amplify auth push

Provisions only Auth cloud resources with the latest local developments.

https://github.com/aws-amplify/amplify-cli/tree/master/packages/amplify-category-auth

という記述がありました。

気になったので試してみたところ、思っていた挙動と違っていたのでメモを残します。

なお、手順などで勘違いしている部分があれば、ご指摘いただけるとありがたいです。

 
目次

 

環境

 

勘違いしていたこと

手元の環境で

のモジュールがあった時に amplify auth push をすると、Authモジュールだけがpushされると思っていました。

ただ、実際には、API・Authの両方がpushされました。

 

新規作成した時の流れ

init

特に変わりない、一般的なinitです。

$ amplify init

Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project auth_before_api
? 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

 

Authモジュールの追加

APIを作る前に、Authモジュールを追加しました。

今回はデフォルト設定にしました。

$ amplify auth add
Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.

 

APIモジュールの追加

次にAPIモジュールを追加します。

$ amplify api add
? Please select from one of the below mentioned services GraphQL
? Provide API name: authbeforeapi
? 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
Creating a base schema for you...

GraphQL schema compiled successfully.

 

状態確認

両方ともpushされていません。

$ amplify status

Current Environment: dev

| Category | Resource name         | Operation | Provider plugin   |
| -------- | --------------------- | --------- | ----------------- |
| Auth     | authbeforeapibf6779e5 | Create    | awscloudformation |
| Api      | authbeforeapi         | Create    | awscloudformation |

 

amplify auth push

Authモジュールだけpushするつもりでした。

$ amplify auth push

Current Environment: dev

| Category | Resource name         | Operation | Provider plugin   |
| -------- | --------------------- | --------- | ----------------- |
| Auth     | authbeforeapibf6779e5 | 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

 
しばらく待つと、想定していなかった

  • APIのエンドポイント
  • API Key

が作成されました。

✔ All resources are updated in the cloud

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

 
状況を確認しても、両方ともpushされていました。

$ amplify status

Current Environment: dev

| Category | Resource name         | Operation | Provider plugin   |
| -------- | --------------------- | --------- | ----------------- |
| Auth     | authbeforeapibf6779e5 | No Change | awscloudformation |
| Api      | authbeforeapi         | No Change | awscloudformation |

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

 

両方とも更新して、再度 amplify auth push した時の流れ

Authモジュールを更新します。

$ amplify auth update
Please note that certain attributes may not be overwritten if you choose to use defaults settings.

You have configured resources that might depend on this Cognito resource.  Updating this Cognito resource could have unintended side effects.

Using service: Cognito, provided by: awscloudformation
 What do you want to do? Walkthrough all the auth configurations
 Select the authentication/authorization services that you want to use: User Sign-Up & Sign-In only (Best used with a cloud API only)
 Multifactor authentication (MFA) user login options: OFF
 Email based user registration/forgot password: Enabled (Requires per-user email entry at registration)
 Please specify an email verification subject: Your verification code
 Please specify an email verification message: Your verification code is {####}
 Do you want to override the default password policy for this User Pool? No
 Specify the app's refresh token expiration period (in days): 30
 Do you want to specify the user attributes this app can read and write? No
 Do you want to enable any of the following capabilities? (Press <space> to select, <a> to toggle all, <i> to invert selection)
 Do you want to use an OAuth flow? No
? Do you want to configure Lambda Triggers for Cognito? No
Successfully updated resource authbeforeapibf6779e5 locally
...

 
続いて、APIもupdateします。

$ amplify api update
? Please select from one of the below mentioned services GraphQL
? Choose an authorization type for the API Amazon Cognito User Pool

GraphQL schema compiled successfully.

 
状態を確認します。

$ amplify status

Current Environment: dev

| Category | Resource name         | Operation | Provider plugin   |
| -------- | --------------------- | --------- | ----------------- |
| Auth     | authbeforeapibf6779e5 | Update    | awscloudformation |
| Api      | authbeforeapi         | Update    | awscloudformation |

 
Authモジュールをpushしてみます。

$ amplify auth push

Current Environment: dev

| Category | Resource name         | Operation | Provider plugin   |
| -------- | --------------------- | --------- | ----------------- |
| Auth     | authbeforeapibf6779e5 | Update    | awscloudformation |
? Are you sure you want to continue? Yes

 
APIとAuthの両方が更新されました。

$ amplify status

Current Environment: dev

| Category | Resource name         | Operation | Provider plugin   |
| -------- | --------------------- | --------- | ----------------- |
| Auth     | authbeforeapibf6779e5 | No Change | awscloudformation |
| Api      | authbeforeapi         | No Change | awscloudformation |

AWS Amplifyで、既存のAmazon Cognitoを使う方法を探してみた

AWS Amplifyでは、Authentication moduleを使うことで、簡単にAmazon Cognitoの新しい環境が作れます。
Authentication

 
ただ、Amplify CLIを使って既存のAmazon Cognito環境を利用する方法が見当たらなかったので、調べた時のメモを残します。

 
目次

 

環境

 

結論

これらのissueがあるように、現時点のAmplify CLIでは既存のCognitoを指定できないようです。

 
そのため、既存のCognitoを使いたい場合は、 $ amplify auth add で作るのではなく、Amplify.configure() の引数に対象のCognito情報を入れる必要があります。

また、$ amplify auth add が必要なAWS Amplify Storage moduleについても、 $ amplify storage add で環境を作るのではなく、 Amplify.configure() の引数に指定する形となります。

 
つまり、現時点で既存のCognitoを使いたい場合は

  • $ amplify auth add
  • $ amplify storage add

ができないようです。

 
以降、$ amplify auth add で既存のCognitoを使えないかを試してみた時のログです。

 

試してみた

実行前の既存のユーザープールは以下です。これを使いたいとします。

$ aws cognito-idp list-user-pools --max-results 20
{
    "UserPools": [
        {
            "Id": "us-east-1_ZTS1WF17W",
            "Name": "directiveauthuserpool-dev",
            "LambdaConfig": {},
            "LastModifiedDate": 1562661761.134,
            "CreationDate": 1562661761.134
        }
    ]
}

 
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 existingawsresources
? 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

 
続いて AmplifyのAuthenticationモジュールを追加します。既存のものが使えるかなと思い名前を合わせてみました。

$ amplify auth add
Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 Do you want to use the default authentication and security configuration? Manual configuration
 Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or oth
er content, Analytics, and more)
 Please provide a friendly name for your resource that will be used to label this category in the project: existingawsresourcesb5bd8fa8b5bd8fa8

# 名前が既存と同じになるよう指定する
 Please enter a name for your identity pool. directiveauthidpool
 Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) No
 Do you want to enable 3rd party authentication providers in your identity pool? No

# こちらも同様
 Please provide a name for your user pool: directiveauthuserpool
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Multifactor authentication (MFA) user login options: OFF
 Email based user registration/forgot password: Enabled (Requires per-user email entry at registration)
 Please specify an email verification subject: Your verification code
 Please specify an email verification message: Your verification code is {####}
 Do you want to override the default password policy for this User Pool? No
 Warning: you will not be able to edit these selections. 
 What attributes are required for signing up? (Press <space> to select, <a> to toggle all, <i> to invert selection)Email
 Specify the app's refresh token expiration period (in days): 30
 Do you want to specify the user attributes this app can read and write? No
 Do you want to enable any of the following capabilities? (Press <space> to select, <a> to toggle all, <i> to invert selection)
 Do you want to use an OAuth flow? No
? Do you want to configure Lambda Triggers for Cognito? No

 
この時にできあがったファイルはこちら

 
続いて、既存のものに合わせるべく、不要なところを修正・削除します。

その後、 amplify auth update

$ amplify auth update
Please note that certain attributes may not be overwritten if you choose to use defaults settings.
Using service: Cognito, provided by: awscloudformation
 What do you want to do? Apply default configuration without Social Provider (Federation)
Successfully updated resource existingawsresourcesb5bd8fa8b5bd8fa8 locally

 
update後の変更点はこちら。

 
amplify push すると成功。

$ amplify push

Current Environment: dev

| Category | Resource name                        | Operation | Provider plugin   |
| -------- | ------------------------------------ | --------- | ----------------- |
| Auth     | existingawsresourcesb5bd8fa8b5bd8fa8 | Create    | awscloudformation |
? Are you sure you want to continue? Yes
...
✔ All resources are updated in the cloud

 
しかし、AWS CLIで見ると、同じ名前で別のユーザープールができあがっていました。残念。
list-user-pools — AWS CLI 1.16.197 Command Reference

$ aws cognito-idp list-user-pools --max-results 20
{
    "UserPools": [
        {
            "Id": "us-east-1_MoYYcoq4F",
            "Name": "directiveauthuserpool-dev",
            "LambdaConfig": {},
            "LastModifiedDate": 1563375883.473,
            "CreationDate": 1563375883.473
        },
        {
            "Id": "us-east-1_ZTS1WF17W",
            "Name": "directiveauthuserpool-dev",
            "LambdaConfig": {},
            "LastModifiedDate": 1562661761.134,
            "CreationDate": 1562661761.134
        }
    ]
}

 

対応

こんな感じのJavaScriptを書いてみました。

use_aws_exportsがtrueの時は aws-exports.js を使い、falseの時は、既存のCognito情報を使っています。

const signInUsingCognito = async (use_aws_exports) => {
  if (use_aws_exports) {
    Amplify.configure(awsconfig);
  }
  else {
    Amplify.configure({
      Auth: {
        // TODO replace for your environment
        identityPoolId: 'xxx',
        region: 'us-east-1',
        userPoolId: 'us-east-1_ZTS1WF17W',
        userPoolWebClientId: 'xxx',
      }
    });
  }

  const username = document.getElementById('user').value;

  await Auth.signIn(username, '11111111')
    .then( user => {
      showUsername(user);
      document.getElementById('message').innerText = ''
    })
    .catch(e => {
      console.log(Object.keys(e));
      console.log(e['message']);
      document.getElementById('message').innerText = e['message'];
    })
};

 
結果です。

aws-exports.jsを使った場合は、Amplify Authenticationモジュールで新規作成したため、ユーザープールにユーザーが存在していないことから、エラーとなりました。

(3) ["code", "name", "message"]0: "code"1: "name"2: "message"length: 3__proto__: Array(0)
app.js:59 User does not exist.

 
一方、既存のCognitoにはユーザーがいるため、サインインができました。

foo
CognitoUser {username: "foo", pool: CognitoUserPool, Session: null, client: Client, signInUserSession: CognitoUserSession, …}

 
今回はCognitoだけでしたが、もし他のリソースを指定したい場合は、AmplifyのGetting Started の下部にある Configuration Parameters for existing AWS resources に記載があります。
Getting Started - Amplify JavaScript

 

ソースコード

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

AWS AppSync + Amplify JavaScript + CustomResourcesで、既存のDynamoDBなどをDatasourceとしたリゾルバーを作成する

AWS Amplify JavaScriptを使ってAWS AppSync APIを作成する場合、 amplify add api した直後はDynamoDBのテーブルが新規作成されます。

既存のDynamoDBを使いたい場合は、 amplify pushAPIをデプロイ後に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の設定ファイルはJSONYAMLの両方をサポートしていますが、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として追加します。

なお、 RefAWS::AppSync::DataSourceDynamoDBConfig の詳細については、以下の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": {
      // 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のドキュメントは以下です。

{
  "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