🔗

AWS CDKでAppSyncとDynamoDBの定義

2023/04/09に公開

プロジェクトで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モジュールのマニフェストです。アプリの名前、バージョン、依存関係、watchbuild用のビルドスクリプトなどの情報が含まれます(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の公式ページを参照ください。
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping.html

cdk deploy

コードが完成したら、下記のコマンドを実行してAWS上にデプロイします。

cdk deploy

上記コマンドだとCDKに定義しているスタックを全てデプロイすることになります。スタック別にデプロイするにはcdk deploy <スタック名>でスタック別にデプロイできる。
また、プロファイルを指定することも可能。

cdk deploy --profile <プロファイル名>

AppSyncとDynamoDBを作成

先に完成したコードをお見せします。この後に詳しい説明を加えていきます。

lib/project-stack.ts
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);
  }:
};
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
  }`;

DynamoDBの定義

今回CDKで定義するのはタイトル通り、AppSyncとDynamoDBの定義になります。DynamoDBは下記のように定義します。

lib/cdk-project-stack.ts
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を取得し場合は下記のようにする。(説明はコード内のコメントで)

lib/project-stack.ts
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自体の定義

lib/project-stack.ts
    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を作成します。

lib/project-stack.ts
    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の定義

lib/project-stack.ts
    const apiSchema = new CfnGraphQLSchema(this, "cdkProjectTableSchema", {
      apiId: graphQLApi.attrApiId,
      definition: schemaDefinition,
    });

Schemaの定義はnew CfnGraphQLSchemaで定義します。
各プロパティの説明

  • apiId: AppSyncとの紐付け部分。先ほど定義したAppSyncのApiIdを指定する。
  • definition: スキーマの定義を行っている。ここではschemaDefinitionという定数をしてしています。これはjs:lib/schema.tsのファイルからインポートしていますので、コードは参考にしてください。
    スキーマのコードは以下の通りです。
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の定義

lib/project-stack.ts
    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ミューテーションを抜粋して説明します。

lib/project-stack.ts
    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