AWS再入門ブログリレーAppSync編

クラスメソッドのAWS再入門ブログリレー 19日目のエントリ『AWS AppSync』編です。
2019.07.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、コンサルティング部のキムです!

当エントリはDevelopers.IOで弊社コンサルティング部による『AWS 再入門ブログリレー 2019』の19日目のエントリです。

昨日は中川の「AWS再入門 Amazon Elastic File System編」でした。

このブログリレーの企画は、普段AWSサービスについて最新のネタ・深い/細かいテーマを主に書き連ねてきたメンバーの手によって、 今一度初心に返って、基本的な部分を見つめ直してみよう、解説してみようというコンセプトが含まれています。

AWSをこれから学ぼう!という方にとっては文字通りの入門記事として、またすでにAWSを活用されている方にとってもAWSサービスの再発見や2019年のサービスアップデートのキャッチアップの場となればと考えておりますので、ぜひ最後までお付合い頂ければ幸いです。

では、さっそくいってみましょう。19日目のテーマは『AWS AppSync編』です。

目次

始める前に

AppSyncは割と最近出たサービスにてアップデートのスピード感も凄いサービスです。なので、まだ構築事例やベストプラクティスは少ないです。 それであくまでに本記事はAppSyncの使い方のベストプラクティスなどではなく、個人的な意見が多く含まれています。 結構長い記事になりましたので、弊社の菊池[AWS AppSync] チュートリアル:複数データソースを組み合わせた GraphQL API の作成も読んでいただくと、より早く簡単理解できると思います。

AppSyncとは

appsync-architecture

AppSyncはAWSの管理系GraphQLサービスにて、ApolloやPrismaと比べてAWSからインフラやサーバーまで提供してくれるサービスになります。その場合私たちは何をすべきかというと以下の三つぐらいですね。

  • GraphQLスキーマの定義
  • リゾルバーの作成
  • データソースおよびIAMロールの管理

つまり、サーバーレスの形でGraphQLのバックエンドを実装できるサービスです。AppSyncを用いなくてもAWS Lambdaなどを活かせてGraphQLのバックエンドを構築するのができましたが、この場合だとLambdaのメモリーサイズ、コールドスタート、データソースとの通信、認証ユーザーのトークン等直接開発しなければならないことがもっと多かったです。一方、AppSyncを用いる場合にはGraphQLのスキーマを作成してフィールドごとのリゾルバーを作成するだけでGraphQLのエンドポイントが生成されます。何よりも本当に便利なのはリゾルバーをVTLというテンプレート言語で作成することです。VTLに初めて接する方には少し難解に見えるかも知れませんが、VTLでリゾルバーを作成するとほとんどの作業がコピー・アンド・ペーストだけでめっちゃ便利です!(しかし、デバッグは難しいです。。。)

VTLで作成されたリゾルバーはLambdaで連携してカスタマイジングもできるんですが、それ以外にもDynamoDB、RDS、Amazon ES (Elasticsearch Service)、Http Endpoint等データソースに直接繋がるためデータハンドルが簡単です。

AppSyncを使うと便利機能がたくさんあって、バックエンドAPIを開発する時凄いスピードで開発できるところが長所です。もちろんFirebaseを用いても簡単に開発が可能ですが、Firebaseに比べてAppSyncの長所は完全なGraphQLサービスとしての優れた自由度と柔軟さにあります。

AppSync Sample Project

AppSyncは四つのサンプルプロジェクトを提供しています。本記事を読んで本気でAppSyncをいじってみようと考える方がいらっしゃったら、このサンプルを必ず分析するのを推奨します。

appsync-sample-projects

AppSyncの機能

AppSyncコンソルに新たなAPIを作ると下のような画面が見えます。下の画面は後本記事で説明するserverless frameworkを使ってAppSync APIを生成した後のキャプチャ画面になります。

appsync-first-page

左側に5つのメニューがあります。これからはメニューごとについて説明させていただきます!

スキーマ(Schema)

appsync-schema-page

まずはスキーマ画面から始めます。スキーマ画面で提供している機能はGraphQLスキーマの作成・保存及びフィールドごとに繋がるリゾルバーの生成・照会・修正・削除になります。 言い直したらスキーマとリゾルバーの設定ですね!

