AWS CDKでAppSyncを扱う
Amplify
ではなくAWS CDK
でAppSync
を試したのでメモ
準備
- cdk のプロジェクトを作成
npx cdk init app --language typescript
front 側の実装はこの記事の趣旨ではないので簡単に書いておきます。
Next.js
のプロジェクトをfrontend
というフォルダに作成
npx create-next-app@latest frontend --typescript
適当に Chakra UI を入れておきます
yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion
あとは参考記事を見ながら、graphql
のコード生成や@apollo/client
などをインストールしてコード生成して適当に CRUD します(適当)
yarn add -D graphql @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo @graphql-codegen/named-operations-object
yarn add @apollo/client
-
参考
-
以下がコード
CDK 側
本題の CDK の解説です。
全 CDK のコード
import * as cdk from "aws-cdk-lib";
import * as appsync from "aws-cdk-lib/aws-appsync";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";
import path = require("path");
export class AppsyncCdkSampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const api = new appsync.GraphqlApi(this, "Api", {
name: "Todo",
schema: appsync.SchemaFile.fromAsset(
path.join(__dirname, "../schemas/schema.graphql")
),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
},
},
xrayEnabled: true,
});
const table = new dynamodb.Table(this, "TodoTable", {
tableName: "todo-table",
partitionKey: {
name: "id",
type: dynamodb.AttributeType.STRING,
},
});
const dataSource = api.addDynamoDbDataSource("DynamoDataSource", table);
// query
dataSource.createResolver("getTodosResolver", {
typeName: "Query",
fieldName: "getAll",
requestMappingTemplate: appsync.MappingTemplate.dynamoDbScanTable(),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(),
});
dataSource.createResolver("getTodoResolver", {
typeName: "Query",
fieldName: "get",
requestMappingTemplate: appsync.MappingTemplate.dynamoDbGetItem(
"id",
"id"
),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});
// mutation
dataSource.createResolver("MutationResolver", {
typeName: "Mutation",
fieldName: "add",
requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem(
appsync.PrimaryKey.partition("id").auto(),
appsync.Values.projecting("input")
),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});
dataSource.createResolver("Delete", {
typeName: "Mutation",
fieldName: "delete",
requestMappingTemplate: appsync.MappingTemplate.dynamoDbDeleteItem(
"id",
"id"
),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});
const updateFunction = new nodejs.NodejsFunction(this, "UpdateFunction", {
functionName: "update-function",
entry: path.join(__dirname, "../lambda/update.ts"),
runtime: lambda.Runtime.NODEJS_18_X,
tracing: lambda.Tracing.ACTIVE,
environment: {
TABLE_NAME: table.tableName,
},
});
table.grantReadWriteData(updateFunction);
const lambdaDataSource = api.addLambdaDataSource(
"LambdaDataSource",
updateFunction
);
lambdaDataSource.createResolver("Update", {
typeName: "Mutation",
fieldName: "update",
});
}
}
まず、今回のサンプルアプリのスキーマは以下のような感じです。Todo アプリです
type Todo {
id: ID!
title: String!
detail: String
isDone: Boolean!
}
type Query {
getAll: [Todo!]
get(id: ID!): Todo!
}
input TodoInput {
title: String!
isDone: Boolean!
detail: String
}
input UpdateInput {
id: ID!
title: String
isDone: Boolean
detail: String
}
type Mutation {
add(input: TodoInput!): Todo
update(input: UpdateInput!): Todo
delete(id: ID!): Todo
}
AppSync
の公式の書き方ほぼまんまですが、API 作成自体は簡単です。
aws-cdk-lib.aws_appsync module · AWS CDK
import * as appsync from "aws-cdk-lib/aws-appsync";
const api = new appsync.GraphqlApi(this, "Api", {
name: "Todo",
schema: appsync.SchemaFile.fromAsset(
path.join(__dirname, "../schemas/schema.graphql")
),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
},
},
xrayEnabled: true,
});
今回は簡単のために認証はAPI_KEY
にしていますが、Cognito や IAM 認証、OpenID Connect、Lambda 認証などが使えるようです。
承認と認証 - AWS AppSync
続いてDynamoDB
を作成して紐付けのための DataSource を定義します。
const table = new dynamodb.Table(this, "TodoTable", {
tableName: "todo-table",
partitionKey: {
name: "id",
type: dynamodb.AttributeType.STRING,
},
});
const dataSource = api.addDynamoDbDataSource("DynamoDataSource", table);
こちらも特に難しいことはないですが、データソースには他にRedis
、RDS
、Lambda
などが使えます。
そして、schema.graphql
に従ってResolver
を定義していきます。
// query
dataSource.createResolver("getTodosResolver", {
typeName: "Query",
fieldName: "getAll",
requestMappingTemplate: appsync.MappingTemplate.dynamoDbScanTable(),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(),
});
dataSource.createResolver("getTodoResolver", {
typeName: "Query",
fieldName: "get",
requestMappingTemplate: appsync.MappingTemplate.dynamoDbGetItem(
"id",
"id"
),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});
typeName
にはQuery
かMutation
かSubscribe
を記載します。fieldName
にはschema.graphql
で定義したQuery
やMutation
のフィールドを記載します。
DynamoDB
であればappsync.MappingTemplate
に定義されているdynamoDbScanTable
やdynamoDbResultList
を使うと直感的にマッピングすることができます。
Mutation
の方はこんな感じ
// mutation
dataSource.createResolver("MutationResolver", {
typeName: "Mutation",
fieldName: "add",
requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem(
appsync.PrimaryKey.partition("id").auto(),
appsync.Values.projecting("input")
),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});
dataSource.createResolver("Delete", {
typeName: "Mutation",
fieldName: "delete",
requestMappingTemplate: appsync.MappingTemplate.dynamoDbDeleteItem(
"id",
"id"
),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});
dynamoDbPutItem
では
appsync.PrimaryKey.partition("id").auto(),
appsync.Values.projecting("input")
を渡してやることで、id
を自動で付与しながら他の受け取った値を渡す事ができます。
最後にUpdate
ですが、マッピングテンプレートに update のものがなかったので、これだけ Lambda にしました。
const updateFunction = new nodejs.NodejsFunction(this, "UpdateFunction", {
functionName: "update-function",
entry: path.join(__dirname, "../lambda/update.ts"),
runtime: lambda.Runtime.NODEJS_18_X,
tracing: lambda.Tracing.ACTIVE,
environment: {
TABLE_NAME: table.tableName,
},
});
table.grantReadWriteData(updateFunction);
const lambdaDataSource = api.addLambdaDataSource(
"LambdaDataSource",
updateFunction
);
lambdaDataSource.createResolver("Update", {
typeName: "Mutation",
fieldName: "update",
});
lambda では普通にDynamoDB
のUpdateCommand
を呼んで値を更新しています。
lambda のコードはこちら
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
UpdateCommand,
UpdateCommandInput,
} from "@aws-sdk/lib-dynamodb";
import { AppSyncResolverHandler } from "aws-lambda";
import {
MutationUpdateArgs,
Todo,
} from "../frontend/src/types/generated/generated-types";
const db = new DynamoDBClient({});
const ddb = DynamoDBDocumentClient.from(db);
const TABLE_NAME = process.env.TABLE_NAME || "";
export const handler: AppSyncResolverHandler<
MutationUpdateArgs,
Todo | null
> = async (event) => {
const todo = event.arguments.input;
console.log(todo);
const params: UpdateCommandInput = {
TableName: TABLE_NAME,
Key: {
id: todo.id,
},
UpdateExpression: "set isDone = :c",
ExpressionAttributeValues: {
":c": todo.isDone,
},
};
console.log(params);
const data = await ddb.send(new UpdateCommand(params));
return data.Attributes as Todo;
};
デプロイしてAppSync
のコンソールからクエリを実行することができます。
スキーマ定義も見ることができます。
つまったポイント
-
createResolver
の中であとから Construct のid
を変えて同じ fieldName でデプロイしようとすると以下のようなエラーが出る
failed: Error: The stack named AppsyncCdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Only one resolver is allowed per field.
この辺は CDK 側でなんとかして欲しい気もするが、一度Resolver
をコメントアウトしてデプロイした後、再度コメントアウトを外してデプロイするなどすれば回避可能。
まとめ
Next.js
でフロントを作る際の BFF としてAppSync
を使うのが良さそうだと言うことで、さらっと一通り試してみました。
あまり凝ったことをしないのであればAmplify
を使えば良い気がしますが、AWS CDK
でも割と簡単にかけたので、このまま CDK で運用したいなと思います。
ゆくゆくはNx
のワークスペースに組み込みたいので、schema
の置き場所とコード生成については別で完結させたいなと思っています 🤔
Discussion