[AWS AppSync] Elasticsearch Service をデータソースに利用する

GraphQLサービス、AWS AppSyncのデータソースとして、Amazon Elasticsearch Serviceを利用してみました。
2018.05.02

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

こんにちは、菊池です。

先日、フルマネージドなGraphQLサービス、AWS AppSyncが正式リリースされました。

先のエントリにて、AppSyncのサンプルスキーマを利用したGraphQL APIの操作を試しました。

上記サンプルスキーマでは、データソースにDynamoDBを利用していました。AppSyncでは他にもElasticsearch Service、Lambdaをデータソースに指定することが可能です。というわけで、今回は公式ドキュメントのチュートリアルを参考に、Elasticsearch Service をデータソースとしてAppSyncを試してみました。

Elasticsearch Serviceを利用したAPIを作成する

それでは、順番に環境を作成していきます。

  1. Elasticsearch Service ドメインの作成
  2. IAM Roleの作成
  3. AppSync APIスキーマの設定
  4. AppSyncデータソースの指定
  5. リゾルバマッピング
  6. クエリの実行

Elasticsearch Service ドメインの作成

まずは、データソースとなるElasticsearch Serviceのドメインを作成します。公式のチュートリアルに、CloudFromationのテンプレートが公開されていますので、それを利用して作成します。

ドキュメントのリンクをクリックするとそのままCloudFormationのスタックが作成可能ですが、東京リージョンに環境を作りたかったのでテンプレートのみ利用して、作成しました。

Elasticsearch Serviceが起動するまでしばらく待ちます。

IAM Roleの作成

続いて、AppSyncサービスがElasticsearch ServiceにアクセスするためのIAM Roleを作成します。

IAM Roleの作成画面から、[AWSサービス] -> [AppSync]を選んで進みます。

適当な名前をつけてIAM Roleを作成します。

作成したIAM Roleに、ポリシーをアタッチします。ポリシーには、Elasticsearch Serviceにアクセスするための以下のような権限を付与しました。deepdish-domainは先に作成したElasticsearch Serviceのドメイン名です。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1234234",
            "Effect": "Allow",
            "Action": [
                "es:ESHttpDelete",
                "es:ESHttpHead",
                "es:ESHttpGet",
                "es:ESHttpPost",
                "es:ESHttpPut"
            ],
            "Resource": [
                "arn:aws:es:ap-northeast-1:xxxxxxxxxxxx:domain/deepdish-domain/*"
            ]
        }
    ]
}

AppSync API スキーマ

次に、AppSync APIを作成します。コンソールから[Create API]を選択して進みます。

API名とスキーマを選択します。今回は、[Custom schema]を選びました。

これで、APIエンドポイントは作成されますので、続けて、左のメニューから[Schema]を選択します。

スキーマを入力する欄がありますので、こちらに記述して保存します。

スキーマは、チュートリアルを参考に、以下のように記述しました。 この定義から後ほど、QuerygetPostallPostsByAuthorsearchPostsを設定していきます。

type Mutation {
	addPost(
		id: ID!,
		author: String,
		title: String,
		url: String,
		ups: Int,
		downs: Int,
		content: String
	): Post
}

type Post {
	id: ID!
	author: String
	title: String
	url: String
	ups: Int
	downs: Int
	content: String
}

type Query {
	getPost(id: ID!): Post
	allPosts: [Post]
	allPostsByAuthor(author: String!): [Post]
	searchPosts: [Post]
}

schema {
	query: Query
	mutation: Mutation
}

AppSyncデータソースの指定

データソースとして利用するElasticsearch Serviceを登録していきます。左のメニューから、[Data Sources]を選択します。新しいデータソースとして、先に作成したElasticsearch Serviceドメイン、IAM Roleを選択します。

任意の名前、データソースの種別としてElasticsearch Service、リージョン、ドメイン、IAM Roleを選択したら保存します。

リゾルバマッピング

リゾルバマッピングにより、クエリとデータソースに対する処理を紐付けしていきます。今回は、

  • getPost
  • allPostsByAuthor
  • searchPosts

の3つのクエリと、

  • addPost

Mutationを設定します。

getPost

Schemaの右のリストから、getPostのAttacheを選択します。

新しくリゾルバを設定する画面になりますので、まずは設定済みのデータソースを選択します。

続けて、[request mapping template][response mapping template] を記述します。データソースがElasticsearch Serviceなので、リクエストはElasticsearchのQuery DSLがベースになります。

リクエストとレスポンスの記述内容詳細は公式ドキュメントを合わせて参照ください。

それぞれ、以下のように記述して保存します。