右側の上から見るとCreate Resourcesというボタンがあります。このボタンを押すとスキーマで使うデータモデルのタイプ定義、DynamoDBの生成、定義されたデータタイプに関する基本的なクエリ、ミューテーション、サブスクリプションなどの定義、更にはリゾルバーまでに自動生成してくれます!

これはCreate Resourcesボタンを押した時の画面です。 appsync-create-resources-1

Create Resourcesボタンを押して、スクロールするとこの項目も見えます。(DynamoDBのテーブル生成はもちろん、インデックスも一緒に生成できます) appsync-create-resources-2

リソースを生成した時の画面です(この時点でDynamoDBの生成も終わった状態です) appsync-create-resources-3

リゾルバー側から見える項目の中でMutation.createMyCustomTypeをクリックして自動生成されたリゾルバーを見てみましょう。 appsync-create-resources-4

このようにAppSyncを始めてる方のためにリゾルバーやスキーマな、データソースどが自動的に設定されます。このように自動的に生成されたスキーマが動作するかどうかをテストするためには、クエリメニューに移動して確認することができますが、クエリメニューに関しては少しあとで説明します。

ここでちょっとだけ指摘したい部分があります。 AppSyncコンソールでこんなに強力な機能を提供するにもかかわらず、本番環境ではこのAppSyncコンソールを使用して実装することは推奨されません。 スキーマやリゾルバーの管理も難しいし、複数人での共同作業にも不都合があるし、いろいろ不便な点が多いからです。

このためAWSからAmplifyというCLI+SDKを提供されています。AmplifyはAppSync APIの開発する以外にもCognitoユーザーとの連携やS3バケットのファイルアップロードなどクライアント側の開発が楽になるツールです。GraphQLのスキーマを作成してAppSyncにアップロードされるスキーマやリゾルバーを自動生成したり有用な機能がたくさんあります。しかし、まだ細かい設定などは難しいところがあって、主に大学生達がアプリ開発プロジェクトを簡単にするためによく使っているそうです。

それでは実際に実務で活用できるツールは何かというと、私はserverless-appsync-pluginをオススメします。 全ての設定を一つのyamlファイルだけで出来るのでとても楽です。IaCは当然基本的にCloudFormationを使っても可能ですが、AppSyncだけではCloudFormationテンプレートを直接作成するのは推奨しません。人生が辛くなるはずです。(笑)

AppSyncの概念説明が終わったらserverless-appsync-pluginを使って簡単なAPIを実装します。その前に一旦スキーマのスカラータイプに進めます。

Scalar Types

皆さんのご存知の通りGraphQLは自動的にtype checkをやっています。例えばリクエストやレスポンスでやり取りするデータごとがIntタイプかStringタイプかを開発者がもう検知しなくていいってことです。GraphQLで定義してる一般的なスカラータイプは下のようです。

  • ID
  • String
  • Int
  • Float
  • Double

AppSyncはこれに追加して下のスカラータイプを提供しています。これらを活かせてより楽に開発できます。

  • AWSDate
  • AWSTime
  • AWSDateTime
  • AWSTimestamp
  • AWSEmail
  • AWSJSON
  • AWSURL
  • AWSPhone
  • AWSIPAddress

スカラータイプについてのより詳細な説明はAWSドキュメントをご参照ください。

下のコードはスカラータイプを活用した例です。

type User {
  id: ID!
  name: String!
  phone: AWSPhone!
  email: AWSEmail!
  myPageUrl: AWSURL!
  createdAt: AWSDateTime!
}

Resolver

スキーマメニューからCreate Resourcesを通じて自動生成されたリゾルバーの画面からもご覧になったと思いますが、リゾルバーはVTLというJAVAベースのテンプレート言語にて作成することになっています。

appsync-unit-resolver-concept

このVTLファイルはマッピングテンプレート(Mapping Template)と呼ばれます。一つのリクエスト用マッピングテンプレートと一つのレスポンス用マッピングテンプレートで一つのリゾルバーを成すことになります。そのリゾルバーのマッピングテンプレートをマッピングテンプレートで呼ぶ理由は、本当にデータをマッピングするだけでいいからです。データソースに接続してコネクションを管理したりする必要なくデータをマッピングだけすれば後はAppSync側から処理してくれます。

