テーマ
今回は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なくレスポンスが返ってくれば成功です