AppSyncのDynamoDBリゾルバーをServerless Frameworkで書いた

こんにちは。てぃろです。

今回は以前書いたAppSyncの解説の続きで、DynamoDBリゾルバーを追加したのでその解説をします。

前回は以下の通り、Lambdaリゾルバーの書き方を解説していました。

ソースはGitHubで公開しています。

デプロイの方法については、リポジトリのREADMEを参照してください。本記事ではソースの中身について解説していきます。是非ソースを見ながら読んでみてください。

今回の記事もAppSyncやGraphQLの概要を知っている方を対象に書いています。先にAppSyncとGraphQLの概要を知りたい方はこちらの解説記事も見てみてください。

AppSyncの全体像から設定すべきものを確認する(DynamoDBリゾルバーの場合)

re:Invent2020の資料より抜粋

AppSyncは大きく3つの要素で構成されています。

  • Schema:スキーマ、いわゆるデータモデル、ユーザからのリクエストとレスポンスの形を定義する
  • Resolver:リゾルバー、ビジネスロジックでデータ処理を担当する
  • DataSources:データソース、リゾルバーからアクセスするデータの保管場所

DynamoDBリゾルバーではこの構成の通りぴったりとハマる形で構築していきます。それは、DataSourceがDynamoDBとして使えるようにResolverを書く、ということを意味します。

SchemaはGraphQLの一般的な方式であり、AppSyncに限って言えばDynamoDBを使うためのResolverの作りこみこそが開発者が最も工夫する余地のある要素なのです。

今回も再現性を持たせて進めるためにServerless Frameworkでの書き方を解説していきます。

コード化したAppSync定義を追加する

前記事でも書いていた通り、Serverless FrameworkでAppSyncを定義するためにはこちらのプラグインを使っていきます。

メインの設定は以下のようにcustomの部分に書きます。Lambdaリゾルバーのときから一部追加があります。

custom:
  appSync:
    name: ${opt:stage, self:provider.stage}-sls-sample
    authenticationType: API_KEY
    schema: ./sls_configurations/appsync/schema/sls_sample.graphql
    dataSources:
      - ${file(./sls_configurations/appsync/data-sources/sls_lambda.yml)}
      - ${file(./sls_configurations/appsync/data-sources/sls_dynamodb.yml)}  // 追加
    mappingTemplatesLocation: ./sls_configurations/appsync/mapping-templates
    mappingTemplates:
      - ${file(./sls_configurations/appsync/mapping-templates-definitions/sls_sample_data.yml)}
      - ${file(./sls_configurations/appsync/mapping-templates-definitions/sls_sample_data_in_dynamodb.yml)}  // 追加
    xrayEnabled: true  // 追加

細かな設定の解説はLambdaリゾルバーの記事を見てください。今回追加及び修正したファイルを定義したいことと合わせて示すと以下のようになります

  1. DynamoDBのテーブルを作る
    • sls_configurations\dynamodb\sls_dynamodb_data.yml
  2. DataSourceとしてのDynamoDBを定義する
    • sls_configurations\appsync\data-sources\sls_dynamodb.yml
  3. どんなSchemaでリクエストを受け付けるか定義を追加する
    • sls_configurations\appsync\schema\sls_sample.graphql
  4. Resolverとしてスキーマからどんな風にパラメータを受け取って返すかを定義する
    • sls_configurations\appsync\mapping-templates-definitions\sls_sample_data_in_dynamodb.yml
    • sls_configurations\appsync\mapping-templates\Mutation.dynamo.request.vtl
    • sls_configurations\appsync\mapping-templates\Mutation.dynamo.response.vtl
    • sls_configurations\appsync\mapping-templates\Query.dynamo.request.vtl
    • sls_configurations\appsync\mapping-templates\Query.dynamo.response.vtl

次の節から順に解説していきます。

ちなみに、上記のcustomにファイルを追加したという観点で注意する点がもう一点あります。それはdataSourcemappingTemplateは参照ファイルが複数になったことで行頭に”-(ハイフン)”が必要になっています

もし私の例のようにファイル分割をする場合にはご注意ください。

dataSourcesの定義

DynamoDBテーブルの定義は以下の通り。Serverless FrameworkではResourcesの書き方はCloudFormationのそれと同じです。

Resources:
  DataSourceTable: 
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: ${opt:stage, self:provider.stage}-${self:service.name}-data
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1

このDynamoDBテーブルをDataSourceとして定義するには以下のように書きます。

- type: AMAZON_DYNAMODB
  name: ${opt:stage, self:provider.stage}_SampleDataInDynamoDB
  description: Sample Data In DynamoDB
  config:
    tableName: { Ref: DataSourceTable }