AppSyncで使わられるリゾルバーのタイプは二つあります。

appsync-resolver-types

ユニットリゾルバー(Unit Resolver)

名前の通りです、で一発で説明が終わってしまうリゾルバーです。一つのデータソース(DynamoDB、RDS等)と連携させて処理するリゾルバーになります。

パイプラインリゾルバー(Pipeline Resolver)

実際にGraphQLのAPIを実装してみるとユニットリゾルバーで解決できない複雑なロジックがたくさんあります。例えば、Friendshipテーブルから二人が友達に登録された場合のみロジックを実行するとか、ポイントを使って決済する場合Pointテーブルにユーザのポイントが十分な場合のみ決済ロジックを処理するとかのいろんな状況があるかと思います。この時に有用な機能がパイプラインリゾルバーになります。

appsync-pipeline-resolver

パイプラインリゾルバーは元々はユニットリゾルバーなはずの個別のリゾルバーをFunctionとして登録して使用します。このFunctionというのは他のリゾルバーからも共有されて利用出来ますので共通的なロジックをFunctionとして作っておいてから様々なパイプラインリゾルバーから使うパターンも可能です。

本記事では扱いませんが、serverless-appsync-pluginを通じても簡単にパイプラインリゾルバーを作成できます。

Data Sources

次はデータソースについて説明させていただきます。あまり内容はありませんが、とりあえずData Sourcesメニューをクリックしたらこのような画面が見えます。

appsync-datasource-page

これは私が予め登録しておいたデータソースです。Create Data Sourceボタンを押すとすぐ作られます。 appsync-datasource-create-datasource

現時点でAppSyncでデータソースにて登録可能なタイプは以下の6種類になります。 appsync-datasource-types

Functions

それぞれのPipeline resolverはBefore Mapping Template, Functions, After Mapping Templateによって行われます。 このとき,真ん中の部分に位置したのがFunctionになります。 一つ一つのFunctionは基本的にユニットリゾルバーとほとんど似ています. 本記事はPipeline resolverに対して詳しく扱っていないのでFunctionsについて簡単に見るだけで行きます。

まず、Functionsメニューをクリックすると下のような画面が見えます。今は一つのFunctionを登録しておいた状態です。 appsync-functions-page

Create functionボタンをクリックするとデータソース及びFunctionの名前、リクエスト用マッピングテンプレートやレスポンス用マッピングテンプレートの設定が可能です。 appsync-create-function-1 appsync-create-function-2

ユニットリゾルバーからパイプラインリゾルバーに変えると、生成されたFunctionをパイプラインリゾルバーのFunctionとして登録できます。 appsync-convert-unit-to-pipeline

赤のボタンを押すとユニットリゾルバ−からパイプラインリゾルバーに変更されます。

appsync-pipeline-resolver-page

先ほど作ったFunctionを追加した場合このようになります。

appsync-pipeline-resolver-function-added

パイプラインリゾルバー及びFunctionに関してより詳細な内容はAWS ドキュメントをご参照できればと思います。

クエリ(Queries)

クエリメニューはGraphQLのPlaygroundみたいな機能を提供します。Cognitoなどを使ってログインした状態でクエリを実行してみたり、ログアウトした状態でクエリを実行してみたりするのができます。

appsync-query-page

左側がリクエスト、右側はレスポンスです。

設定(Settings)

SettingsメニューはAPI情報(エンドポイントURL、API ID、API Key)や認証方法(API Key、Cognito、IAM、OpenID)やロギングなどを設定するのが可能です。 Settingsに関してはあまり直観的すぎてこれ以上説明が要らないと思います。(笑)

それでは、これからは簡単なチュートリアルを通じてserverless-appsync-pluginを活用してAppSyncのプロジェクトをどうやって作るのかを紹介します。

AppSync Tutorial with Serverless Framework

ここではServerless Frameworkserverless-appsync-pluginを活かせて簡単なAppSyncプロジェクトを構築してみます。 本チュートリアルで使うデータモデルは以下の三つです。

type User
    id: ID!
    name: String!
    email: AWSEmail!
    posts: [Post!]!
    createdAt: AWSDateTime!
}

type Post {
    id: ID!
    user: User!
    title: String!
    content: String!
    likes: [Like!]!
    createdAt: AWSDateTime!
}
    
