LoginSignup
4
1

More than 1 year has passed since last update.

AWS CDK v2 で AppSync を作ってみた

Last updated at Posted at 2022-10-29

はじめに

AWS 上のリソースを IaC として定義して、自動デプロイをやりやすくする CDK (AWS Cloud Development Kit) があります。CDK をそれほど触ったことがなかったので、今回は AppSync DynamoDB を CDK でデプロイしてみます。

また、CDK は、CDK v1 と CDK v2 の 2 種類があります。CDK v1 のサポートは、2023 年 6月に終了する予定です。なので、CDK v2 を使ったデプロイ方法を整理していきます。

CDK Install

まず初めに、CDK をインストールしないといけません。Node.js のバージョン 16 を nvm を使ってインストールしたうえで、aws-cdk をインストールしています。

nvm use v16.18.0
npm install -g aws-cdk

 

CDK のバージョンを確認します。この記事の時点で最新の 2.49.0 がインストールされました。

> cdk --version
2.49.0 (build 793dd76)

CDK のプロジェクト作成

手元の環境で、CDK のプロジェクトを作成します。インターネット上で情報が豊富な typescript を選びます。

cd ~/temp/cdkdir
mkdir cdk-appsync
cd cdk-appsync
cdk init app --language typescript

 

cdk init app コマンドにより、いろいろ自動作成されました。

> ls -la
total 336
drwxr-xr-x   7 ec2-user docker    229 Oct 29 00:56 ./
drwxr-xr-x   3 ec2-user docker     25 Oct 28 23:59 ../
drwxr-xr-x   2 ec2-user docker     28 Oct 29 00:56 bin/
-rw-r--r--   1 ec2-user docker   1412 Oct 29 00:56 cdk.json
drwxr-xr-x   8 ec2-user docker    184 Oct 29 00:56 .git/
-rw-r--r--   1 ec2-user docker     93 Oct 29 00:56 .gitignore
-rw-r--r--   1 ec2-user docker    157 Oct 29 00:56 jest.config.js
drwxr-xr-x   2 ec2-user docker     34 Oct 29 00:56 lib/
drwxr-xr-x 277 ec2-user docker   8192 Oct 29 00:56 node_modules/
-rw-r--r--   1 ec2-user docker     65 Oct 29 00:56 .npmignore
-rw-r--r--   1 ec2-user docker    573 Oct 29 00:56 package.json
-rw-r--r--   1 ec2-user docker 301122 Oct 29 00:56 package-lock.json
-rw-r--r--   1 ec2-user docker    536 Oct 29 00:56 README.md
drwxr-xr-x   2 ec2-user docker     33 Oct 29 00:56 test/
-rw-r--r--   1 ec2-user docker    650 Oct 29 00:56 tsconfig.json

 

まず、テンプレートの時点で、cdk deploy を行います。手を加える前に正しくデプロイできるか確認しておきましょう。

cdk deploy

 

cdk deploy により CloudFormation の Stack が作成されました。Stack の中には CDK Medatata が作成されています。正常に CDK が動作することが確認できました。

image-20221029013401887.png

CDK コードを作成する

このプロジェクト上で、CDK を使って、AppSync とそれに紐づく DynamoDB を作成していきます。Document を読み解いて CDK を記載するのは、初めはちょっと大変なので、サンプルコードから引っ張ってきます。

AWS が管理している Repository に、各言語での様々なサンプルが掲載されています。今回は、この中から以下のサンプルコードをほぼ丸パクリしていきます。

 

