DynamoDBにアクセスするAppSyncをCloudFormationで作る
目次
はじめに
クラウドビルダーズのhijikuroです。
この記事はクラウドビルダーズ Advent Calendar 2024の15日目の記事です。
クラウドビルダーズのメンバーでチャレンジしているカレンダーとなっております。
他の記事も是非チェックしてみてください!
プロジェクトでAppSyncを使っていたのですが、ずっとマネジメントコンソールで構築していて、結局IaC化できずでした。ということで、反省を兼ねて記事にしました。
マネジメントコンソールで構築するパターンとCloudFormationで構築するパターンでやってみました。
やってみる
事前準備
DynamoDBを下記の設定で作成します。(AppSyncコンソールで一緒に作ることもできます。)
マネジメントコンソールでAppSyncを作る
AppSyncのコンソールから「APIを作成」をクリックし、「GraphQL API」を選択します。
「GraphQL API データソース」の「DynamoDBテーブルからはじめる」をチェックし次へ。
API名を入力し、作成済みのDynamoDBのリージョン、テーブル名を選択し次へ進みます。
「モデル名」を入力します。ここで入力した名前はAPIの名称に含まれるので、「ToDo」としておきます。
フィールドで「id」のタイプを「ID」にしておきます。
その他の項目はすべて「String」で追加します。
内容に問題なければ、確認画面へ進み、そのまま作成します。
マネジメントコンソールで構築するとよくわからないのですが、この時にスキーマやリゾルバーも作成してくれてます。
APIが作成されたら、コンソールのクエリ画面からテスト実行してみます。
「実行する」ボタンを押して、「createToDo」を選択すると、クエリが実行されます。
(DynamoDB側にもデータが入っているはずです。)
余談
複数回AppSyncを構築する場合、項目を手動で追加する手順がしんどかったです。入力を間違えるリスクもあります…
また、データソースを追加する場合は、下記画像のように直接入力できるので少し楽になるのですが、それでもデータソースが多いとマネジメントコンソールでは設定に時間がかかります。
CloudFormationでAppSyncを作る
次にCloudFormationでAppSyncを作ってみます。
使用したテンプレートは下記です。長いです。
スキーマと各リゾルバーはマネジメントコンソールで構築されたものから抜き出しました。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
TableName:
Description: dynamoDB table name
Type: String
Default: ToDoTable
Resources:
GraphQLApi:
Type: AWS::AppSync::GraphQLApi
Properties:
AuthenticationType: API_KEY
Name: ToDo API v2
ApiKey:
Type: AWS::AppSync::ApiKey
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
Expires: 1734447600 # UNIX時間で有効期限を設定
AppSyncRole:
Type: AWS::IAM::Role
Properties:
RoleName: ToDoAPIv2Role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- appsync.amazonaws.com
Path: '/'
Policies:
- PolicyName: ToDoAPIv2Policy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:DeleteItem
- dynamodb:GetItem
- dynamodb:Scan
- dynamodb:Query
- dynamodb:UpdateItem
Resource:
- !Sub 'arn:aws:dynamodb:ap-northeast-1:${AWS::AccountId}:table/${TableName}'
- !Sub 'arn:aws:dynamodb:ap-northeast-1:${AWS::AccountId}:table/${TableName}/*'
DataSource:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
Name: !Sub ${TableName}
Type: AMAZON_DYNAMODB
ServiceRoleArn: !GetAtt AppSyncRole.Arn
DynamoDBConfig:
AwsRegion: ap-northeast-1
TableName: !Sub ${TableName}
GraphQLSchema:
Type: AWS::AppSync::GraphQLSchema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
Definition: |
input CreateToDoInput {
name: String
when: String
where: String
description: String
}
input DeleteToDoInput {
id: ID!
}
input ModelSizeInput {
ne: Int
eq: Int
le: Int
lt: Int
ge: Int
gt: Int
between: [Int]
}
input TableBooleanFilterInput {
ne: Boolean
eq: Boolean
attributeExists: Boolean
}
input TableFloatFilterInput {
ne: Float
eq: Float
le: Float
lt: Float
ge: Float
gt: Float
between: [Float]
attributeExists: Boolean
}
input TableIDFilterInput {
ne: ID
eq: ID
le: ID
lt: ID
ge: ID
gt: ID
contains: ID
notContains: ID
between: [ID]
beginsWith: ID
attributeExists: Boolean
size: ModelSizeInput
}
input TableIntFilterInput {
ne: Int
eq: Int
le: Int
lt: Int
ge: Int
gt: Int
between: [Int]
attributeExists: Boolean
}
input TableStringFilterInput {
ne: String
eq: String
le: String
lt: String
ge: String
gt: String
contains: String
notContains: String
between: [String]
beginsWith: String
attributeExists: Boolean
size: ModelSizeInput
}
input TableToDoFilterInput {
id: TableIDFilterInput
name: TableStringFilterInput
when: TableStringFilterInput
where: TableStringFilterInput
description: TableStringFilterInput
}
type ToDo {
id: ID!
name: String
when: String
where: String
description: String
}
type ToDoConnection {
items: [ToDo]
nextToken: String
}
input UpdateToDoInput {
id: ID!
name: String
when: String
where: String
description: String
}
type Mutation {
createToDo(input: CreateToDoInput!): ToDo
updateToDo(input: UpdateToDoInput!): ToDo
deleteToDo(input: DeleteToDoInput!): ToDo
}
type Query {
getToDo(id: ID!): ToDo
listToDos(filter: TableToDoFilterInput, limit: Int, nextToken: String): ToDoConnection
}
type Subscription {
onCreateToDo(
id: ID,
name: String,
when: String,
where: String,
description: String
): ToDo
@aws_subscribe(mutations: ["createToDo"])
onUpdateToDo(
id: ID,
name: String,
when: String,
where: String,
description: String
): ToDo
@aws_subscribe(mutations: ["updateToDo"])
onDeleteToDo(
id: ID,
name: String,
when: String,
where: String,
description: String
): ToDo
@aws_subscribe(mutations: ["deleteToDo"])
}
ResolverGetToDo:
Type: AWS::AppSync::Resolver
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
DataSourceName: !GetAtt DataSource.Name
TypeName: 'Query'
FieldName: 'getToDo'
Kind: UNIT
Runtime:
Name: APPSYNC_JS
RuntimeVersion: 1.0.0
Code: |
import { util } from '@aws-appsync/utils';
import { get } from '@aws-appsync/utils/dynamodb';
export function request(ctx) {
const { id } = ctx.args;
const key = { id };
return get({
key,
})
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
return util.appendError(error.message, error.type, result);
}
return result;
}
ResolverListToDos:
Type: AWS::AppSync::Resolver
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
DataSourceName: !GetAtt DataSource.Name
TypeName: 'Query'
FieldName: 'listToDos'
Kind: UNIT
Runtime:
Name: APPSYNC_JS
RuntimeVersion: 1.0.0
Code: |
import { util } from '@aws-appsync/utils';
import { scan } from '@aws-appsync/utils/dynamodb';
export function request(ctx) {
const { filter, limit, nextToken } = ctx.args;
return scan({
limit,
nextToken,
filter,
})
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
return util.appendError(error.message, error.type, result);
}
const { items = [], nextToken } = result;
return { items, nextToken };
}
ResolverCreateToDo:
Type: AWS::AppSync::Resolver
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
DataSourceName: !GetAtt DataSource.Name
TypeName: 'Mutation'
FieldName: 'createToDo'
Kind: UNIT
Runtime:
Name: APPSYNC_JS
RuntimeVersion: 1.0.0
Code: |
import { util } from '@aws-appsync/utils';
import { put } from '@aws-appsync/utils/dynamodb';
export function request(ctx) {
const { input: values } = ctx.args;
const key = { id: util.autoId() };
const condition = { and: [{ id: { attributeExists: false } }] };
return put({
key,
item: ctx.args.input,
condition,
})
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
return util.appendError(error.message, error.type, result);
}
return result;
}
ResolverUpdateToDo:
Type: AWS::AppSync::Resolver
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
DataSourceName: !GetAtt DataSource.Name
TypeName: 'Mutation'
FieldName: 'updateToDo'
Kind: UNIT
Runtime:
Name: APPSYNC_JS
RuntimeVersion: 1.0.0
Code: |
import { util } from '@aws-appsync/utils';
import { update } from '@aws-appsync/utils/dynamodb';
export function request(ctx) {
const { id, ...values } = ctx.args.input;
const key = { id };
const condition = {};
for (const k in key) {
condition[k] = { attributeExists: true };
}
return update({
key,
update: values,
condition,
})
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
return util.appendError(error.message, error.type, result);
}
return result;
}
ResolverDeleteToDo:
Type: AWS::AppSync::Resolver
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
DataSourceName: !GetAtt DataSource.Name
TypeName: 'Mutation'
FieldName: 'deleteToDo'
Kind: UNIT
Runtime:
Name: APPSYNC_JS
RuntimeVersion: 1.0.0
Code: |
import { util } from '@aws-appsync/utils';
import { remove } from '@aws-appsync/utils/dynamodb';
export function request(ctx) {
const { id } = ctx.args.input;
const key = { id };
return remove({
key,
})
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
return util.appendError(error.message, error.type, result);
}
return result;
}
上記でAppSyncを構築後、AppSyncコンソールからlistクエリを実行できました。
おわりに
実際にCloudFormationでAppSyncを構築してみての感想ですが、どうしてもスキーマやリゾルバーの記述でコードが長くなってしまいます。「CodeS3Location」というパラメータでS3に置いたコードのファイルを指定することもできますが、S3にアップロードしてテンプレートと別管理というのも微妙かなと思います。ですので、結局CDKを使うのが一番楽な気がします。参考ドキュメント
AppSyncをはじめて触るような場合はスキーマもリゾルバーも書き方がよく分からないと思うので、まずはマネジメントコンソールで構築するのはおすすめです。ただし、設定内容が決まってきたら、IaC化なりCLIで手順を用意するなりしたほうが良いと思います。(でないと、移行する時に構築が大変…)
AppSync自体はDynamoDBだけでなく、RDSだったりLambdaだったり色々なものと接続でき、使いこなせればとても便利だと思うので、引き続きキャッチアップしていきたいです。(最近出たAppSync Event APIも試してみたい。)