type Like {
    id: ID!
    user: User!
    post: Post!
    createdAt: AWSDateTime!
}

テストのシナリオは以下の通りです。

  • 新規ユーザーの生成
  • 新規ポストの作成
  • ポストに『いいね』を付ける
  • 新規ポストが生成されたらサブスクリプションを通じて伝達
  • 特定のポストに『いいね』が付いたらサブスクリプションを通じて伝達

本チュートリアルのコードはGithub Repositoryでご確認ください。

プロジェクトの初期セッティング

本チュートリアルを実行するためには以下のツールのインストールが必要になります。

私が使っているツールのバージョンはこれです。 yarn-and-serverless-version

プロジェクトのディレクトリを構成します。 私はappsync-tutorialという名前のディレクトリから作業します。

mkdir appsync-tutorial
cd appsync-tutorial

touch serverless.yml
mkdir schema
mkdir resolvers

とりあえずserverless-appsync-pluginを予めインストールしておきましょう

yarn add serverless-appsync-plugin

最後にDone in 10.12sみたいに出力されたら完了です。

Schema

今回はスキーマを作成します。 serverless-appsync-pluginにはSchema stitchingという機能を提供していますので、スキーマをModuleごとに分けて作成できます。

cd schema
touch user.graphql
touch post.graphql
touch like.graphql

三つのスキーマが用意されたら以下のようにスキーマを作成しましょう。

user.graphql

type User {
    userId: ID!
    name: String!
    email: AWSEmail!
    posts: [Post!]!
    createdAt: AWSDateTime!
}

input CreateInputUser {
    name: String!
    email: AWSEmail!
}

type Query {
    listUser: [User!]!
    getUser(userId: ID!): User
}

type Mutation {
    createUser(input: CreateInputUser!): User
}

ユーザータイプは名前とEメールをインプットとして生成し、idやcreatedAtフィールドはリゾルバーにて自動生成します。 またpostsフィールドの場合は他のフィールドとは異なり、UserテーブルではなくPostテーブルからデータをもたらします。この部分に関してはserverless.ymlファイルを作成する際に改めて説明します。

post.graphql

type Post {
    postId: ID!
    user: User!
    title: String!
    content: String!
    likes: [Like!]!
    createdAt: AWSDateTime!
}

input CreatePostInput {
    userId: ID!
    title: String!
    content: String!
}

type Query {
    listPost: [Post!]!
    listPostByUser(userId: ID!): [Post!]!
    getPost(postId: ID!): Post
}

type Mutation {
    createPost(input: CreatePostInput!): Post
}

type Subscription {
    onNewPostCreated: Post @aws_subscribe(mutations: ["createPost"])
}

ポストタイプはユーザーと比べてサブスクリプション(Subscription)が登場しました。サブスクリプション(Subscription)はAppSyncのミューテーション(Mutation)が実行された際、クライアント側にそのデータを伝達してくれます。本チュートリアルはcreatePostが実行されたらonNewPostCreatedというサブスクリプションを登録したクライアントにリアルタイムでそのデータを伝達するシナリオを検証します。AppSyncのサブスクリプションはWebSocket上でMQTTプロトコルで行われっています。より詳細な内容はAWSドキュメントをご覧ください。

like.graphql

type Like {
    likeId: ID!
    userId: ID!
    postId: ID!
    createdAt: AWSDateTime!
}

type Query {
    listLike(postId: ID!): [Like!]!
}

type Mutation {
    likePost(userId: ID!, postId: ID!): Like
    cancelLikePost(likeId: ID!): Like
}

type Subscription {
    onPostLiked(postId: ID!): Like @aws_subscribe(mutations: ["likePost"])
    onPostLikeCanceled(postId: ID!): Like @aws_subscribe(mutations: ["cancelLikePost"])
}

最後にライクタイプです。ライクタイプはサブスクリプションがポストタイプと異なり、特定なポストIDについてのサブスクリプションのみが得られます。 それ以外にもページネーションなど複雑なクエリのためのフィルターなどの設定も可能になりますが、本チュートリアルはあくまでAppSyncをより直観的に理解できるようにするためそこまでの設定はしていません。 AppSyncが提供しているサンプルプロジェクトではこのような様々なパターンを学べますのでご参照くださいませ。