今回の構成では、lib/cdk-appsync-stack.ts のファイルを編集して、AppSync と DynamoDB の定義を行います。わかりやすいように、全部のコードを記載します。後から大事なポイントを記載するので、これは読み飛ばして頂いて大丈夫です。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema, CfnDataSource, CfnResolver } from 'aws-cdk-lib/aws-appsync';
import { Table, AttributeType, StreamViewType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam';

export class CdkAppsyncStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const tableName = 'items'

    const itemsGraphQLApi = new CfnGraphQLApi(this, 'ItemsApi', {
      name: 'items-api',
      authenticationType: 'API_KEY'
    });

    new CfnApiKey(this, 'ItemsApiKey', {
      apiId: itemsGraphQLApi.attrApiId
    });

    const apiSchema = new CfnGraphQLSchema(this, 'ItemsSchema', {
      apiId: itemsGraphQLApi.attrApiId,
      definition: `type ${tableName} {
        ${tableName}Id: ID!
        name: String
      }
      type Paginated${tableName} {
        items: [${tableName}!]!
        nextToken: String
      }
      type Query {
        all(limit: Int, nextToken: String): Paginated${tableName}!
        getOne(${tableName}Id: ID!): ${tableName}
      }
      type Mutation {
        save(name: String!): ${tableName}
        delete(${tableName}Id: ID!): ${tableName}
      }
      type Schema {
        query: Query
        mutation: Mutation
      }`
    });

    const itemsTable = new Table(this, 'ItemsTable', {
      tableName: tableName,
      partitionKey: {
        name: `${tableName}Id`,
        type: AttributeType.STRING
      },
      billingMode: BillingMode.PAY_PER_REQUEST,
      stream: StreamViewType.NEW_IMAGE,

      // The default removal policy is RETAIN, which means that cdk destroy will not attempt to delete
      // the new table, and it will remain in your account until manually deleted. By setting the policy to
      // DESTROY, cdk destroy will delete the table (even if it has data in it)
      removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
    });

    const itemsTableRole = new Role(this, 'ItemsDynamoDBRole', {
      assumedBy: new ServicePrincipal('appsync.amazonaws.com')
    });

    itemsTableRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBFullAccess'));

    const dataSource = new CfnDataSource(this, 'ItemsDataSource', {
      apiId: itemsGraphQLApi.attrApiId,
      name: 'ItemsDynamoDataSource',
      type: 'AMAZON_DYNAMODB',
      dynamoDbConfig: {
        tableName: itemsTable.tableName,
        awsRegion: this.region
      },
      serviceRoleArn: itemsTableRole.roleArn
    });

    const getOneResolver = new CfnResolver(this, 'GetOneQueryResolver', {
      apiId: itemsGraphQLApi.attrApiId,
      typeName: 'Query',
      fieldName: 'getOne',
      dataSourceName: dataSource.name,
      requestMappingTemplate: `{
        "version": "2017-02-28",
        "operation": "GetItem",
        "key": {
          "${tableName}Id": $util.dynamodb.toDynamoDBJson($ctx.args.${tableName}Id)
        }
      }`,
      responseMappingTemplate: `$util.toJson($ctx.result)`
    });
    getOneResolver.addDependsOn(apiSchema);

    const getAllResolver = new CfnResolver(this, 'GetAllQueryResolver', {
      apiId: itemsGraphQLApi.attrApiId,
      typeName: 'Query',
      fieldName: 'all',
      dataSourceName: dataSource.name,
      requestMappingTemplate: `{
        "version": "2017-02-28",
        "operation": "Scan",
        "limit": $util.defaultIfNull($ctx.args.limit, 20),
        "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))
      }`,
      responseMappingTemplate: `$util.toJson($ctx.result)`
    });
    getAllResolver.addDependsOn(apiSchema);

    const saveResolver = new CfnResolver(this, 'SaveMutationResolver', {
      apiId: itemsGraphQLApi.attrApiId,
      typeName: 'Mutation',
      fieldName: 'save',
      dataSourceName: dataSource.name,
      requestMappingTemplate: `{
        "version": "2017-02-28",
        "operation": "PutItem",
        "key": {
          "${tableName}Id": { "S": "$util.autoId()" }
        },
        "attributeValues": {
          "name": $util.dynamodb.toDynamoDBJson($ctx.args.name)
        }
      }`,
      responseMappingTemplate: `$util.toJson($ctx.result)`
    });
    saveResolver.addDependsOn(apiSchema);

    const deleteResolver = new CfnResolver(this, 'DeleteMutationResolver', {
      apiId: itemsGraphQLApi.attrApiId,
      typeName: 'Mutation',
      fieldName: 'delete',
      dataSourceName: dataSource.name,
      requestMappingTemplate: `{
        "version": "2017-02-28",
        "operation": "DeleteItem",
        "key": {
          "${tableName}Id": $util.dynamodb.toDynamoDBJson($ctx.args.${tableName}Id)
        }
      }`,
      responseMappingTemplate: `$util.toJson($ctx.result)`
    });
    deleteResolver.addDependsOn(apiSchema);

  }
}

 

