AWS AppSync
AWS AppSync 開発者ガイド

チュートリアル : AWS Lambda リゾルバー

AWS AppSync では、AWS Lambda を使用して GraphQL フィールドを解決することができます。たとえば、GraphQL クエリが Amazon RDS インスタンスを呼び出し、GraphQL のミューテーションを Amazon Kinesis ストリームに書き込むことができます。このセクションでは、GraphQL フィールド処理の呼び出しに応じてビジネスロジックを実行する Lambda 関数を記述する方法について説明します。

Lambda 関数の作成

以下の例は、Node.js に記述された、ブログ投稿アプリケーションの一部としてブログ投稿に関するさまざまな処理を実行する Lambda 関数を示しています。

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); var posts = { "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; var relatedPosts = { "1": [posts['4']], "2": [posts['3'], posts['5']], "3": [posts['2'], posts['1']], "4": [posts['2'], posts['1']], "5": [] }; console.log("Got an Invoke Request."); switch(event.field) { case "getPost": var id = event.arguments.id; callback(null, posts[id]); break; case "allPosts": var values = []; for(var d in posts){ values.push(posts[d]); } callback(null, values); break; case "addPost": // return the arguments back callback(null, event.arguments); break; case "addPostErrorWithData": var id = event.arguments.id; var result = posts[id]; // attached additional error information to the post result.errorMessage = 'Error with the mutation, data has changed'; result.errorType = 'MUTATION_ERROR'; callback(null, result); break; case "relatedPosts": var id = event.source.id; callback(null, relatedPosts[id]); break; default: callback("Unknown field, unable to resolve" + event.field, null); break; } };

この Lambda 関数は、ID による投稿の取得、投稿の追加、投稿のリストの取得、および指定した投稿に関連する投稿の取得を処理します。

注意 : event.fieldswitch ステートメントにより、Lambda 関数は現在解決しているフィールドを確認することができます。

ここでは、AWS マネジメントコンソールを使用して、または以下を選択して AWS CloudFormation を使用して、この Lambda 関数を作成します。

aws cloudformation create-stack --stack-name AppSyncLambdaExample \ --template-url https://s3-us-west-2.amazonaws.com/awsappsync/resources/lambda/LambdaCFTemplate.yaml \ --capabilities CAPABILITY_NAMED_IAM

AWS アカウントでは、米国西部 2 (オレゴン) 地区のこの AWS CloudFormation スタックを起動できます。

AWS Lambda のデータソースを設定する

AWS Lambda 関数を作成した後、コンソールで AWS AppSync GraphQL API に移動し、[Data Sources (データソース)] タブを選択します。

[New (新規)] を選択して、データソースにわかりやすい名前を入力し ("Lambda" など)、[Data source type (データソースのタイプ)] で [AWS Lambda] を選択します。適切な AWS リージョンを選択します。Lambda 関数がリストに表示されます。

Lambda 関数を選択した後、(AWS AppSync により適切なアクセス許可が割り当てられる) 新しいロールを作成するか、以下のインラインポリシーを含む既存のロールを選択します。

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lambda:InvokeFunction" ], "Resource": "arn:aws:lambda:REGION:ACCOUNTNUMBER:function/LAMBDA_FUNCTION" } ] }

そのロールにはさらに次のように、AWS AppSync との信頼関係を設定する必要があります。

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

GraphQL スキーマを作成する

以上でデータソースが Lambda 関数に接続されたので、GraphQL スキーマを作成します。

AWS AppSync コンソールのスキーマエディタで、スキーマが以下のスキーマと一致することを確認します。

schema { query: Query mutation: Mutation } type Query { getPost(id:ID!): Post allPosts: [Post] } type Mutation { addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int relatedPosts: [Post] }

リゾルバーの設定

AWS Lambda のデータソースと有効な GraphQL スキーマが登録されたので、リゾルバーを使用して GraphQL フィールドを Lambda のデータソースに接続することができます。

リゾルバーを作成するには、マッピングテンプレートが必要です。マッピングテンプレートの詳細については、「リゾルバーのマッピングテンプレートの概要」に記載された AWS AppSync のマッピングテンプレートの概要を参照してください。

Lambda のマッピングテンプレートの詳細については、「Lambda のリゾルバーのマッピングテンプレートリファレンス」を参照してください。

このステップでは、getPost(id:ID!): PostallPosts: [Post]addPost(id: ID!, author: String!, title: String, content: String, url: String): Post!、および Post.relatedPosts: [Post] の各フィールドで Lambda 関数にリゾルバーをアタッチします。

AWS AppSync コンソールのスキーマエディタで、右側から getPost(id:ID!): Post に対して、[Attach Resolver (リゾルバーをアタッチ)] を選択します。

AWS Lambda データソースを選択します。[request mapping template (リクエストマッピングテンプレート)] セクションで、[Invoke And Forward Arguments (呼び出しと引数の転送)] を選択します。

payload オブジェクトを変更し、フィールド名を追加します。テンプレートは次のようになります。

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "getPost", "arguments": $utils.toJson($context.arguments) } }

