AWS AppSyncでDynamoDBのCRUD操作(生成、読取、更新、削除)Lambdaを使わずVTLで実装してみた(CDK)

テーマ

今回はAppSyncからVTL経由でDyamoDBをリクエストして、CRUD操作する実装をCDKで実装しました

簡単にキーワードを確認

AWS AppSync

サーバーレスのGraphQL APIを作成するサービス

AWS CDK

PythonやTypeScriptなどのプログラミング言語を使用して、AWSリソースを定義するツール

CloudFormationのテンプレートをプログラミング言語で出力することが出来る

VTL

VTL(Velocity Template Language)はテンプレート言語で今回はDynamoDBへのリクエストの操作設定などをここに実装していきます

イメージ

ディレクトリ構成

backend/
 ├ cdk_stacks/
 │    ├ appsync
 │    │   ├ vtl
 │    │   │  ├ create_user.vtl
 │    │   │  ├ delete_user.vtl
 │    │   │  ├ get_user.vtl
 │    │   │  ├ query_users_from_email.vtl
 │    │   │  ├ query_users_from_prefecture_prefix
 │    │   │  └ update_user.vtl
 │    │   └ schema.graphql
 │    ├ __init__.py
 │    └ backend_stack.py
 ├ app.py
 └ requirements.txt

スキーマ設計

schemaファイルではUserのレコードを作り、emailやaddressで検索する設計にしました

type User {
  id: String!
  meta: String!
  name: String
  birthdate: String
  gender: String
  email: String
  phone_number: String
  postal_code: String
  address: String
  created_at: AWSTimestamp
  updated_at: AWSTimestamp
  ttl: AWSTimestamp
}

input CreateUserInput {
  name: String
  birthdate: String
  gender: String
  email: String
  phone_number: String
  postal_code: String
  address: String
}

input UpdateUserInput {
  name: String
  birthdate: String
  gender: String
  email: String
  phone_number: String
  postal_code: String
  address: String
  created_at: AWSTimestamp
}

input KeyInput {
  id: String!
  meta: String!
}

input EmailQueryInput {
  email: String!
}

input PrefecturePrefixQueryInput {
  prefecture_prefix: String!
}

type Query {
  getUser(key: KeyInput!): User!
  queryUsersFromEmail(query: EmailQueryInput): [User]
  queryUsersFromPrefecturePrefix(query: PrefecturePrefixQueryInput): [User]
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(key: KeyInput!, input: UpdateUserInput!): User!
  deleteUser(key: KeyInput!): User!
}

スタックファイル

以下がスタック定義のファイルです

テーマから外れるので説明は割愛していますが、 AppSyncのAuthorizationTypeはAuth0のOIDCに設定しています

from aws_cdk import (
    Stack,
    RemovalPolicy,
    aws_dynamodb as dynamodb,
    aws_appsync as appsync,
)
from constructs import Construct
import os

dirname = os.path.dirname(__file__)
backend_dirname = os.path.abspath(os.path.dirname(
    os.path.abspath(__file__)) + "/../../backend/")


class TechChallengeBackendStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        env = os.getenv('ENVIRONMENT', 'dev')

        if env == "dev":  # dev
            apiAuthorizerIssuer = "https://xxxxxxxxxxx.jp.auth0.com/"

        # DynamoDB
        ddb_table = dynamodb.Table(self, "Table",
                                   partition_key=dynamodb.Attribute(
                                       name="id", type=dynamodb.AttributeType.STRING),
                                   sort_key=dynamodb.Attribute(
                                       name="meta", type=dynamodb.AttributeType.STRING),
                                   billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
                                   time_to_live_attribute="ttl",
                                   removal_policy=RemovalPolicy.DESTROY
                                   )
        ddb_table.add_global_secondary_index(
            partition_key=dynamodb.Attribute(
                name="meta", type=dynamodb.AttributeType.STRING),
            sort_key=dynamodb.Attribute(
                name="email", type=dynamodb.AttributeType.STRING),
            index_name="meta-email-idx"
        )

        ddb_table.add_global_secondary_index(
            partition_key=dynamodb.Attribute(
                name="meta", type=dynamodb.AttributeType.STRING),
            sort_key=dynamodb.Attribute(
                name="address", type=dynamodb.AttributeType.STRING),
            index_name="meta-address-idx"
        )

        gq_api = appsync.GraphqlApi(self, "graphqlapi",
                                        name="AppSync",
                                        schema=appsync.SchemaFile.from_asset(
                                            os.path.join(dirname, "appsync/schema.graphql")),
                                        authorization_config=appsync.AuthorizationConfig(
                                            default_authorization=appsync.AuthorizationMode(
                                                authorization_type=appsync.AuthorizationType.OIDC,
                                                open_id_connect_config=appsync.OpenIdConnectConfig(
                                                    oidc_provider=apiAuthorizerIssuer,
                                                )
                                            )
                                        ),
                                        xray_enabled=True)

        ddb_ds = gq_api.add_dynamo_db_data_source("DynamoDBDataSourse", ddb_table)

        ddb_ds.create_resolver("getUserResolver",
            type_name="Query",
            field_name="getUser",
            request_mapping_template=appsync.MappingTemplate.from_file(os.path.join(dirname, "appsync/vtl/get_user.vtl")),
            response_mapping_template=appsync.MappingTemplate.dynamo_db_result_item()
        )

        ddb_ds.create_resolver("queryUsersFromEmailResolver",
            type_name="Query",
            field_name="queryUsersFromEmail",
            request_mapping_template=appsync.MappingTemplate.from_file(os.path.join(dirname, "appsync/vtl/query_users_from_email.vtl")),
            response_mapping_template=appsync.MappingTemplate.dynamo_db_result_list()
        )

        ddb_ds.create_resolver("queryUsersFromPrefecturePrefixResolver",
            type_name="Query",
            field_name="queryUsersFromPrefecturePrefix",
            request_mapping_template=appsync.MappingTemplate.from_file(os.path.join(dirname, "appsync/vtl/query_users_from_prefecture_prefix.vtl")),
            response_mapping_template=appsync.MappingTemplate.dynamo_db_result_list()
        )

        ddb_ds.create_resolver("createUserResolver",
            type_name="Mutation",
            field_name="createUser",
            request_mapping_template=appsync.MappingTemplate.from_file(os.path.join(dirname, "appsync/vtl/create_user.vtl")),
            response_mapping_template=appsync.MappingTemplate.dynamo_db_result_item()
        )

        ddb_ds.create_resolver("updateUserResolver",
            type_name="Mutation",
            field_name="updateUser",
            request_mapping_template=appsync.MappingTemplate.from_file(os.path.join(dirname, "appsync/vtl/update_user.vtl")),
            response_mapping_template=appsync.MappingTemplate.dynamo_db_result_item()
        )

        ddb_ds.create_resolver("deleteUserResolver",
            type_name="Mutation",
            field_name="deleteUser",
            request_mapping_template=appsync.MappingTemplate.from_file(os.path.join(dirname, "appsync/vtl/delete_user.vtl")),
            response_mapping_template=appsync.MappingTemplate.dynamo_db_result_item()
        )

VTLファイル

挙動が確認できたVTLファイルです

PutItem

・新規作成パターン

idに自動でULIDを付与しています

{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key" : {
    "id" : $util.dynamodb.toDynamoDBJson($util.autoUlid()),
    "meta" : $util.dynamodb.toDynamoDBJson("User")
  },
  "attributeValues": $util.dynamodb.toMapValuesJson({
    "name": $ctx.args.input.name,
    "birthdate": $ctx.args.input.birthdate,
    "gender": $ctx.args.input.gender,
    "email": $ctx.args.input.email,
    "phone_number": $ctx.args.input.phone_number,
    "postal_code": $ctx.args.input.postal_code,
    "address": $ctx.args.input.address,
    "created_at": $util.time.nowEpochSeconds()
  })
}

・更新パターン

idは既存のレコードのものを指定して更新処理を行なっています

{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key" : {
    "id" : $util.dynamodb.toDynamoDBJson($ctx.args.key.id),
    "meta" : $util.dynamodb.toDynamoDBJson($ctx.args.key.meta)
  },
  "attributeValues": $util.dynamodb.toMapValuesJson({
    "id": $ctx.args.key.id,
    "meta": $ctx.args.key.meta,
    "name": $ctx.args.input.name,
    "birthdate": $ctx.args.input.birthdate,
    "gender": $ctx.args.input.gender,
    "email": $ctx.args.input.email,
    "phone_number": $ctx.args.input.phone_number,
    "postal_code": $ctx.args.input.postal_code,
    "address": $ctx.args.input.address,
    "created_at": $ctx.args.input.created_at,
    "updated_at": $util.time.nowEpochSeconds()
  })
}

