TWBMT

技術的な記事や覚書について書いていきます。その内、自作サイトとかに技術記事をまとめたい。

【AppSync】AppSyncのリゾルバーに VTL を使うべきか Lambda を使うべきか

はじめに

「AppSyncのリゾルバーにVTLとLambdaのどちらを使えば良いか」という記述は中々AWS公式のDOCSからは見つける事が出来ません。

なので、実際に何回かAppSyncを使って開発した経験を交えつつ、VTLとLambdaの使い分けについてまとめていきます。

VTL VS Lambda

以下のスタックオーバーフローの回答が凄く良くまとまっていますので、こちらを参考に考えていきます。

stackoverflow.com

このスタックオーバーフローの回答を参考にするとLambdaとVTLの使い分けるかどうかは
コスト・レイテンシーと開発効率のトレードオフ
という結論になります。

それぞれの観点についての特徴・差異は以下の通りです。

  1. コスト
    LambdaとAppSyncは別なAWSサービスである為、Lambdaを使用する為には追加料金が発生します。
    Lambdaは低価格なサービスなのではあまり気にならない様に感じますが、頻繁にリクエストするユースケースが想定される場合は十分に検討の余地あると思います。
    VTLの場合はAppSyncから直接使用できる為、追加コストは発生しません。

  2. レイテンシー
    AppSync・Lambda間で通信を行う為のレイテンシーが発生します。
    またLambdaのコールドスタートが発生する場合、その分の遅延時間が追加で発生します。
    以下の記事によると、DirectLambdaResolverという仕組みが導入されてからも変わらないとの事でした。
    dev.to こちらもVTLの場合はAppSyncで直接動作する為、追加のレイテンシーが発生しません。

  3. 開発効率 基本的に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はどうしても小さいテストが書きづらい為、複数の条件分岐やループを書く様な事は避けた方が得策だと思います。

まとめと後書き

さくっとまとめます。

  1. 開発効率が落ちない限りはVTLを使う。
  2. 更新処理やその他ソート処理などが絡む場合は最初からLambdaを使う。
  3. もしもコストもレイテンシーも気にならないのであれば存分にLambdaを使う。

こんな感じで使い分けると良いのかなと考えています。 使い分けに悩んだ人の参考になれば幸いです。

また、AppSyncに関しての記事はどうしても少ない or 見つけづらいので、 良し悪しや正しい所間違っている所あればコメントなどご反応頂けると嬉しいです。

追記(2021/12/01)

Twitterにていくつか情報を頂いた為、追記させて頂きます。

突き詰めて考えると普遍的なベストプラクティスというモノは存在せず、ユースケースに応じて実装案を模索するしか無いのかなぁと思いました。

VTLライクなスキーマを用意するか、AppSyncを捨てて自前でapolloサーバーを建てるか、何事もユースケース次第ですね。