[response mapping template (レスポンスマッピングテンプレート)] セクションで、[Return Lambda Result (Lambda 関数の結果を返す)] を選択します。

この場合、ベーステンプレートをそのまま使用します。次のようになります。

$utils.toJson($context.result)

[Save] を選択します。最初のリゾルバーが正常に追加されました。以下のように、残りのフィールドについてこの操作を繰り返します。

addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! リクエストマッピングテンプレートについて

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "addPost", "arguments": $utils.toJson($context.arguments) } }

addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! レスポンスマッピングテンプレートについて

$utils.toJson($context.result)

allPosts: [Post] リクエストマッピングテンプレートについて

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "allPosts" } }

allPosts: [Post] レスポンスマッピングテンプレートについて

$utils.toJson($context.result)

Post.relatedPosts: [Post] リクエストマッピングテンプレートについて

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "relatedPosts", "source": $utils.toJson($context.source) } }

Post.relatedPosts: [Post] レスポンスマッピングテンプレートについて

$utils.toJson($context.result)

GraphQL API をテストする

これで Lambda 関数が GraphQL リゾルバーに接続されたので、コンソールまたはクライアントアプリケーションを使用してミューテーションまたはクエリが実行できます。

AWS AppSync コンソールの左側で [Queries (クエリ)] タブを選択し、次のコードを貼り付けます。

addPost ミューテーション

mutation addPost { addPost( id: 6 author: "Author6" title: "Sixth book" url: "https://www.amazon.com/" content: "This is the book is a tutorial for using GraphQL with AWS AppSync." ) { id author title content url ups downs } }

getPost クエリ

query { getPost(id: "2") { id author title content url ups downs } }

allPosts クエリ

query { allPosts { id author title content url ups downs relatedPosts { id title } } }

エラーを返す

指定したフィールドの解決でエラーが発生する場合があります。AWS AppSync により、以下のソースからエラーを生成できます。

  • リクエストまたはレスポンスのマッピングテンプレート

  • Lambda 関数

マッピングテンプレートからの場合

VTL テンプレートから $utils.error ヘルパーメソッドを使用して故意にエラーを発生させることができます。これは引数として、errorMessageerrorType、および必要に応じて data の各値を含みます。data は、エラーの発生時にクライアントに追加のデータを返す場合に利用できます。data オブジェクトは GraphQL の最終レスポンスで errors に追加されます。

次の例では、Post.relatedPosts: [Post] レスポンスマッピングテンプレートでそれを使用する方法を示しています。

$utils.error("Failed to fetch relatedPosts", "LambdaFailure", $context.result)

これにより、以下のような GraphQL レスポンスが生成されます。

{ "data": { "allPosts": [ { "id": "2", "title": "Second book", "relatedPosts": null }, ... ] }, "errors": [ { "path": [ "allPosts", 0, "relatedPosts" ], "errorType": "LambdaFailure", "locations": [ { "line": 5, "column": 5 } ], "message": "Failed to fetch relatedPosts", "data": [ { "id": "2", "title": "Second book" }, { "id": "1", "title": "First book" } ] } ] }

ここで、エラーおよび errorMessageerrorTypedatadata.errors[0] オブジェクトに存在するため、allPosts[0].relatedPostsnull になります。

Lambda 関数からの場合

