こんにちは、広野です。
AWS AppSync を使用したアプリケーションを開発する機会があり、リゾルバ、主に VTL の書き方に関してまとまった知識が得られたので紹介します。
AWS AppSync とは
AWS AppSync はサーバーレスアプリケーションのバックエンドを作成するときに使用するサービスです。GraphQL というクエリ言語を使用してアプリケーションからリクエストします。AWS AppSync が受けたリクエストを処理するためにリゾルバという設定があり、その中で VTL (Apache Velocity Template Language) という言語でコードを書きます。同じ目的で Amazon API Gateway と AWS Lambda を使用する構成の方がメジャーですよね。ここでは、AWS AppSync = Amazon API Gateway、リゾルバ = AWS Lambda と置き換えてもらえたらわかりやすいと思います。
AWS AppSync には Amazon API Gateway + AWS Lambda の構成と比較して、以下の利点があります。(ChatGPT に聞いてみた)
- パフォーマンス
VTL は AWS AppSync 内で実行されるため、AWS Lambda 関数を呼び出すよりもレイテンシーが低い。サブスクリプションの機能をデフォルトで備えていることも含め、リアルタイム性が高いアプリケーションにおいて優位性がある。 - コスト効率
VTL は AWS AppSync の一部として実行されるため、追加のコストがかからない。 - スケーラビリティ
AWS AppSync と VTL は自動的にスケーリングされる。AWS Lambda 関数は同時実行数の制約がある。
ただし、AWS AppSync のデメリットもあります。
- VTL は低機能なため、AWS Lambda 関数で書いていたこと全てを実現できるわけではない。
→ JavaScript で書けるようにもなっているので、それにより解決していると思われる。(本記事では紹介しない)
また、本記事では本末転倒ではあるが、AWS AppSync と AWS Lambda を統合することも一般的に行われている。 - AWS AppSync を使用するために必要な GraphQL が難解で、そもそも AWS AppSync に手を出せない。w
私の実感を加味してまとめると
・しかし裏で使われるリゾルバ (VTL) でできることが少ない。簡易な CRUD 処理であれば実装可。
・GraphQL も勉強しないといけない。学習コスト高い。
リゾルバとは
リゾルバは、AWS AppSync がアプリケーションから受け取った GraphQL クエリによるリクエストから、引数を取得してバックエンドにあるデータソース(データベース等)とのやり取りを仲介する役割を持ちます。Amazon API Gateway と Amazon DynamoDB の構成と比較すると以下のようになると思えばよいでしょう。
- Amazon API Gateway → AWS Lambda → Amazon DynamoDB
- AWS AppSync → リゾルバ → Amazon DynamoDB
1つの Amazon API Gateway が複数の AWS Lambda と統合できるのと同じように、1つの AWS AppSync は複数のリゾルバを持つことができます。
しかし、注意しないといけないのは、基本、1つのリゾルバは 1つのデータソースしか統合できません。AWS Lambda 関数であれば、関数コード内で自由に複数のデータソースとのやり取りを書くことができると思いますが、そのような自由度はリゾルバにはありません。
※リゾルバの書き方によっては一部例外はあります。後ほど紹介します。
リゾルバでは VTL という言語で以下 2つの処理を定義します。
- リクエストマッピングテンプレート
- レスポンスマッピングテンプレート
役割としては
- リクエストマッピングテンプレートは、AWS AppSync が受けたリクエストの引数を受け取り、バックエンドに処理をさせるコードに置き換える。(マッピングする)
バックエンドが Amazon DynamoDB であればそれ用の記述を、Amazon Aurora であればまたそれ用の記述になる。アプリケーション側ではバックエンドの違いを気にせず同じクエリでデータを取得することができる。(これは GraphQL のメリット) - レスポンスマッピングテンプレートは、バックエンドが返してきたレスポンスを加工し、アプリケーションが期待するフォーマットに置き換える。(マッピングする)
以降、Amazon DynamoDB から 1つのアイテムだけを取得する VTL のサンプルを紹介します。今後、シリーズものとしていくつか他のサンプルも用意するつもりです。
Amazon DynamoDB に GetItem する VTL
例えば、AWS AppSync から以下のリクエストを受けたとします。Amazon DynamoDB には適切なデータがある想定です。テーブル名はリゾルバの別の設定で行います。
- 引数となるパラメータ: パーティションキー pkey、ソートキー skey
- 必要なレスポンス: data1, data2 という属性の値
マッピングテンプレートは JSON 形式で記述します。その中に VTL が混在する感じです。わかりにくい。
リクエストマッピングテンプレート
{ "version": "2018-05-29", "operation" : "GetItem", "key": { "pkey": $util.dynamodb.toDynamoDBJson($context.arguments.pkey), "skey": $util.dynamodb.toDynamoDBJson($context.arguments.skey) }, "consistentRead" : false, "projection": { "expression": "#data1, #data2", "expressionNames": { "#data1": "data1", "#data2": "data2" } } }
operation には、GetItem を書きます。これは Amazon DynamoDB に GetItem をするぞ、という意思表示です。
key には、受け取った引数を書きます。ここでは、pkey, skey というキーに対して受け取った引数 $context.arguments.pkey と $context.arguments.skey を指定していると思って下さい。受け取った引数はマッピングテンプレート内では $context.arguments 内に格納されるので、この記述のように取り出すことができます。
$util.dynamodb.toDynamoDBJson で囲んでいる理由ですが、Amazon DynamoDB にキーを渡すときに特有の JSON フォーマットにする必要があるため、それをいちいち手書きしなくてもよいようにしてくれます。手書きだと、例ですが “pkey” : { “S” : $util.toJson($context.arguments.pkey) } と書かないといけないことになります。
consistentRead は、結果整合性でよいか、強整合性が必要かを指定します。
projection は、Amazon DynamoDB から受け取りたい属性を指定します。expressionNames では、例えば data1 という属性名を内部的に #data1 という名前に置き換えて、expression で指定しています。これは、Amazon DynamoDB の予約語や AWS AppSync の禁止文字列などが属性名に使われているときにエラーを回避するお作法です。ちなみに projection の項目を書かなければそのアイテムの全属性を取得します。
これで Amazon DynamoDB に GetItem をかけることができます。
ドキュメントによっては $context.arguments を $ctx.args と書いてあるサンプルもあります。私の経験上ですが、どちらでも同じ動きをするようです。
レスポンスマッピングテンプレート
#if($util.isNullOrEmpty($context.result) $util.toJson({ "data1": "no data", "data2": "no data" }) #else $util.toJson($context.result) #end
超基本は、$util.toJson($context.result) を 1行書くだけで成り立ちます。$context.result に Amazon DynamoDB から返ってきたデータが格納されます。気を付けないといけないのは、$util.toJson で囲まないといけないことです。実はマッピングテンプレート内部では、$context.result が JSON フォーマットで書かれていないからです。それを JSON に Parse してくれるのが $util.toJson なのです。
このサンプルで少々手を入れているのは、データが無かったときは data1 と data2 に no data というテキストを入れて返すようにしていることです。$context.result と同じ JSON フォーマットに合わせており、受け取るアプリ側はデータがあろうが無かろうが同じフォーマットでレスポンスを受け取れるようにしています。※すみません、エラーのときの処理は入れていません。
レスポンスマッピングテンプレートでは、受け取ったレスポンス $context.result の内容に応じて、if 文でデータを加工することができます。if 構文を書くときには、その構文の先頭に必ず # を入れます。最後には #end で閉じます。if 文のネストも可能です。
任意の JSON データをレスポンスとして返すときも、必ず $util.toJson で囲むようにしましょう。ドキュメントによっては $util を $utils と書いているサンプルがありますが、私の経験上はどちらでも同じように動いています。
VTL に関しては以下の AWS 公式ドキュメントも必要に応じてご確認ください。
まとめ
いかがでしたでしょうか。
Amazon DynamoDB への CRUD 処理の基本中の基本とも言える GetItem ですが、すでにおなかいっぱいなのではないかと思います。ですが、一度 VTL がどんなものか理解できれば、そうでもなく感じてしまうので不思議です。今後他の VTL パターンも紹介しますね。お楽しみに。
本記事が皆様のお役に立てれば幸いです。