【Direct Lambda Resolvers: No VTL】 AWS AppSync GraphQL API で Velocity Template なしで Lambda Function を呼び出せるようになりました

2020.08.06

個人的に嬉しいアップデートです。 AWS AppSync で Velocity Template なしで Lambda Function を呼び出せるようになりました!

AWS AppSync releases Direct Lambda Resolvers for GraphQL APIs
Introducing Direct Lambda Resolvers: AWS AppSync GraphQL APIs without VTL | AWS Mobile Blog

Direct Lambda Resolvers for GraphQL APIs とは?

従来、AWS AppSync で GraphQL API をつくり、Lambda Function を呼び出すためには、入出力を変換する Velocity Template(以下、VTL) を挟む必要がありました。こういうやつです。

velocity_template.png

今回のアップデートは、 AWS AppSync が Lambda Function を呼び出す場合に限り、VTL の利用をオプションにできる というものです。上に貼っている画像はアップデート後の 画面なのですが、 Enable request mapping template という設定ができることがわかりますね。

何が嬉しいの?

アップデートの趣旨として、「Lambda Function がそもそも入出力フォーマットを整形することもできる上、VTLもあるのでは、何の役割をどこにもたせるか考えなくちゃいけないよね」という点があります。ですので現場の判断として、「GraphQLの入出力を整形する役割も一律 Lambda Function でやるから、VTLは使わないようにする」といった選択が可能になりました。これは裏側が Lambda Function だからこそとれる選択肢で、ゆえに、AppSync GraphQL API 裏側が DynamoDB, ElasticSearch, RDS, HTTP の場合は、引き続き VTL が 必須 です。この点ご注意ください。

どうやって使えばいいの?

冒頭の画像のように、AWSコンソール上で作って Lambda Function とつなげれば、画面上からオン/オフを切り替えることが可能です。この記事では AWS CDK を使って VTL なしの GraphQL API を作ってみます。

AWS Blog の記述をみていくと、

As far as the AWS CLI and AWS CloudFormation are concerned, VTL templates are now fully optional with Lambda data sources. For example, the following command to create an AppSync resolver which doesn’t provide VTL templates executes successfully as long as the data source linked to the resolver is a Lambda function:

とあります。どうやら リゾルバを作るときにVTLテンプレートを渡さなければ、勝手にVTLなしとして作るよ ということを言っています。AWS CDK は CloudFormation Template へ変換して実行されるので、VTLテンプレートを指定せずに リゾルバを作ればVTLなしとして生成できそうです。

リゾルバ…?データソース…?

ここでいまいちど用語を整理しておきましょう。 AWS AppSync とは、フルマネージドの GraphQL サービスです。AppSync で作られる一番大きい単位のAWSソースは GraphQL API です。ここにいろいろ情報を詰め込んでいきます。GraphQL である以上、スキーマ定義 が必要になります。スキーマ定義は簡単にいうと、その GraphQL API でどんなリクエスト・レスポンスができるかの一覧を整理したものです。以下のような形で、たいていは schema.graphql などのファイルで管理します。

type Reply {
  replyMessage: String!
}
input Message {
  message: String!
}
type Query {
  getReply: Reply!
}

次に、このスキーマ定義に対して、 どことつなぐか という情報が必要です。これをデータソースといいます。データソースは、一般的に外部サービス、ロジック、データベースなどを想定していて、具体的な能力は GraphQL Server に依存します。 AWS AppSync の場合、以下のデータソースが選択できます。

  • Amazon DynamoDB Table
  • Amazon Elasticsearch domain
  • AWS Lambda function
  • Relational database
  • HTTP endpoint
  • None

最後に リゾルバ は、データソースに対して、 GraphQL API がどやって入出力処理をさせるかという情報です。この部分で、DynamoDB のレスポンスを加工したり、Base64 エンコードしたりといった処理を、VTLで行うことができます。

VTL なしの Lambda リゾルバ - AWS CDK

次のコードを見てください。Lambda Function と AppSync GraphQL API を作っています。AWS CDK のコードです。

import * as cdk from '@aws-cdk/core';
import { CfnOutput, Stack } from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as appsync from '@aws-cdk/aws-appsync';
import { AuthorizationType, SchemaDefinition } from '@aws-cdk/aws-appsync';
import {
    GlobalProps,
    NODE_LAMBDA_LAYER_DIR,
    NODE_LAMBDA_SRC_DIR,
} from './global-props';