AWS AppSync はまた、Lambda 関数からスローされたエラーも認識できます。Lambda プログラミングモデルを使用し、処理されるエラーを発生させることができます。Lambda 関数からエラーがスローされた場合、AWS AppSync は現在のフィールドの解決に失敗します。Lambda から返されたエラーメッセージのみがレスポンスに設定されます。また現在のところ、Lambda 関数からエラーを発生させて、クライアントにエラー関連以外のデータを渡すことはできません。

注意 : Lambda 関数が処理されないエラーを発生させた場合、AWS AppSync は AWS Lambda によって設定されたエラーメッセージを使用します。

以下の Lambda 関数はエラーを発生させます。

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); callback("I fail. Always."); };

これにより、以下のような GraphQL レスポンスが返されます。

{ "data": { "allPosts": [ { "id": "2", "title": "Second book", "relatedPosts": null }, ... ] }, "errors": [ { "path": [ "allPosts", 0, "relatedPosts" ], "errorType": "Lambda:Handled", "locations": [ { "line": 5, "column": 5 } ], "message": "I fail. Always." } ] }

高度なユースケース : バッチ処理

この例の Lambda 関数には、指定した投稿に関連する投稿のリストを返す relatedPosts フィールドが含まれています。この例のクエリでは、Lambda 関数からの allPosts フィールドの呼び出しにより 5 件の投稿が返されます。返された各投稿に対して relatedPosts の解決も指定しているので、relatedPosts フィールドの処理が続けて 5 回呼び出されます。