Resolvers (Mapping Template)

次はresolversです。 かなり多くのリゾルバーファイルを作らなければいけません。見方によっては一番面倒な作業であるかもしれませんが、実はこれはAmplifyを活用すれば必要なリゾルバーファイルやコードが自動的に生成されてとても便利です。Amplifyは必要に応じて適切に活用しましょう。しかし本チュートリアルではシンプルに説明するため一つずつ全部作ってみます。この時、ファイル名が結構重要になります。serverless-appsync-pluginからdefaultに認識するファイル名がありますので、できれば以下のようなルールを守りましょう。

{type}.{field}.request.vtl {type}.{field}.respose.vtl

スカラータイプやサブスクリプションに関してのリゾルバーは要らないです。

cd ../resolvers

# User
touch User.posts.response.vtl
touch User.posts.request.vtl
touch Query.getUser.request.vtl
touch Query.getUser.response.vtl
touch Query.listUser.request.vtl
touch Query.listUser.response.vtl
touch Mutation.createUser.request.vtl
touch Mutation.createUser.response.vtl

# Post
touch Post.user.request.vtl
touch Post.user.response.vtl
touch Post.likes.request.vtl
touch Post.likes.response.vtl
touch Query.getPost.request.vtl
touch Query.getPost.response.vtl
touch Query.listPost.request.vtl
touch Query.listPost.response.vtl
touch Query.listPostByUser.request.vtl
touch Query.listPostByUser.response.vtl
touch Mutation.createPost.request.vtl
touch Mutation.createPost.response.vtl

# Like
touch Query.listLike.request.vtl
touch Query.listLike.response.vtl
touch Mutation.likePost.request.vtl
touch Mutation.likePost.response.vtl
touch Mutation.cancelLikePost.request.vtl
touch Mutation.cancelLikePost.response.vtl

かなり多くのファイルができてしまいました。これらをいつ全部作成するのって思っている方もいらっしゃるかもしれませんが、実はこのリゾルバー、ほぼコピー・アンド・ペーストです。完成されたリゾルバーはGithub Repositoryをご確認ください。

ここではこの中でいくつかだけ見てみます。

Mutation.createUser.request.vtl

$util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $util.time.nowISO8601())))

{
    "version": "2017-02-28",
    "operation": "PutItem",
    "key": {
        "userId":   $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.userId, $util.autoId()))
    },
    "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input),
    "condition": {
        "expression": "attribute_not_exists(#userId)",
        "expressionNames": {
            "#userId": "userId"
        }
    }
}

Mutation.createUser.response.vtl

$util.toJson($context.result)

VTLの文法を初めてご覧になった方だとコレなに?って感じるかもしれませんが、AppSyncで使うVTLの説明に関しましてはAWSドキュメント上でやさしく説明されており本当に分かりやすいです。

ここには文法抜きでコードの流れの観点で説明させていただきます。

最初はそれぞれのリゾルバーまたはマッピングテンプレートを登録する時どのデータソースと繋がるかを選択します。本チュートリアルで使っているserverless-appsync-pluginを利用する場合にはserverless.ymlファイルに作成します。(少し後で説明させていただきます)

なのでマッピングテンプレートを読む際はデータソースがもう決まっている状態になります。Mutation.createUser.request.vtl及びMutation.createUser.response.vtlではDynamoDBのUserテーブルと連携されています。

"version"はいつでもこのままで"2017-02-28"となります。 "operation"はDynamoDBだとcreateUserに該当するoperationが"PutItem"になります。 以降のフィールドに関しましてはどんなoperationに該当するかによって異なりますが、ここのDynamoDBのPutItemはKeyとAttributeを要求するため上記のように作成します。

$util、$context(または $ctx)、$arguments (または $args) 等はAppSyncからリゾルバー作成のため提供しているオブジェクトにてContext Reference及びUtility Referenceページから詳細な内容を確認できます。

User.posts.request.vtl

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "index" : "userId-index",
    "query" : {
        "expression": "userId = :userId",
        "expressionValues" : {
            ":userId" : $util.dynamodb.toDynamoDBJson($ctx.source.userId)
        }
    }
}

User.posts.response.vtl

$util.toJson($context.result.items)