export async function greetingServiceApplicationStack(
    scope: cdk.Construct,
    id: string,
    global: GlobalProps,
): Promise<Stack> {
    const stack = new cdk.Stack(scope, id, {
        stackName: global.getStackName(id),
    });

    // node_modules LayerVersion
    const nodeModulesLayer = new lambda.LayerVersion(
        stack,
        'NodeModulesLayer',
        {
            layerVersionName: 'NodeModulesLayer',
            code: lambda.Code.fromAsset(NODE_LAMBDA_LAYER_DIR),
            description: 'Node.js modules layer',
            compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
        },
    );

    // つなぎこみたい Lambda Function 
    const greetingFn = new lambda.Function(stack, 'getGreetingReply', {
        functionName: 'getGreetingReply-function',
        code: lambda.Code.fromAsset(NODE_LAMBDA_SRC_DIR),
        handler:
            'lambda/handlers/api-gw/greeting/api-gw-get-greeting-reply-handler.handler',
        runtime: lambda.Runtime.NODEJS_12_X,
        layers: [nodeModulesLayer],
        environment: {
            REGION: cdk.Stack.of(stack).region,
        },
    });

    // GraphQL API のおおもと、ここにいろいろくっつけていく
    const graphApi = new appsync.GraphQLApi(stack, 'GreetingBff', {
        name: global.getGraphApiName('GreetingBff'),
        authorizationConfig: {
            defaultAuthorization: {
                authorizationType: AuthorizationType.API_KEY,
            },
        },
        schemaDefinition: SchemaDefinition.CODE,
    });

    // スキーマ定義 schema.graphql ファイルも渡せますが今回は文字列で指定しています。
    graphApi.updateDefinition(`
input Message {
    message: String!
}

type Query {
    getReply(input: Message): Reply!
}

type Reply {
    reply: String!
}
 `);

    // Lambda Function Datasource
    const greetingFnDataSource = graphApi.addLambdaDataSource(
        'greetingFnDataSource',
        'greetingFnDataSource',
        greetingFn,
    );

    /**
     * Lambda Direct Resolvers
     */
    greetingFnDataSource.createResolver({
        typeName: 'Query',
        fieldName: 'getReply',
    });

    new CfnOutput(stack, 'GreetingFunctionArn', {
        exportName: 'GreetingFunctionArn',
        value: greetingFn.functionArn,
    });

    return stack;
}

greetingFnDataSource.createResolver で、従来は VTL のパラメータも指定していたのですが、オプションパラメータになっていたので雑に指定せずに書いてみました。特にコンパイルエラーは出ていないので、デプロイしてしまいましょう。いけるのか…?

> yarn deploy

GreetingServiceStack (dev-greeting-service-GreetingServiceStack): deploying...
[0%] start: Publishing 5d817eb93d0aa3d6e46c4b24dfda0d6acae98f8d5d8a027a8f20a9bfc062d459:current
[50%] success: Published 5d817eb93d0aa3d6e46c4b24dfda0d6acae98f8d5d8a027a8f20a9bfc062d459:current
[50%] start: Publishing 7b178c003aaac29efd5de78c7105b5ed731fae687ca338a0b04a41cc3bf1b11b:current
[100%] success: Published 7b178c003aaac29efd5de78c7105b5ed731fae687ca338a0b04a41cc3bf1b11b:current
dev-greeting-service-GreetingServiceStack: creating CloudFormation changeset...

 ✅  GreetingServiceStack (dev-greeting-service-GreetingServiceStack)

デプロイできてしまいました。AWS コンソール上はどうなっているかというと:

resolver_no_vtl.png

VTLがオフになっていました。 リリースブログの記載どおり、テンプレートを指定せずにリゾルバを作成すれば、自動的に判断してくれるようです。楽チンですね〜

実行してみる

VTLなしの GraphQL API を叩いてみましょう。裏側で動く Lambda Function は以下のような簡単なロジックです。

import { LambdaContext } from '../../lambda-context';

export async function handler(
    event: GreetingEvent,
    context?: LambdaContext,
): Promise<GreetingResponse> {
    console.info('event', JSON.stringify(event));
    console.info('context', JSON.stringify(context?.awsRequestId));

    return {
        reply: `Fine, and you? > ${event.message}`,
    };
}

type GreetingEvent = {
    message: string;
};

type GreetingResponse = {
    reply: string;
};

message として入力を受け取り、それをくっつけて reply として返す Function です。 GraphQL から Query を叩いてみます。

query-no-param.png

無事、実行できました、が、パラメータがうまく渡っていないようです。Lambda Function のログを見てみました。

{
  "arguments": {
    "message": {
      "message": "hello, direct lambda!"
    }
  },
  "identity": null,
  "source": null,
  "request": {
    "headers": {
...
    }
  },
  "prev": null,
  "info": {
    "selectionSetList": [
      "reply"
    ],
    "parentTypeName": "Query",
    "selectionSetGraphQL": "{\n  reply\n}",
    "fieldName": "getReply",
    "variables": {
      "input": {
        "message": "hello, direct lambda!"
      }
    }
  },
  "stash": {
    
  }
}

なるほど、どうやら、VTLを通さない場合、いろいろな情報を Lambda Function の入力へ詰めるために、入力パラメータは arguments に入れられていることがわかりました。これを受けて Lambda Function を修正します。

import { LambdaContext } from '../../lambda-context';

export async function handler(
    event: AppSyncPassThroughInput,
    context?: LambdaContext,
): Promise<GreetingResponse> {
    console.info('event', JSON.stringify(event));
    console.info('context', JSON.stringify(context?.awsRequestId));

    return {
        reply: `Fine, and you? > ${event.arguments.input.message}`,
    };
}

type AppSyncPassThroughInput = {
    arguments: {
        input: GreetingEvent;
    };
};

type GreetingEvent = {
    message: string;
};

type GreetingResponse = {
    reply: string;
};

GraphQL の実行結果です:

query-param.png

無事にパラメータを渡すことができました。

まとめ

AWS CDK で VTL なしの Lambda リゾルバをデプロイし、パラメータを渡して Labmda Function を実行することができました。今回のリリースでさらに GraphQL API の構築オプションが増え、より責任の所在を明確にできるようになりました。AppSync は好きなサービスのひとつなので、今後も動向をチェックしていこうと思います。