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

#AWS AppSync
#CDK
#Python
#VTL

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

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

PythonやTypeScriptなどのプログラミング言語を使用して、AWSリソースを定義するツール CloudFormationのテンプレートをプログラミング言語で出力することが出来る

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

アーキテクチャ図1

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で検索する設計にしました

schema.graphql

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に設定しています

backend_stack.py

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ファイルです

・新規作成パターン idに自動でULIDを付与しています

create_user.vtl

{ "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は既存のレコードのものを指定して更新処理を行なっています

update_user.vtl

{ "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() }) }

・レコードの削除

delete_user.vtl

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

・レコードの取得

get_user.vtl

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

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

query_users_from_email.vtl

{ "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の前方一致でレコードリストを取得

query_users_from_prefecture_prefix.vtl

{ "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なくレスポンスが返ってくれば成功です