Userタイプにはpostsというフィールドがありました。このフィールドだけはUserテーブルではなくPostテーブルから持します。 User.posts.request.vtlを見ると"index"という項目があります。User.posts.request.vtlやUser.posts.response.vtlのデータソースはDynamoDBのPostテーブルに設定しておき、このPostテーブルのGSI(Global Secondary Index)、つまりここでは userId-indexを参照して該当するuserIdによる全てのPostをリストで持たせることになります。このGSIもserverless.ymlから定義できます。

serverless.yml

次は上の全てリソースをyamlファイルにて管理するserverless.ymlについて説明させていただきます。

service: classmethod-appsync-tutorial

frameworkVersion: ">=1.48.0 <2.0.0"

provider:
  name: aws
  runtime: nodejs10.x
  stage: dev
  region: ap-northeast-2

plugins:
  - serverless-appsync-plugin

custom:
  appSync:
    name: AppSyncTutorialByClassmethod
    authenticationType: AMAZON_COGNITO_USER_POOLS
    userPoolConfig:
      awsRegion: ap-northeast-2
      defaultAction: ALLOW
      userPoolId: { Ref: AppSyncTutorialUserPool }
    region: ap-northeast-2
    mappingTemplatesLocation: resolvers
    mappingTemplates:
      
      # User
      - 
        type: User
        field: posts
        dataSource: Post
      - 
        type: Query
        field: listUser
        dataSource: User
      - 
        type: Query
        field: getUser
        dataSource: User
      - 
        type: Mutation
        field: createUser
        dataSource: User

      # Post
      - 
        type: Post
        field: user
        dataSource: User
      - 
        type: Post
        field: likes
        dataSource: Like
      -
        type: Query
        field: listPost
        dataSource: Post
      - 
        type: Query
        field: listPostByUser
        dataSource: Post
      - 
        type: Query
        field: getPost
        dataSource: Post
      - 
        type: Mutation
        field: createPost
        dataSource: Post

      # Like
      - 
        type: Query
        field: listLike
        dataSource: Like
      - 
        type: Mutation
        field: likePost
        dataSource: Like
      - 
        type: Mutation
        field: cancelLikePost
        dataSource: Like

        
    schema:
      - schema/user.graphql
      - schema/post.graphql
      - schema/like.graphql
    
    #serviceRole: # if not provided, a default role is generated
    dataSources:
      - type: AMAZON_DYNAMODB
        name: User
        description: User Table
        config:
          tableName: User
          iamRoleStatements:
            - Effect: Allow
              Action:
                - dynamodb:*
              Resource:
                - arn:aws:dynamodb:${self:provider.region}:*:table/User
                - arn:aws:dynamodb:${self:provider.region}:*:table/User/*

      - type: AMAZON_DYNAMODB
        name: Post
        description: Post Table
        config:
          tableName: Post
          iamRoleStatements:
            - Effect: Allow
              Action:
                - dynamodb:*
              Resource:
                - arn:aws:dynamodb:${self:provider.region}:*:table/Post
                - arn:aws:dynamodb:${self:provider.region}:*:table/Post/*
      
      - type: AMAZON_DYNAMODB
        name: Like
        description: Like Table
        config:
          tableName: Like
          iamRoleStatements:
            - Effect: Allow
              Action:
                - dynamodb:*
              Resource:
                - arn:aws:dynamodb:${self:provider.region}:*:table/Like
                - arn:aws:dynamodb:${self:provider.region}:*:table/Like/*


resources:
  Resources:
    AppSyncTutorialUserPool:
      Type: AWS::Cognito::UserPool
      DeletionPolicy: Retain
      Properties:
        UserPoolName: AppSyncTutorialUserPool
        AutoVerifiedAttributes:
          - email
        Policies:
          PasswordPolicy:
            MinimumLength: 8
        UsernameAttributes:
          - email

    AppSyncTutorialUserPoolWebClient:
      Type: AWS::Cognito::UserPoolClient
      Properties:
          ClientName: Web
          GenerateSecret: false
          RefreshTokenValidity: 30
          UserPoolId: { Ref: AppSyncTutorialUserPool }


    UserTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: User
        KeySchema:
          -
            AttributeName: userId
            KeyType: HASH
        AttributeDefinitions:
          -
            AttributeName: userId
            AttributeType: S
        BillingMode: PAY_PER_REQUEST

    PostTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: Post
        KeySchema:
          -
            AttributeName: postId
            KeyType: HASH
        AttributeDefinitions:
          -
            AttributeName: postId
            AttributeType: S
          -
            AttributeName: userId
            AttributeType: S
        BillingMode: PAY_PER_REQUEST

        # GSI - userId
        GlobalSecondaryIndexes:
          -
            IndexName: userId-index
            KeySchema:
              - AttributeName: userId
                KeyType: HASH
              - AttributeName: postId
                KeyType: RANGE
            Projection:
              ProjectionType: ALL

    LikeTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: Like
        KeySchema:
          - AttributeName: likeId
            KeyType: HASH
        AttributeDefinitions:
          - AttributeName: likeId
            AttributeType: S
          - AttributeName: userId
            AttributeType: S
          - AttributeName: postId
            AttributeType: S
        BillingMode: PAY_PER_REQUEST

        GlobalSecondaryIndexes:

          # GSI - userId
          - IndexName: userId-index
            KeySchema:
              -
                AttributeName: userId
                KeyType: HASH
              -
                AttributeName: likeId
                KeyType: RANGE
            Projection:
              ProjectionType: ALL
          
          # GSI - postId
          - IndexName: postId-index
            KeySchema:
              -
                AttributeName: postId
                KeyType: HASH
              -
                AttributeName: likeId
                KeyType: RANGE
            Projection:
              ProjectionType: ALL

使っているserverless frameworkの"frameworkVersion"が異なる場合必ずこの値を変えてください。 "provider"にて"stage"や"region"等を設定してCloudFormationが動作するregionを決めます。serverless.ymlファイル一つに当たりCloudFormation スタック一つになります。 "plugins"は"serverless-appsync-plugin"を入力します。

"custom"の"appSync"の下位に作成されたものをserverless-appsync-pluginで読み込んで処理することになります。

一つずつ見てみると、"authenticationType"の場合API KeyやCognitoなどが設定できますが、本テュートリアルではCognito User Poolを生成して作業します。"userPoolConfig"の下にある"userPoolId"の"Ref"は、"custom"項目下の"resources"のAWS:Cognito:UserPoolのUserPoolNameと一致しなければなりません。

また"appSync"の"region"を見てください。この項目は直接入力しなくても自動的に"provider"の"region"に設定されます。 "mappingTemplatesLocation"はVTLファイルが位置されたディレクトリの経路を意味します。ディフォルトでmaping-templatesが設定されていますが本テュートリアルではresolversというフォルダーの下にVTLファイルを置いたのでresolversと書いておきます。

次は"mappingTemplates"です。

"type"はGraphQLスキーマのtypeを意味します。 Userタイプの場合はpostsというPostタイプのフィールドがありましたが、postsはスカラータイプではない為、このフィールドに該当する別のリゾルバーが必要になります。

# User
- 
    type: User
    field: posts
    dataSource: Post

もう一つの例を挙げるとlistUserの場合typeがQueryでした。listUserが接続されるデータソースはDynamoDBのUserテーブルですので下のようになります。

- 
    type: Query
    field: listUser
    dataSource: User

ここでリクエストやレスポンス用のマッピングテンプレートのファイル名は書いてありません。別に"request"や"response"という項目にファイル名を書いてもいいですが、上記でも述べたようにserverless-appsync-pluginではVTLファイル名のコンベンションにより

request: {type}.{field}.request.vtl
response: {type}.{field}.response.vtl

をデフォルト値として決められています。そのコンベンションに従い本チュートリアルではrequestやresponse項目はなしで進めます。

"appSync"の下位の"dataSource"は実際にテーブルを生成するものではなく、既存テーブルを参照してこのテーブルについてのIAM Roleを生成する役割があります。実際にテーブルを作るのは"custom"の下の"resources"で行います。

ここでは IAM RoleのActionを以下のように設定しましたが、実際のプロジェクトの時は必要な権限のみ含めたほうが推奨です。

Action:
  - dynamodb:*

最後に"resources"項目です。

"resources"項目にはCognito User Pool及びDynamoDBのテーブルの生成を担当しています。 DynamoDBのGSI(Global Secondary Index)もここで生成します。

デプロイ

ここまで完了しましたらディプロイは簡単です。

serverless deploy -v

上記のコマンドを入力したらCloudFormationを通じてディプロイが始まります。slsはserverlessのaliasです。

sls-deploy-1 sls-deploy-2

テストしてみよう(Queryメニュー)

これからはAWSコンソールに接続して上記のテストシナリオを確認してみます。

その前にCognito Userを作りましょう。Cognitoのサービスに移動します。下の画面でManage User Poolボタンをクリックします。

appsync-cognito-intro

先ほどのディプロイから生成されたUserPoolをクリックします。

appsync-cognito-select-userpool

左側のメニューから Users and Groups を選択します。

appsync-cognito-user-groups

Create User ボタンをクリックします。

appsync-cognito-create-user

serverless.ymlでCognito User Poolを生成した時設定した通りユーザを作ります。

Properties:
  UserPoolName: AppSyncTutorialUserPool
  AutoVerifiedAttributes:
    - email
  Policies:
    PasswordPolicy:
      MinimumLength: 8
  UsernameAttributes:
    - email

本チュートリアルではemailをユーザ名として使います。パスワードはただ8字以上であればOKです。

appsync-cognito-create-user-input

Cognito ユーザが生成されました!

appsync-cognito-user-created

もう一つ、App Client IDを確認する為 App Client Settings のメニューをクリックします。

appsync-cognito-app-client-settings

ここで Client ID を確認してコピーしておきます。

appsync-cognito-app-client-id

いよいよ AppSyncコンソルに入ります。Queryメニューをクリックして下のような画面に移動します。

appsync-query-ready

クエリを実行する前にログインが必要なのでログインボタンをクリックしてログインしましょう。

appsync-query-cognito-login

最初のログインをしたらパスワードの変更が必要です。さっさと変更します。

appsync-query-cognito-change-password

まずはユーザから作ってみます。左側のようなミューテーションを書いて実行してみると右側の結果が出してきます。

appsync-query-mutation-createuser

次はポストを作ってみます。

appsync-query-mutation-createpost

新しいユーザとポストを作った後、リストクエリを実行します。

appsync-query-listpost

今度は特定のユーザのポストのみをリストで持たすlistPostByUserクエリの実行結果です。予想した通りよく動いています。 appsync-query-listpostbyuser

次はサブスクリプションがよく動いているのかどうかを確認する為、もう一つのウェブブラウザーを用意します。 まずは新しいポストが生成された際にサブスクリプションが動作するかを確認してみます。

appsync-subscription-test-createPost-ready

右側のブラウザーではサブスクリプションを実行して新たなデータが生成されるのをお待ちしています。 左側のミューテーションを実行すると下の画面のように右側に結果が伝達されたのを確認できます。

appsync-subscription-test-createPost-subscription-work

はい、よく動いてます!

最後に特定なポストのいいねの生成のみをサブスクリプションできるかどうかも確認してみます。 左側のミューテーションの実行をお待ちしている画面です。

appsync-subscription-like-post-start

左側のpostIdと右側のpostIdが異なっていますので、予想される結果は右側に何も伝達されないってことです。 左側のミューテーションを実行したら左側の結果は出てきましたが、30秒ほど待っていても右側は何も起こっていませんでした。

appsync-subscription-like-post-nothing-happened

今回は同じpostIdで設定してみます。

appsync-subscription-like-post-success-expected

結果は成功でした。

appsync-subscription-like-post-success

テストは以上になりますが、まだ残っているミューテーションやクエリなどはGithub Repositoryの完成されているコードから試してください。

最後に

以上、AWS再入門ブログリレー 19日目のエントリ『AWS AppSync』編でした。

AppSyncは2018年4月にGAされて、2018年11月にパイプラインリゾルバーやAurora Serverlessサポートなどのメイジャーアップデートを続いてきました。割と最近リリースされたサービスですので、まだ使い方やベストプラクティスなどはよく知れていません。でも実際にAppSyncをいじってみた結果、思ったよりすごく便利なサービスだったことが分かりました。本記事がAppSyncに入門しようといらっしゃる方へ少しでも役に立ったらとても幸いです。

次のブログリレー(7/29)は川原の「Amazon RDS」編です。お楽しみに!!