AWS CDKでAppSyncとDynamoDBの定義
プロジェクトでAWS CDKを活用してawsのサービスを作成する機会がありましたので、その経験談をこの記事に残します。
前提
-
aws configure
を実行していて、awsのユーザー情報をローカル内に存在している
cdk install
cdkをインストールしていない方は以下のコマンドを実行してグローバルインストールしてください。
npm install -g aws-cdk
// 確認のため以下のコマンド実行
cdk --version
2.67.0 (build b6f7f39) // 筆者の場合
cdk init
まず最初にプロジェクトを作成し、cdkの初期設定をします。
mkdir cdk-project
cd cdk-project
cdk init --language typescript
typescript
の部分はプロジェクトで使用される言語を指定してください。
プロジェクトディレクトリの構成
cdk init
を実行することによりディレクトリやファイルが作成されます。作成された内容は以下のような構成です。
- lib/cdk-project-stack.ts CDKアプリケーションのメインスタックが定義されます。CDKで開発するAWSのサービスやサービスの関連性をここで定義します。
- bin/cdk-project.ts CDKアプリケーションのエントリポイントです。lib/cdk-project-stack.tsで定義されたスタックをロードします。スタックを追加する場合はここに追加のスタックを定義する
- package.json npmモジュールのマニフェストです。アプリの名前、バージョン、依存関係、watchやbuild用のビルドスクリプトなどの情報が含まれます(package-lock.jsonはnpmによって管理されます)
- cdk.json アプリの実行方法をツールキットに指示させるためのファイルです。今回の場合は、npx ts-node bin/cdk-workshop.tsです。
- tsconfig.json プロジェクトの TypeScript 設定 です。
- .gitignore & .npmignore Gitとnpm用のファイルです。ソースコードの管理に含める/除外するファイルと、パッケージマネージャーへの公開用設定が含まれています。
-
node_modules npmによって管理され、プロジェクトのすべての依存関係が含まれます。
上記の通り今回触るファイルはlib/cdk-project-stack.tsのみになります。
cdk bootstrap
下記コマンドの実行してcdkアプリケーションをデプロイできる環境をAWS上に作成します。
cdk bootstrap
プロファイルを指定する時には下記のように実行。test-inoue
となっているところは設定されるプロファイ名になります。
cdk bootstrap --profile <プロファイル名>
cdk bootstrapの詳細はAWSの公式ページを参照ください。
cdk deploy
コードが完成したら、下記のコマンドを実行してAWS上にデプロイします。
cdk deploy
上記コマンドだとCDKに定義しているスタックを全てデプロイすることになります。スタック別にデプロイするにはcdk deploy <スタック名>
でスタック別にデプロイできる。
また、プロファイルを指定することも可能。
cdk deploy --profile <プロファイル名>
AppSyncとDynamoDBを作成
先に完成したコードをお見せします。この後に詳しい説明を加えていきます。
import * as cdk from "aws-cdk-lib";
import { ManagedPolicy, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import {
CfnApiKey,
CfnDataSource,
CfnGraphQLApi,
CfnGraphQLSchema,
CfnResolver,
} from "aws-cdk-lib/aws-appsync";
import { schemaDefinition } from "./schema";
import { AttributeType, BillingMode, Table } from "aws-cdk-lib/aws-dynamodb";
export class CdkProjectStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// DynamoDBの定義
const targetTable = new Table(this, 'cdkProjectTable', {
tableName: "cdkProjectTable", // テーブル名の定義
partitionKey: { //パーティションキーの定義
name: 'id',
type: AttributeType.STRING, // typeはあとNumberとbinary
},
sortKey: { // ソートキーの定義
name: 'name',
type: AttributeType.STRING,
},
billingMode: BillingMode.PAY_PER_REQUEST, // オンデマンド請求
pointInTimeRecovery: true, // PITRを有効化
timeToLiveAttribute: 'expired', // TTLの設定
removalPolicy: cdk.RemovalPolicy.DESTROY, // cdk destroyでDB削除可
});
// AppSyncの定義
const graphQLApi = new CfnGraphQLApi(this, "cdkProjectApi", {
name: "cdkProjectApi",
authenticationType: "API_KEY",
xrayEnabled: false,
});
new CfnApiKey(this, "cdkProjectApiKey", {
apiId: graphQLApi.attrApiId,
});
// AppSyncのRole定義(tableDataBaseの定義に利用)
const apiRole = new Role(this, "cdkProjectTableLoadApiRole", {
assumedBy: new ServicePrincipal("appsync.amazonaws.com"),
});
apiRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName("AmazonDynamoDBFullAccess") // DynamoDBの全権限
);
// Schemaの定義
const apiSchema = new CfnGraphQLSchema(this, "cdkProjectTableSchema", {
apiId: graphQLApi.attrApiId,
definition: schemaDefinition,
});
// DataBaseの定義
const dataSource = new CfnDataSource(this, "cdkProjectTableDataSource", {
apiId: graphQLApi.attrApiId,
name: "cdkProjectTableDynamoDataSource",
type: "AMAZON_DYNAMODB",
dynamoDbConfig: {
tableName: targetTable.tableName,
awsRegion: this.region,
},
serviceRoleArn: apiRole.roleArn,
});
// Resolverの定義
const getOneResolver = new CfnResolver(this, "GetOneQueryResolver", {
apiId: graphQLApi.attrApiId,
typeName: "Query",
fieldName: "getOne",
dataSourceName: dataSource.name,
requestMappingTemplate: `{
"version": "2018-05-29",
"operation": "GetItem",
"key" : {
"id" : $util.dynamodb.toDynamoDBJson($ctx.args.id),
"name" : $util.dynamodb.toDynamoDBJson($ctx.args.name)
}
}`,
responseMappingTemplate: `#if ( $ctx.error )
$util.error($ctx.error.message, $ctx.error.type)
#end
$util.toJson($ctx.result)`,
});
getOneResolver.addDependency(apiSchema);
const getAllResolver = new CfnResolver(this, "GetAllQueryResolver", {
apiId: graphQLApi.attrApiId,
typeName: "Query",
fieldName: "getList",
dataSourceName: dataSource.name,
requestMappingTemplate: `{
"version": "2018-05-29",
"operation": "Scan",
"limit": $util.defaultIfNull($ctx.args.limit, 20),
"nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))
}`,
responseMappingTemplate: `$util.toJson($ctx.result)`,
});
getAllResolver.addDependency(apiSchema);
const createResolver = new CfnResolver(this, "CreateMutationResolver", {
apiId: graphQLApi.attrApiId,
typeName: "Mutation",
fieldName: "create",
dataSourceName: dataSource.name,
requestMappingTemplate: `{
"version": "2018-05-29",
"operation": "PutItem",
"key" : {
"id" : $util.dynamodb.toDynamoDBJson($ctx.args.id),
"name" : $util.dynamodb.toDynamoDBJson($ctx.args.name)
},
"attributeValues": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
"name": $util.dynamodb.toDynamoDBJson($ctx.args.name)
}
}`,
responseMappingTemplate: `$util.toJson($ctx.result)`,
});
createResolver.addDependency(apiSchema);
const deleteResolver = new CfnResolver(this, "DeleteMutationResolver", {
apiId: graphQLApi.attrApiId,
typeName: "Mutation",
fieldName: "delete",
dataSourceName: dataSource.name,
requestMappingTemplate: `{
"version": "2018-05-29",
"operation": "DeleteItem",
"key" : {
"id" : $util.dynamodb.toDynamoDBJson($ctx.args.id),
"name" : $util.dynamodb.toDynamoDBJson($ctx.args.name)
},
}`,
responseMappingTemplate: `$util.toJson($ctx.result)`,
});
deleteResolver.addDependency(apiSchema);
}:
};
export const schemaDefinition = `
type cdkProjectTable {
id: String!
name: String!
value: String
}
type PaginatedcdkProjectTable {
items: [cdkProjectTable!]!
nextToken: String
}
type Query {
getList(limit: Int, nextToken: String): PaginatedcdkProjectTable!
getOne(id: String!, name: String!): cdkProjectTable!
}
type Mutation {
create(id: String!, name: String!): cdkProjectTable
delete(id: String!, name: String!): cdkProjectTable
}`;
DynamoDBの定義
今回CDKで定義するのはタイトル通り、AppSyncとDynamoDBの定義になります。DynamoDBは下記のように定義します。
import * as cdk from "aws-cdk-lib";
import { AttributeType, BillingMode, Table } from "aws-cdk-lib/aws-dynamodb";
export class CdkProjectStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const targetTable = new Table(this, 'Sample-table', { // 'Sample-table'はStack内で一意
tableName: "sample-table", // テーブル名の定義
partitionKey: {
name: 'id',
type: AttributeType.STRING,
},
sortKey: {
name: 'name',
type: AttributeType.STRING,
},
billingMode: BillingMode.PAY_PER_REQUEST,
pointInTimeRecovery: true,
timeToLiveAttribute: 'expired',
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}
各プロパティの説明
-
partitionKey
&sortKey
: このプロパティでそれぞれプライマリキーとソートキーを定義しています。また、プロパティ内のtype
にはNUMBERとBINARYを設定できる。 -
billingMode
: 読み込み/書き込みキャパシティーの設定。-
BillingMode.PAY_PER_REQUEST
で「オンデマンド」 -
BillingMode.PROVISIONED
で「プロビジョニング」
-
-
pointInTimeRecovery
: PITRを有効化/無効化の設定 -
timeToLiveAttribute
: TTLの設定で、対象項目名を指定。エポック時間で削除時間を指定します。デフォルトは設定無し。 -
removalPolicy
:cdk destroy
コマンドでDB削除可能にするプロパティ
上記で定義したDynamoDBをAppSyncと紐付けして定義していく。
ちなみに既存のDynamoDBを取得し場合は下記のようにする。(説明はコード内のコメントで)
const targetTable = Table.fromTableArn(
this, // scope範囲
"projectNameTable", // テーブル名
"arn:aws:dynamodb:ap-northeast-1:xxxxxxxxx:table/projectNameTable" // テーブルARN
);
AppSyncの定義
AppSyncの定義は大きく分けて、「AppSync自体の定義」、「Roleの定義」、「Schemaの定義」、「DataBaseの定義」、「Resolverの定義」の5つになります。
AppSync自体の定義
const graphQLApi = new CfnGraphQLApi(this, "cdkProjectApi", {
name: "cdkProjectApi",
authenticationType: "API_KEY",
xrayEnabled: false,
});
new CfnApiKey(this, "cdkProjectApiKey", {
apiId: graphQLApi.attrApiId,
});
new CfnGraphQLApi
でAppSyncの定義をしていきます。
各プロパティの説明
-
name
: AppSyncの名前を定義。 -
authenticationType
: 認証の指定。例だと"API_KEY"
でCognitoを指定する場合は"AMAZON_COGNITO_USER_POOLS"
でユーザープールを指定する。 -
xrayEnabled
: GraphqlApiにAWS X-Ray トレーシングを使用するかどうかを示すプロパティ。
また、認証用のAPI_Keyはnew CfnApiKey
の部分で定義しています。apiId
プロパティで定義したAppSyncのApiIdを指定する。
Roleの定義
定義したAppSyncにアタッチするRoleを作成します。
const apiRole = new Role(this, "cdkProjectTableLoadApiRole", {
assumedBy: new ServicePrincipal("appsync.amazonaws.com"),
});
apiRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName("AmazonDynamoDBFullAccess")
);
今回のAppSync/DataBaseの作成とは若干違うので簡単に説明すると
new Role
でロールの作成をして、プリンシパルとして「AppSync」を指定しています。
apiRole.addManagedPolicy
で作成したロールにポリシーをアタッチしています。今回は大雑把に"AmazonDynamoDBFullAccess"
(DynamoDBの全権限)を指定しています。(.addManagedPolicy
だとAWSがデフォルトであるポリシーをアタッチするようになります。詳細な権限を指定したい場合はnew Policy
でポリシーを作成する必要がある)
Schemaの定義
const apiSchema = new CfnGraphQLSchema(this, "cdkProjectTableSchema", {
apiId: graphQLApi.attrApiId,
definition: schemaDefinition,
});
Schemaの定義はnew CfnGraphQLSchema
で定義します。
各プロパティの説明
-
apiId
: AppSyncとの紐付け部分。先ほど定義したAppSyncのApiIdを指定する。 -
definition
: スキーマの定義を行っている。ここではschemaDefinition
という定数をしてしています。これはjs:lib/schema.ts
のファイルからインポートしていますので、コードは参考にしてください。
スキーマのコードは以下の通りです。
export const schemaDefinition = `
type cdkProjectTable {
id: String!
name: String!
value: String
}
type PaginatedcdkProjectTable {
items: [cdkProjectTable!]!
nextToken: String
}
type Query {
getList(limit: Int, nextToken: String): PaginatedcdkProjectTable!
getOne(id: String!, name: String!): cdkProjectTable!
}
type Mutation {
create(id: String!, name: String!): cdkProjectTable
delete(id: String!, name: String!): cdkProjectTable
}`;
-
type cdkProjectTable
でテーブルのattributesを定義しています。 -
type Query
でクエリを定義。クエリは検索関連のデータ操作を定義しています。 -
type Mutation
でミューテーションの定義になります。ミューテーションはcreateやupdate, deleteなどのデータ操作を定義しています。
DataBaseの定義
const dataSource = new CfnDataSource(this, "cdkProjectTableDataSource", {
apiId: graphQLApi.attrApiId,
name: "cdkProjectTableDynamoDataSource",
type: "AMAZON_DYNAMODB",
dynamoDbConfig: {
tableName: targetTable.tableName,
awsRegion: this.region,
},
serviceRoleArn: apiRole.roleArn,
});
new CfnDataSource
でAppSyncのデータベースの定義をしています。ここでAppSynctと最初に定義したDynamoDBとの紐付けをしています。
各プロパティの説明
-
apiId
: 紐付けするAppSyncのApiIdを指定。 -
name
: AppSync上でデータソースを識別するための名前。 -
type
: データソースの定義。今回はDynamoDBとの連携のため"AMAZON_DYNAMODB"
を指定。他にも"AMAZON_EVENTBRIDGE"
や"AWS_LAMBDA"
なども指定できる。 -
dynamoDbConfig
: 紐付けするDyanmoDBを指定。tableName
でテーブル名。awsRegion
で紐づけるDynamoDBがあるリージョンをしていする。 -
serviceRoleArn
: データソースにアクセスする際の権限を指定する。type
が"AMAZON_DYNAMODB"
を指定してる場合は必須。
Resolverの定義
Resolverはスキーマで定義してる内容のリクエストテンプレートやレスポンステンプレートを定義している。要はAPIのリクエストとレスポンスを定義。今回はコードが長いのでgetOne
クエリとcreate
ミューテーションを抜粋して説明します。
const getOneResolver = new CfnResolver(this, "GetOneQueryResolver", {
apiId: graphQLApi.attrApiId,
typeName: "Query",
fieldName: "getOne",
dataSourceName: dataSource.name,
requestMappingTemplate: `{
"version": "2018-05-29",
"operation": "GetItem",
"key" : {
"id" : $util.dynamodb.toDynamoDBJson($ctx.args.id),
"name" : $util.dynamodb.toDynamoDBJson($ctx.args.name)
}
}`,
responseMappingTemplate: `#if ( $ctx.error )
$util.error($ctx.error.message, $ctx.error.type)
#end
$util.toJson($ctx.result)`,
});
getOneResolver.addDependency(apiSchema);
const createResolver = new CfnResolver(this, "CreateMutationResolver", {
apiId: graphQLApi.attrApiId,
typeName: "Mutation",
fieldName: "create",
dataSourceName: dataSource.name,
requestMappingTemplate: `{
"version": "2018-05-29",
"operation": "PutItem",
"key" : {
"id" : $util.dynamodb.toDynamoDBJson($ctx.args.id),
"name" : $util.dynamodb.toDynamoDBJson($ctx.args.name)
},
"attributeValues": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
"name": $util.dynamodb.toDynamoDBJson($ctx.args.name)
}
}`,
responseMappingTemplate: `$util.toJson($ctx.result)`,
});
createResolver.addDependency(apiSchema);
new CfnResolver
でリゾルバを定義しています。
各プロパティの説明
-
apiId
: 紐付けするAppSyncのApiIdを指定。 -
typeName
: ここでGraphQLのタイプをしてします。("Query"
or"Mutation"
or"Subscription"
) -
fieldName
: スキーマに定義したフィールド名を指定する。 -
dataSourceName
: リゾルバを実行するデータソースの指定。先ほど定義したデータソースの名前を指定してます。 -
requestMappingTemplate
: リクエスト マッピング テンプレートの定義。この部分はcdkというよりもAppSyncのマッピングテンプレートの仕組み。
"id": $util.dynamodb.toDynamoDBJson($ctx.args.${tableName}Id)
の定義により、AppSyncのパラメータとして渡されたデータを取得することができる。また、DynamoDBの注意点として、ソートキーを定義している場合はこのリクエストテンプレートにソートキーも追加しないといけない。
そして、作成したリゾルバをスキーマと紐付けするため、.addDependency(apiSchema)
メソッドで先ほど定義したスキーマ情報と紐付けする。
最後に
冒頭のコマンド紹介でもお伝えした通り、上記を定義したらcdk deploy
を実施して、AWS上に反映させてようやく活用することができます。
今回CDKでAppSuncとDynamoDBを作成しましたが、正直複雑なQueryやMutationを作成しない場合はAmplify cliで作成した方がDynamoDBも自動的に作成してくれるのでそちらを活用した方がいいかなと思いました。Amplify cliで作成したDynamoDBをCDKで活用したい場合はインポートすることで利用できるので、Amplify cliとCDKをうまく連携して一つのアプリケーションを作成しても良いかもしれません。
参考:
・ https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_appsync-readme.html
・ https://qiita.com/sugimount-a/items/3c1bd1a47c37f3fe3425
・ https://qiita.com/yamato1491038/items/f388afa3aa4f701321f5
Discussion