ここでポイントはtableNameです。DynamoDBテーブルの定義で使った2行目のテーブルの識別子とでもいうべき名前を参照するよう指示しています。これによって変化する可能性のある本当のDynamoDBテーブルの名前を使うことなく、DataSourceとして使用するDynamoDBテーブルを指定することができています。

schemaの定義

DynamoDBリゾルバーに関係する部分のみ抜粋します。

type SampleDataInDynamoDB {
    id: ID!
    name: String
    description: String
    created_at: String!
}

type Query {
    getSampleDataInDynamoDB(id: ID!): SampleDataInDynamoDB
}

type Mutation {
    createSampleDataInDynamoDB(input: SampleInputForDynamoDB!): SampleDataInDynamoDB!
}

input SampleInputForDynamoDB {
    name: String!
    description: String
}

こちらは特に変わったことはありません。ポイントがあるとすればMutationのcreateSampleDataInDynamoDBは引数としてIDを取らず、リクエストを受けたらIDを自動的に採番するようにします。その方法は後述するmappingTemplatesの定義でご説明します。

mappingTemplatesの定義

- dataSource: ${opt:stage, self:provider.stage}_SampleDataInDynamoDB
  type: Query
  field: getSampleDataInDynamoDB
  request: Query.dynamo.request.vtl
  response: Query.dynamo.response.vtl
- dataSource: ${opt:stage, self:provider.stage}_SampleDataInDynamoDB
  type: Mutation
  field: createSampleDataInDynamoDB
  request: Mutation.dynamo.request.vtl
  response: Mutation.dynamo.response.vtl

ここがDynamoDBリゾルバーの肝です。.vtlファイルとして書かれているリクエスト/レスポンスのテンプレートがAppSyncとDynamoDBをどのようにつなぐのかを定義します。

その特徴は以下の通りです。

  • GetItemやPutItem、ScanといったDynamoDBで使われるオペレーションが可能
  • for文やif分によるロジックの記述も可能
  • ID自動採番や日時の取得とフォーマットといった関数が利用可能

これらを定義したテンプレートのことをマッピングテンプレートと言います。詳細は以下の公式ドキュメントをご覧ください。

今回はシンプルにデータ登録の処理を以下のように記述しました。

{
    "version": "2017-02-28",
    "operation": "PutItem",  // DynamoDBのデータ登録のリクエストを指定
    "key": {
        "id": { "S": "$util.autoId()" },  // IDの自動採番
    },
    "attributeValues" : {
        "name": $util.dynamodb.toDynamoDBJson($ctx.args.input.name) ,
        "description": $util.dynamodb.toDynamoDBJson($ctx.args.input.description) ,
        "created_at": { "S": "$util.time.nowFormatted("yyyy-MM-dd HH:mm:ssZ", "+09:00") " } // 日時の取得とフォーマット
    }
}

以上のようにして、リクエストテンプレートでは、DynamoDBに対するオペレーションを定義していたのです。一方レスポンスはというと、DynamoDBはJson形式でレスポンスを返してくれるのでそれをそのまま返す形にしています。

ところで、実は私はこのリクエストテンプレートの定義で少しハマりました。具体的には、本当は$ctx.args.input.nameとするべきところを$ctx.args.nameとしていたのです。

ここで一度クライアントがGraphQLでリクエストするときのjsonを紹介します。以下のように書きます。

mutation Sample {
  createSampleDataInDynamoDB(input: {name: "Wow", description: "Yeahaaaa"})  // ★
  {
    id
    name
    description
    created_at
  }
}

★を付けた行の()内に書いてある引数を$ctx.argsとして受け取ることができます。つまり、こうなります。

$ctx.args = { input: {name: "Wow", description: "Yeah"} }

Lambdaリゾルバーではinputという階層が一つ入ったところでLambda関数内の処理でどうにでもできました。しかしこれはマッピングテンプレートなのでLambdaほどの柔軟性はなく、厳密に書く必要がありました。今回は上記のような関係性だと気付かなかったためinputを抜かして書いたことに気づかずハマってしまっていたのでした…(泣

まとめ

今回はAppSyncのDynamoDBリゾルバーをServerless Framework使って書いたのでそのポイントを解説していきました。

やはり設定は多いですが、Lambdaリゾルバーに比べるとシンプルな構造です。大きな違いは処理系がLambdaではなくマッピングテンプレートのvtlファイルで定義されることです。

DynamoDBリゾルバーを使ったGraphQLの使い勝手のよさは、このマッピングテンプレートの品質に左右されると言っても過言ではないでしょう。