request mapping template
{
    "version":"2017-02-28",
    "operation":"GET",
    "path":"/id/post/$context.arguments.id",
    "params":{
        "headers":{},
        "queryString":{},
        "body":{}
    }
}
response mapping template
$utils.toJson($context.result.get("_source"))

allPostsByAuthor

こちらも同様に設定していきます。

request mapping template
{
    "version":"2017-02-28",
    "operation":"GET",
    "path":"/id/post/_search",
    "params":{
        "headers":{},
        "queryString":{},
        "body":{
            "from":0,
            "size":50,
            "query":{
                "match":{
                    "author":"$context.arguments.author"
                }
            }
        }
    }
}
response mapping template
[
    #foreach($entry in $context.result.hits.hits)
    #if( $velocityCount > 1 ) , #end
    $utils.toJson($entry.get("_source"))
    #end
]

searchPosts

request mapping template
{
    "version":"2017-02-28",
    "operation":"GET",
    "path":"/id/post/_search",
    "params":{
        "headers":{},
        "queryString":{},
        "body":{
            "from":0,
            "size":50
        }
    }
}
response mapping template
[
    #foreach($entry in $context.result.hits.hits)
    #if( $velocityCount > 1 ) , #end
    $utils.toJson($entry.get("_source"))
    #end
]

addPost

最後に、データをインサートするためのaddPostを登録します。

{
    "version":"2017-02-28",
    "operation":"PUT",
    "path":"/id/post/$context.arguments.id",
    "params":{
        "headers":{},
        "queryString":{},
        "body":{
            "id":"$context.arguments.id",
            "author":"$context.arguments.author",
            "ups":"$context.arguments.ups",
            "downs":"$context.arguments.downs",
            "url":"$context.arguments.url",
            "content":"$context.arguments.content",
            "title":"$context.arguments.title"
        }
    }
}
response mapping template
$utils.toJson($context.result.get("_source"))

クエリの実行

それでは、設定したAPIにクエリを実行していきます。左メニューの[Queries]からクエリが実行可能です。

addPost

まずはデータのインサートです。

リクエスト
mutation {
    addPost(
        id:"12345"
        author: "Fred"
        title: "My first book"
        content: "This will be fun to write!"
    ){
        id
        author
        title
    }
}
レスポンス
{
  "data": {
    "addPost": {
      "id": "12345",
      "author": "Fred",
      "title": "My first book"
    }
  }
}

インサートできました。もう一件インサートしておきます。

リクエスト
mutation {
    addPost(
        id:"67890"
        author: "John"
        title: "My second book"
        content: "This will be fun to write!"
    ){
        id
        author
        title
    }
}
レスポンス
{
  "data": {
    "addPost": {
      "id": "67890",
      "author": "John",
      "title": "My second book"
    }
  }
}

getPost

getPostでは、idを指定したクエリが可能です。

リクエスト
query {
    getPost(id: "12345") {
        id
        title
        author
        content
    }
}
レスポンス
{
  "data": {
    "getPost": {
      "id": "12345",
      "title": "My first book",
      "author": "Fred",
      "content": "This will be fun to write!"
    }
  }
}

searchPosts

searchPostsで、一括してデータを取得できます。

リクエスト
query {
    searchPosts {
        id
        title
        author
        content
    }
}
レスポンス
{
  "data": {
    "searchPosts": [
      {
        "id": "12345",
        "title": "My first book",
        "author": "Fred",
        "content": "This will be fun to write!"
      },
      {
        "id": "67890",
        "title": "My second book",
        "author": "John",
        "content": "This will be fun to write!"
      }
    ]
  }
}

allPostsByAuthor

allPostsByAuthorでは、authorにマッチするデータを取得できます。

リクエスト
query {
    allPostsByAuthor(author: "John") {
        id
        title
        author
        content
    }
}
レスポンス
{
  "data": {
    "allPostsByAuthor": [
      {
        "id": "67890",
        "title": "My second book",
        "author": "John",
        "content": "This will be fun to write!"
      }
    ]
  }
}

まとめ

AWS AppSyncのデータソースとして、Elasticsearch Serviceを利用してみました。APIからデータストアへのリクエストには、Query DSLがほぼそのまま利用できますので、慣れ親しんだデータソースであれば簡単に利用できそうです。

最後に、公式チュートリアルでは、Elasticsearch Serviceをデータソースとして使う上でのベストプラクティスが記載されていますので、紹介しておきます。

  • Amazon Elasticsearch ServiceはプライマリDBではなく、参照用として用いるべきである。DynamoDBと組み合わせた利用も可能。(参考:Tutorial: Combining GraphQL Resolvers
  • Elasticsearch Serviceドメインへは、AppSyncサービスからのみアクセスを許可する。
  • 小さい、低コストなクラスタを使って開発を開始し、本番稼働時にHA構成へと移行する