大事なポイントをいくつか抜粋します。まず、冒頭にある import の部分です。CDK を利用する際には、ライブラリとして提供されているので、CDK 上で定義したいものに合わせて import を行います。今回は、AppSync, DynamoDB, IAM を定義するので、これらに関するものを import しています。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema, CfnDataSource, CfnResolver } from 'aws-cdk-lib/aws-appsync';
import { Table, AttributeType, StreamViewType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam';

 

AppSync で GraphQL API を宣言しているところはこちらです。AppSync の API 名を items-api としています。認証タイプは API_KEY です。

    const itemsGraphQLApi = new CfnGraphQLApi(this, 'ItemsApi', {
      name: 'items-api',
      authenticationType: 'API_KEY'
    });

 

AppSync のスキーマを定義する箇所はここです。通常の AppSync であれば graphql.schema で定義しますが、CDK の場合は TypeScript のコードの中に埋め込みます。コードの中には変数も使えるので、プログラム上のメリットが活かせるのが良い点ですね。

    const tableName = 'items' 
    
    const apiSchema = new CfnGraphQLSchema(this, 'ItemsSchema', {
      apiId: itemsGraphQLApi.attrApiId,
      definition: `type ${tableName} {
        ${tableName}Id: ID!
        name: String
      }
      type Paginated${tableName} {
        items: [${tableName}!]!
        nextToken: String
      }
      type Query {
        all(limit: Int, nextToken: String): Paginated${tableName}!
        getOne(${tableName}Id: ID!): ${tableName}
      }
      type Mutation {
        save(name: String!): ${tableName}
        delete(${tableName}Id: ID!): ${tableName}
      }
      type Schema {
        query: Query
        mutation: Mutation
      }`
    });

 

上記の定義により、次の graphql.schema が生成されます。${tableName} の変数が、それぞれ展開されて定義される形です。

type items {
	itemsId: ID!
	name: String
}

type Paginateditems {
	items: [items!]!
	nextToken: String
}

type Query {
	all(limit: Int, nextToken: String): Paginateditems!
	getOne(itemsId: ID!): items
}

type Mutation {
	save(name: String!): items
	delete(itemsId: ID!): items
}

type Schema {
	query: Query
	mutation: Mutation
}

 

DynamoDB のテーブル定義はこんな感じです。テーブル名や、パーティションキーなどが定義されています。

    const itemsTable = new Table(this, 'ItemsTable', {
      tableName: tableName,
      partitionKey: {
        name: `${tableName}Id`,
        type: AttributeType.STRING
      },
      billingMode: BillingMode.PAY_PER_REQUEST,
      stream: StreamViewType.NEW_IMAGE,

      // The default removal policy is RETAIN, which means that cdk destroy will not attempt to delete
      // the new table, and it will remain in your account until manually deleted. By setting the policy to
      // DESTROY, cdk destroy will delete the table (even if it has data in it)
      removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
    });

 

AppSync のデータソースの定義や、それを使ったリゾルバーの定義がこの辺りです。データソースとして、DynamoDB のテーブルが定義されています。また、リゾルバーとして getOne という Query が定義されています。裏側では、DynamoDB に対して GetItem がされています。"${tableName}Id": $util.dynamodb.toDynamoDBJson($ctx.args.${tableName}Id) の定義により、AppSync のパラメータとして渡された ItemsId のデータを取得する内容となります。これは、CDK というよりも、AppSync のマッピングテンプレートの仕組みですね。

    const dataSource = new CfnDataSource(this, 'ItemsDataSource', {
      apiId: itemsGraphQLApi.attrApiId,
      name: 'ItemsDynamoDataSource',
      type: 'AMAZON_DYNAMODB',
      dynamoDbConfig: {
        tableName: itemsTable.tableName,
        awsRegion: this.region
      },
      serviceRoleArn: itemsTableRole.roleArn
    });

    const getOneResolver = new CfnResolver(this, 'GetOneQueryResolver', {
      apiId: itemsGraphQLApi.attrApiId,
      typeName: 'Query',
      fieldName: 'getOne',
      dataSourceName: dataSource.name,
      requestMappingTemplate: `{
        "version": "2017-02-28",
        "operation": "GetItem",
        "key": {
          "${tableName}Id": $util.dynamodb.toDynamoDBJson($ctx.args.${tableName}Id)
        }
      }`,
      responseMappingTemplate: `$util.toJson($ctx.result)`
    });
    getOneResolver.addDependsOn(apiSchema);

CDK デプロイ

これらの CDK のコードを書いたあとに、cdk diff を実行すると、現在からの差分を確認できます。AppSync や DynamoDB や IAM Role が新規追加される様子がわかります。

> cdk diff
Stack CdkAppsyncStack
IAM Statement Changes
┌───┬──────────────────────────┬────────┬────────────────┬───────────────────────────────┬───────────┐
│   │ Resource                 │ Effect │ Action         │ Principal                     │ Condition │
├───┼──────────────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤
│ + │ ${ItemsDynamoDBRole.Arn} │ Allow  │ sts:AssumeRole │ Service:appsync.amazonaws.com │           │
└───┴──────────────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬──────────────────────┬────────────────────────────────────────────────────────────────┐
│   │ Resource             │ Managed Policy ARN                                             │
├───┼──────────────────────┼────────────────────────────────────────────────────────────────┤
│ + │ ${ItemsDynamoDBRole} │ arn:${AWS::Partition}:iam::aws:policy/AmazonDynamoDBFullAccess │
└───┴──────────────────────┴────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[+] AWS::AppSync::GraphQLApi ItemsApi ItemsApi 
[+] AWS::AppSync::ApiKey ItemsApiKey ItemsApiKey 
[+] AWS::AppSync::GraphQLSchema ItemsSchema ItemsSchema 
[+] AWS::DynamoDB::Table ItemsTable ItemsTable5AAC2C46 
[+] AWS::IAM::Role ItemsDynamoDBRole ItemsDynamoDBRole7D2E3F6D 
[+] AWS::AppSync::DataSource ItemsDataSource ItemsDataSource 
[+] AWS::AppSync::Resolver GetOneQueryResolver GetOneQueryResolver 
[+] AWS::AppSync::Resolver GetAllQueryResolver GetAllQueryResolver 
[+] AWS::AppSync::Resolver SaveMutationResolver SaveMutationResolver 
[+] AWS::AppSync::Resolver DeleteMutationResolver DeleteMutationResolver 

 

cdk deploy で実際にデプロイを行います。

cdk deploy

 

CloudFormation では、このように CDK で定義した各種リソースがデプロイされている様子がわかります。

image-20221029022057037.png

AppSync

新しく API が作成され、以下の Schema が定義されています。CDK で指定したものが利用されています。

image-20221029022521482.png

Query の定義はこんな感じです。CDK で定義した 4 つのリゾルバーが見えています。

image-20221029022841829.png

DynamoDB

items のテーブルが作成されています。

image-20221029022626202.png

AppSync で操作してみる

AppSync の画面で、実際にクエリーを実行してみます。save で Mutation をします。

image-20221029180758510.png

裏側では、しっかり DynamoDB にデータが保存されています。

image-20221029180857972.png

次に、AppSync でクエリーをして、先ほど格納したデータを取得できるか確認します。all ですべての item を取得してみると、先ほど格納した 1 件のデータが取得できました。複数のデータを格納すると、複数のデータを取得できます。

image-20221029180951527.png

検証を通じてわかったこと

  • CDK v2 のサンプルコードが GitHub で公開されており、まずはそこから検証をし始めるのがやりやすい
  • CDK v2 のコード上で、AppSync に紐づく graphql.schema の指定や、カスタムリゾルバーの指定を行うことが出来る。

参考資料

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1