query { allPosts { // 1 Lambda invocation - yields 5 Posts id author title content url ups downs relatedPosts { // 5 Lambda invocations - each yields 5 posts id title } } }

この例では大して意味がないように思われますが、こうした余分な取得処理が積み重なることで、アプリケーションに急速に害をなす可能性があります。

同じクエリで返された関連する Posts について、再度 relatedPosts を取得すると、呼び出しの数は大幅に増加します。

query { allPosts { // 1 Lambda invocation - yields 5 Posts id author title content url ups downs relatedPosts { // 5 Lambda invocations - each yield 5 posts = 5 x 5 Posts id title relatedPosts { // 5 x 5 Lambda invocations - each yield 5 posts = 25 x 5 Posts id title author } } } }

この比較的単純なクエリでは、AWS AppSync は Lambda 関数を 1 + 5 + 25 = 31 回呼び出します。

これはよくある課題であり、N+1 問題と呼ばれます (この例では、N = 5)。これによりアプリケーションのレイテンシーとコストが増大します。

この問題を解決する 1 つの方法は、同様なフィールドのリゾルバーリクエストを同時にバッチ処理することです。この例では、Lambda 関数は指定された 1 つの投稿に関連する投稿のリストを解決するのではなく、指定された一連の投稿に関連する投稿のリストを解決できます。

これを示すため、Post.relatedPosts: [Post] リゾルバーをバッチ処理が有効なリゾルバーに切り替えます。

AWS AppSync コンソールの右側で、既存の Post.relatedPosts: [Post] リゾルバーを選択します。リクエストマッピングテンプレートを次のように変更します。

{ "version": "2017-02-28", "operation": "BatchInvoke", "payload": { "field": "relatedPosts", "source": $utils.toJson($context.source) } }

operation フィールドのみを Invoke から BatchInvoke に変更します。ペイロードフィールドはテンプレートで指定した内容の配列となります。この例では、Lambda 関数は、入力として以下を受け取ります。

[ { "field": "relatedPosts", "source": { "id": 1 } }, { "field": "relatedPosts", "source": { "id": 2 } }, ... ]

リクエストマッピングテンプレートで BatchInvoke を指定した場合、Lambda 関数はリクエストのリストを受け取り、結果のリストを返します。

具体的には、結果のリストがリクエストペイロードエントリのサイズおよび順序と一致する必要があり、これにより AWS AppSync は結果を照合できます。

このバッチ処理の例では、Lambda 関数は次のように一連の結果を返します。

[ [{"id":"2","title":"Second book"}, {"id":"3","title":"Third book"}], // relatedPosts for id=1 [{"id":"3","title":"Third book"}] // relatedPosts for id=2 ]

次の Node.js の AWS Lambda 関数は、Post.relatedPosts フィールドに対してこのバッチ処理機能を次のように実行します。

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); var posts = { "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; var relatedPosts = { "1": [posts['4']], "2": [posts['3'], posts['5']], "3": [posts['2'], posts['1']], "4": [posts['2'], posts['1']], "5": [] }; console.log("Got a BatchInvoke Request. The payload has %d items to resolve.", event.length); // event is now an array var field = event[0].field; switch(field) { case "relatedPosts": var results = []; // the response MUST contain the same number // of entries as the payload array for (var i=0; i< event.length; i++) { console.log("post {}", JSON.stringify(event[i].source)); results.push(relatedPosts[event[i].source.id]); } console.log("results {}", JSON.stringify(results)); callback(null, results); break; default: callback("Unknown field, unable to resolve" + field, null); break; } };

個々のエラーを返す

Lambda 関数から単一のエラーを返せることや、マッピングテンプレートから 1 つのエラーを生成できることは以前の例で説明しました。バッチ処理を呼び出した場合、Lambda 関数からエラーが発生すると、バッチ処理全体が失敗としてフラグ付けされます。これは、データストアとの接続が切れた場合など、回復不可能なエラーが発生するシナリオでは問題ないかもしれません。ここでは、バッチ処理の一部が成功し、他が失敗した場合、エラーと有効なデータの両方を返すことができます。AWS AppSync では、バッチ処理のレスポンスがバッチの元のサイズと一致する要素のリストとなるように要求されるため、エラーから有効なデータが識別できるようなデータ構造を定義する必要があります。

たとえば、関連する一連の投稿が返されることを Lambda 関数が期待している場合には、dataerrorMessageerrorType の各フィールドを任意に含む Response オブジェクトのリストを返すことを選択できます。errorMessage フィールドが存在する場合は、エラーが発生したことを意味します。

次のコードは Lambda 関数の更新方法を示しています。

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); var posts = { "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; var relatedPosts = { "1": [posts['4']], "2": [posts['3'], posts['5']], "3": [posts['2'], posts['1']], "4": [posts['2'], posts['1']], "5": [] }; console.log("Got a BatchInvoke Request. The payload has %d items to resolve.", event.length); // event is now an array var field = event[0].field; switch(field) { case "relatedPosts": var results = []; results.push({ 'data': relatedPosts['1'] }); results.push({ 'data': relatedPosts['2'] }); results.push({ 'data': null, 'errorMessage': 'Error Happened', 'errorType': 'ERROR' }); results.push(null); results.push({ 'data': relatedPosts['3'], 'errorMessage': 'Error Happened with last result', 'errorType': 'ERROR' }); callback(null, results); break; default: callback("Unknown field, unable to resolve" + field, null); break; } };

この例では、次のレスポンスマッピングテンプレートが Lambda 関数の各項目を解析し、発生するエラーがあればそれを生成します。

#if( $context.result && $context.result.errorMessage ) $utils.error($context.result.errorMessage, $context.result.errorType, $context.result.data) #else $utils.toJson($context.result.data) #end

この例では、以下のような GraphQL レスポンスが返されます。

{ "data": { "allPosts": [ { "id": "1", "relatedPostsPartialErrors": [ { "id": "4", "title": "Fourth book" } ] }, { "id": "2", "relatedPostsPartialErrors": [ { "id": "3", "title": "Third book" }, { "id": "5", "title": "Fifth book" } ] }, { "id": "3", "relatedPostsPartialErrors": null }, { "id": "4", "relatedPostsPartialErrors": null }, { "id": "5", "relatedPostsPartialErrors": null } ] }, "errors": [ { "path": [ "allPosts", 2, "relatedPostsPartialErrors" ], "errorType": "ERROR", "locations": [ { "line": 4, "column": 9 } ], "message": "Error Happened" }, { "path": [ "allPosts", 4, "relatedPostsPartialErrors" ], "data": [ { "id": "2", "title": "Second book" }, { "id": "1", "title": "First book" } ], "errorType": "ERROR", "locations": [ { "line": 4, "column": 9 } ], "message": "Error Happened with last result" } ] }