DeleteItem

・レコードの削除

{
  "version": "2017-02-28",
  "operation": "DeleteItem",
  "key" : {
    "id" : $util.dynamodb.toDynamoDBJson($ctx.args.key.id),
    "meta" : $util.dynamodb.toDynamoDBJson($ctx.args.key.meta)
  }
}

GetItem

・レコードの取得

{
  "version": "2017-02-28",
  "operation": "GetItem",
  "key" : {
    "id" : $util.dynamodb.toDynamoDBJson($ctx.args.key.id),
    "meta" : $util.dynamodb.toDynamoDBJson($ctx.args.key.meta)
  }
}

Query

・emailが一致するレコードリストを取得

{
  "version": "2017-02-28",
  "operation": "Query",
  "index": "meta-email-idx",
  "query" : {
    "expression" : "meta=:meta AND email=:email",
    "expressionValues" : {
      ":meta": $util.dynamodb.toDynamoDBJson("User"),
      ":email": $util.dynamodb.toDynamoDBJson($ctx.args.query.email)
    }
  }
}

・addressの前方一致でレコードリストを取得

{
  "version": "2017-02-28",
  "operation": "Query",
  "index": "meta-address-idx",
  "query" : {
    "expression" : "meta=:meta AND begins_with(address, :prefecture_prefix)",
    "expressionValues" : {
      ":meta": $util.dynamodb.toDynamoDBJson("User"),
      ":prefecture_prefix": $util.dynamodb.toDynamoDBJson($ctx.args.query.prefecture_prefix)
    }
  }
}

検証

PostmanやAWSコンソールから以下のクエリで実行します

AWSコンソールでは、

AWS AppSync >> API選択 >> クエリ

と進み、Authorization TokenにAuth0からaccess_tokenを発行して貼り付けます

access_tokenはAuth0コンソール

APIs >> API選択 >> Testタブ

Response項目のaccess_tokenから得ることができます

mutation CreateMutation {
  createUser(
    input: {
      name: "田中 太郎",
      address: "東京都新宿区中落合1-2-3クレオ123",
      birthdate: "1973-04-12",
      email: "tanaka123@example.co.jp",
      gender: "男",
      phone_number: "03-1234-5678",
      postal_code: "123-7934"
    }
  ) {
    address
    birthdate
    created_at
    email
    gender
    id
    meta
    phone_number
    postal_code
  }
}

mutation updateMutation {
  updateUser(
    key: {
      id: "01GR0A6322T20RKAWGXNT3QMBT",
      meta: "User",
    },
    input: {
      name: "田中 太郎",
      address: "東京都新宿区中落合1-2-3クレオ123",
      birthdate: "1973-04-12",
      email: "tanaka123++@example.co.jp",
      gender: "男",
      phone_number: "03-1234-5678",
      postal_code: "123-7934",
      created_at: 1675047930
    }
  ) {
    id
    meta
    address
    birthdate
    email
    gender
    phone_number
    postal_code
    created_at
    updated_at
  }
}

query GetQuery {
  getUser(
    key: {
      id: "01GR0A6322T20RKAWGXNT3QMBT",
      meta: "User",
    }
  ) {
    id
    meta
    address
    birthdate
    email
    gender
    phone_number
    postal_code
    created_at
    updated_at
  }
}

query QueyUsersFromEmail {
  queryUsersFromEmail(
    query: {
      email: "tanaka123++@example.co.jp",
    }
  ) {
    id
    meta
    address
    birthdate
    email
    gender
    phone_number
    postal_code
    created_at
    updated_at
  }
}

query QueryUsersFromPrefecturePrefix {
  queryUsersFromPrefecturePrefix(
    query: {
      prefecture_prefix: "東京都",
    }
  ) {
    id
    meta
    address
    birthdate
    email
    gender
    phone_number
    postal_code
    created_at
    updated_at
  }
}



mutation deleteMutation {
  deleteUser(
    key: {
      id: "01GR0A6322T20RKAWGXNT3QMBT",
      meta: "User",
    }
  ) {
    id
    meta
    address
    birthdate
    email
    gender
    phone_number
    postal_code
    created_at
    updated_at
  }
}

Errorなくレスポンスが返ってくれば成功です

無料相談実施中
AWSを使用したサーバーレスアプリケーションの構築
サーバーレス開発内製化、AWSへの移行等
様々な課題について無料でご相談お受けいたします。
最新情報をチェックしよう!