【AppSync】AppSyncのリゾルバーに VTL を使うべきか Lambda を使うべきか
はじめに
「AppSyncのリゾルバーにVTLとLambdaのどちらを使えば良いか」という記述は中々AWS公式のDOCSからは見つける事が出来ません。
なので、実際に何回かAppSyncを使って開発した経験を交えつつ、VTLとLambdaの使い分けについてまとめていきます。
VTL VS Lambda
以下のスタックオーバーフローの回答が凄く良くまとまっていますので、こちらを参考に考えていきます。
このスタックオーバーフローの回答を参考にするとLambdaとVTLの使い分けるかどうかは
コスト・レイテンシーと開発効率のトレードオフ
という結論になります。
それぞれの観点についての特徴・差異は以下の通りです。
コスト
LambdaとAppSyncは別なAWSサービスである為、Lambdaを使用する為には追加料金が発生します。
Lambdaは低価格なサービスなのではあまり気にならない様に感じますが、頻繁にリクエストするユースケースが想定される場合は十分に検討の余地あると思います。
VTLの場合はAppSyncから直接使用できる為、追加コストは発生しません。レイテンシー
AppSync・Lambda間で通信を行う為のレイテンシーが発生します。
またLambdaのコールドスタートが発生する場合、その分の遅延時間が追加で発生します。
以下の記事によると、DirectLambdaResolverという仕組みが導入されてからも変わらないとの事でした。
dev.to こちらもVTLの場合はAppSyncで直接動作する為、追加のレイテンシーが発生しません。開発効率 基本的にLambdaの方がVTLよりも高機能ですよね。 後ほど実体験交えてこの辺りについて書いていきますので、詳細割愛します。
以上の3点がVTLとLambdaを使い分ける上で大きなポイントです。 まとめると「コスト・レイテンシーと開発効率のトレードオフ」と言えると思います。
実際に書いてみた感想
「コスト・レイテンシーと開発効率のトレードオフ」とはいえ、 実際にAppSyncを作成していてVTLを使って十分だった場面、Lambdaが欲しいと思った場面がありました。
それらについて書いていきます。
*AppSyncのデータソースにはDynamoDBを使う想定で書いていきます。
意外と書きやすいVTL
もちろんVTL自体はお世辞にも書きやすいとは言えませんが、
以下の様な複雑な処理を伴わない場合は十分読みやすく簡潔に書く事が出来ます。
## sample_create.vtl { "version": "2017-02-28", "operation": "PutItem", "key": { "PK": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.PK, $util.autoId())), "SK": $util.dynamodb.toDynamoDBJson($context.args.input.SK) }, "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input), "condition": $util.toJson($condition) }
恐らくCreate, Read, Delete辺りのシンプルな操作であれば、Lambdaにせずとも十分楽且つ速く書けると思います。
もし後からLambdaにリプレースする場合も、記述量が少なければそんなに苦ではありません。
またリクエストテンプレートとマッピングテンプレートで責務分割が強制されるお陰で、VTLの方が読みやすいと感じる事もありました。 これはコーディング規約で解決できる問題という事は内緒です。
Lambdaが最初から欲しい場合
更新処理や複数のデータソースに触れる様なリゾルバーに関しては、最初からLambdaを使ったほうが良いと思います。
以下は公式のサンプルコードですが、これを改良して使っていくのは中々厳しいものがありました。
(長いのでトグルにしました。)
## AWS公式のUpdateリゾルバーのサンプル ## 引用元: https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/tutorial-dynamodb-resolvers.html { "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id) }, ## Set up some space to keep track of things you're updating ** #set( $expNames = {} ) #set( $expValues = {} ) #set( $expSet = {} ) #set( $expAdd = {} ) #set( $expRemove = [] ) ## Increment "version" by 1 ** $!{expAdd.put("version", ":one")} $!{expValues.put(":one", { "N" : 1 })} ## Iterate through each argument, skipping "id" and "expectedVersion" ** #foreach( $entry in $context.arguments.entrySet() ) #if( $entry.key != "id" && $entry.key != "expectedVersion" ) #if( (!$entry.value) && ("$!{entry.value}" == "") ) ## If the argument is set to "null", then remove that attribute from the item in DynamoDB ** # # Note: 項目欠損時の条件分岐はここに書き入れる必要があります。辛いですね。 # #set( $discard = ${expRemove.add("#${entry.key}")} ) $!{expNames.put("#${entry.key}", "$entry.key")} #else ## Otherwise set (or update) the attribute on the item in DynamoDB ** $!{expSet.put("#${entry.key}", ":${entry.key}")} $!{expNames.put("#${entry.key}", "$entry.key")} $!{expValues.put(":${entry.key}", { "S" : "${entry.value}" })} #end #end #end ## Start building the update expression, starting with attributes you're going to SET ** #set( $expression = "" ) #if( !${expSet.isEmpty()} ) #set( $expression = "SET" ) #foreach( $entry in $expSet.entrySet() ) #set( $expression = "${expression} ${entry.key} = ${entry.value}" ) #if ( $foreach.hasNext ) #set( $expression = "${expression}," ) #end #end #end ## Continue building the update expression, adding attributes you're going to ADD ** #if( !${expAdd.isEmpty()} ) #set( $expression = "${expression} ADD" ) #foreach( $entry in $expAdd.entrySet() ) #set( $expression = "${expression} ${entry.key} ${entry.value}" ) #if ( $foreach.hasNext ) #set( $expression = "${expression}," ) #end #end #end ## Continue building the update expression, adding attributes you're going to REMOVE ** #if( !${expRemove.isEmpty()} ) #set( $expression = "${expression} REMOVE" ) #foreach( $entry in $expRemove ) #set( $expression = "${expression} ${entry}" ) #if ( $foreach.hasNext ) #set( $expression = "${expression}," ) #end #end #end ## Finally, write the update expression into the document, along with any expressionNames and expressionValues ** "update" : { "expression" : "${expression}" #if( !${expNames.isEmpty()} ) ,"expressionNames" : $utils.toJson($expNames) #end #if( !${expValues.isEmpty()} ) ,"expressionValues" : $utils.toJson($expValues) #end }, "condition" : { "expression" : "version = :expectedVersion", "expressionValues" : { ":expectedVersion" : $util.dynamodb.toDynamoDBJson($context.arguments.expectedVersion) } } }
更新処理の場合、GraphQLの特性から項目欠損とnullによって振る舞いを変える為に、どうしてもif文が増えます。
VTLはどうしても小さいテストが書きづらい為、複数の条件分岐やループを書く様な事は避けた方が得策だと思います。
まとめと後書き
さくっとまとめます。
- 開発効率が落ちない限りはVTLを使う。
- 更新処理やその他ソート処理などが絡む場合は最初からLambdaを使う。
- もしもコストもレイテンシーも気にならないのであれば存分にLambdaを使う。
こんな感じで使い分けると良いのかなと考えています。 使い分けに悩んだ人の参考になれば幸いです。
また、AppSyncに関しての記事はどうしても少ない or 見つけづらいので、 良し悪しや正しい所間違っている所あればコメントなどご反応頂けると嬉しいです。
追記(2021/12/01)
Twitterにていくつか情報を頂いた為、追記させて頂きます。
VTLを使用した場合、DB知識・ドメイン知識が抜け落ちるのはキツイ
個人的にはこ問題が顕在化する規模で使ったことが無い為、目からウロコでした。
確かにVTLの記述性の低さによるデメリットだと思います。
GraphQLスキーマとPK/SK定義でカバー出来ないかと思いましたが、規模次第ですね。リゾルバーをLambdaで統一したいけど、実際レイテンシーがキツイ
大まかに1リクエスト辺り600ms分ずつ遅延する計算となり、全てのAPIをLambdaにする事には良し悪しがある模様です。
上記で引用している記事やTwitterで教えて頂いたzennの記事に具体的な数値がまとめられています。
AWS AppSync Direct Lambda vs DynamoDB Resolver - DEV Community
AppSync(vtl)とLambdaでのDynamoDBアクセスの速度比較
突き詰めて考えると普遍的なベストプラクティスというモノは存在せず、ユースケースに応じて実装案を模索するしか無いのかなぁと思いました。
VTLライクなスキーマを用意するか、AppSyncを捨てて自前でapolloサーバーを建てるか、何事もユースケース次第ですね。
【覚書】 AppSyncリゾルバー(VTL)を書く為のVSCodeプラグイン
はじめに
AppSyncのリゾルバーをVTLで書く時に使用したプラグインが調子良かったのでご紹介します。
プラグインの紹介
appsync-resolver-autocomplete
というプラグインです。
公式のデモのGIFの通り、AppSyncの組み込みのユーティリティ関数などのオートコンプリートを表示してくれます。
引用元: AppSync Resolver Autocomplete - Visual Studio Marketplace
使用感
全く何もない状態でDOCSを見ながら書くよりもtypoも減りますし、十分使えました。
最後のコミットが2020年で止まっていますが、基本的なユーティリティ関数が更新される事も少ないと思います。 なので、個人的には気にしないことにしています。 (他の選択肢もほぼ無いですしね。)
上記のプラグインが動かない場合
上記のプラグインを入れて *.vtl
ファイルを開いてもオートコンプリートが働かない事がありました。
デフォルトではVSCodeはVTLをサポートしていない事が原因だったので以下のプラグインを入れた所、無事にオートコンプリートが効く様になりました。
まとめ
徒手空拳でVTLと戦うのは中々大変でしたが、これで少し楽になりました。
AppSyncはAWSの中でもかなり好きなサービスなので、また何かAppSyncにまつわる記事を書きたいですね。
【DynamoDB】 CloudFormation cannot update a stack when a custom-named resource requires replacing. の対策
CloudFormationを基にDynamoDB の Table などカスタム名付きのリソースを更新する際にタイトルの様なエラーが発生することがあります。
結論的にはCloudFormationを使用する上では避けられないエラーですので、このエラーの原因と上手い対応方法についてのメモをまとめていきます。
エラーの原因
以下のドキュメントにCloudFormationがリソースを更新する時の振る舞いが記載してあります。
Update behaviors of stack resources - AWS CloudFormation
このドキュメントの Replacement
の項目に、「AWS CloudFormationは、アップデートの際にリソースを再作成し、新しい物理IDも生成します」と記載があります。
調べていく限りはこれが原因の模様でして、カスタム名付きのリソースの場合はリプレイスを伴う更新の際に「 カスタム名 = 物理ID」であるが為に、物理IDが衝突してエラーが発生するという事の様です。
「置換せずに更新してくれ!」っても思いますが「KeySchemaを変更して保持しているインデックス情報を書き換える事とDynamoDBの使用状況を基に課金料金を決めることとは訳が違うんだよ!」と言われたら「確かに。」と思ってしまいます。
という訳で、つらつらと対策をまとめていきます。
対策
公式のドキュメントではタイトルのエラーの対応として以下の2パターンが紹介されています。
- カスタム名を使用せずにリソースを作成する
- カスタム名を変更してデプロイ後、元々のカスタム名で再デプロイする
「1. 」の場合は、カスタム名を使用しない事によって毎回一意な物理IDを払い出す為にエラーを避けることが出来るという事ですね。
もしかしたら、IAMやAutoScalingGroup なんかの永続性のある情報を持たないリソースなら有用かもしれない。
カスタム名を使わなければ行けない場合は自然と「2. 」の方法になります。
ただ、これをやるとDynamoDBのストレージの内容が全て吹き飛びます。 困ったもんですね。
とはいえ、DBの置換自体は避けられない事ですので大人しく対応策を調べていったところ、リソースの更新時の振る舞いの一つに Retain
というプロパティを見つけました。
UpdateReplacePolicy 属性 - AWS CloudFormation
CloudFormation上からはリソースを削除するが実リソースとしては残す、というものの様です。
これを使えば、かなり面倒ですが、
「新DB作成 -> 旧DBから新DBへデータ移動 -> 真DB作成(旧DBと同一名で作成) -> 新DBから真DBへデータ移動」
という手順でカスタム名を維持したままDBを保つことが出来そうです。
ただ、データ移行が面倒くさいので、
カスタム名に version名 を付随させて更新の度にverisonを上げるというやり方でも良さそうな気もしますね。
まとめ
ざっくりとですが、
- エラーの原因はリソースの置換の際の物理IDの衝突が原因。
- 置換が必須の更新(DynamoDBのLSIなど)の場合、カスタム名の改名が必要になる。
- その場合の移行方法として、
UpdateReplacePolicy
のRetein
などを利用し、旧リソースからデータを移動させる方針が良さそう。
といった次第ですね。
ひとしきりエラーの原因と対応方針は見つけられたのですが、 置換更新する際のベストプラクティスやデータ移行の方法まだ見つけられていないのでそちらも見つけ次第更新したいですね。
追記
~ この辺りも参考になりそう。(アカウント間でのデータ移行の方法なのでオーバースペックかも。) ~ aws.amazon.com
~ DynamoDBインポート / エクスポート機能を利用すると楽なことを確認。~ dev.classmethod.jp
色々調べた結果、scan + batchWrite
を実行するスクリプトを書く事が一番の様子。
また追って更新する。
【覚書】AngularプロジェクトでのEs-Lint / Prettierによる快適コーディング設定
前書き
AngularプロジェクトへのEsLint / Prettierの導入方法の覚え書き。
個人レベルでは開発者体験の向上、チーム開発ではレビューのコスト削減の為、Linter/Formatterは必須だなぁと思うのでサクッとお手軽に導入していきましょう。
概要
以下の手順でお手軽に導入する事が出来る。
npx ng add @angular-eslint/schematics
npm install -D prettier@latest eslint-config-prettier@latest
Command Pallete
->Prettier: Create Configuration File
ルートディレクトリの
.eslintrc.json
.prettierrc
をお好みの設定に上書き
*動作環境
node v16.1.0
@angular/cli 12.2.7
Eslint / Prettierの設定について
細かい設定まで追えている訳ではない為、
- 基本的にデフォルトの設定を基に使用
- セミコロンのみ無し
という設定の追加方法だけ書き残します。
以下の記述を.prettierrc
と.eslintrc.js
へそれぞれ追加すればOK。
.prettierrc
{ "rules": { "semicolon": [ false, "always" ], "no-unexpected-multiline":"error", // other settings... } }
.eslintrc.js
{ "semi": false // other settings... }
余談: セミコロンについて
他言語(Java/Kotlin)を書いていて「セミコロンが無い方が、読み心地・書き心地どちらも良いなぁ」と感じていて、 少し調べてみると以下の記事と出会いました。
「Typescriptでセミコロンは使うな!!」という事が述べられています。
(Don’t use Semicolons in TypeScript!)https://medium.com/@eugenkiss/dont-use-semicolons-in-typescript-474ccfe4bdb3
要旨としては
「セミコロンを使わない方が煩わしさが減る」
「セミコロンを使用する事で防がれるJavascriptのエラーはリンターで防ぐ事が出来る」
といった話です。
この記事の中の
「タイピングも少なくなり、IDEの余計な警告に手を止められる事が減る」
という話は凄く共感していて、
Java/Kotlinを書いている時にIDEのWarningやErrorによって手を止められる時間は少ない方が良いと感じています。
(もちろんJavaはセミコロン必須だけれども。)
WarningやErrorは有り難いけれど、手を止めずに済むなら手を止めないで済む方が良いですね。
(ちなみにPrettierの semi
のドキュメントでは「ASI障害の危険性がある場合のみ自動挿入」と記載があり、安心してPrettierを頼って良さそう。)
https://prettier.io/docs/en/options.html
参考
(angular-eslint/angular-eslint)https://github.com/angular-eslint/angular-eslint (Don’t use Semicolons in TypeScript!)https://medium.com/@eugenkiss/dont-use-semicolons-in-typescript-474ccfe4bdb3