AWS CDKでAppSyncのスタックを作ってみた

AWS CDKをさわれば触るほど、素晴らしさに気づけます。
今回は、AWS CDKを使ってAppSyncとDynamoDBを使用したサービスを作ってみたいと思います。

本記事では、TypeScriptの使用方法や、CDKの基本的な使い方は紹介しません。 もくもくと、CDKでAppSyncを使うまでにしたことを記載します。
また本ブログで使用した、AWS CDKのバージョンは1.3.0です。

目次

プロジェクトの初期設定

AWS CDKのスタックを作成するための初期設定を行ないます。 個人の趣向ですが、cdk initをせずにプロジェクトの設定を行います。

$ mkdir cdk-appsync-101 && cd $_
$ git init
$ npm init -y
$ npm i @aws-cdk/{aws-appsync,aws-dynamodb,aws-iam,core}
$ npm i -D aws-cdk @types/node typescript

次に、package,jsonの設定をしてます。
下記のscriptsの部分を変更してください。

{
  "name": "cdk-appsync-101",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "deploy": "tsc && cdk deploy"
  },
  "author": "37108",
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^12.7.1",
    "aws-cdk": "^1.3.0",
    "typescript": "^3.5.3"
  },
  "dependencies": {
    "@aws-cdk/aws-appsync": "^1.3.0",
    "@aws-cdk/aws-dynamodb": "^1.3.0",
    "@aws-cdk/aws-iam": "^1.3.0",
    "@aws-cdk/core": "^1.3.0"
  }
}

ハイライトした部分の変更はできましたか。authorLICENSEなどの細かい部分は人によってまちまちなのであまり気にしないでください。

次に、tsconfig.jsonを作成します。
あまりルールを厳しくはしていないのでお好みで変えてください。

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "outDir": "./dist/",
    "lib": [ "es2016", "es2017.object", "es2017.string" ],
    "noUnusedLocals": false,
    "noUnusedParameters": false
  }
}

最後にcdk.jsonを追加して初期設定は終わりです。

{
  "app": "node dist/index"
}

これでプロジェクトの初期設定は終了です。

CDKのコーディング

コーディングをしてAppSync API、DynamoDBのテーブルを作成していきます。

クラスの作成

まずはクラスの作成をしていきます。 ディレクトリ配下にsrcディレクトリを作成し、中に、index.tsというファイルを作成します。
このファイルに下記コードを書きます。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core')

export class AppSync101Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
  }
}

const app = new cdk.App()
new AppSync101Stack(app, 'AppSync101Stack')
app.synth()

普段作成している、AWS CDKのプロジェクトと同じ様に記載すれば問題ありません。

スキーマの定義

GraphQLで使用するスキーマを定義します。
クラス内で定義しても良いのですが、可読性がよくないので別ファイルに分離します。
新たにsrc配下にschema.tsというファイルを作成し下記の様に書きます。
getItemというクエリと、addItem、deleteItemというミューテーションを記述しただけの簡素なスキーマです。

const tableName = 'items'

export const definition = `
type ${tableName} {
  id: ID!
  name: String
}
type Query {
  getItem(id: ID!): ${tableName}
}
type Mutation {
  addItem(name: String!): ${tableName}
  deleteItem(id: ID!): ${tableName}
}
type Schema {
  query: Query
  mutation: Mutation
}
`

AppSync APIの作成

いよいよAppSync APIを作成していきます。
認証方式はAPIで、スキーマは先ほど作ったものを使用していきます。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core')
import { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema } from '@aws-cdk/aws-appsync'

import { definition } from './schema'

const tableName = 'items'

export class AppSync101Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
    const graphQLApi = new CfnGraphQLApi(this, 'itemsApi', {
      name: 'items-api',
      authenticationType: 'API_KEY'
    })
    new CfnApiKey(this, 'ItemsApiKey', {
      apiId: graphQLApi.attrApiId
    })

    const Schema = new CfnGraphQLSchema(this, 'ItemsSchema', {
      apiId: graphQLApi.attrApiId,
      definition,
    })
  }
}

const app = new cdk.App()
new AppSync101Stack(app, 'AppSync101Stack')
app.synth()

ここまでできたら一旦デプロイしてみましょう。
もちろんリゾルバがないので何もできない、侘しいAPIが出来上がります。

$ npm run deploy
AppSync101Stack: deploying...
AppSync101Stack: creating CloudFormation changeset...

✅ AppSync101Stack

AppSync APIが作成できましたね。

DynamoDB テーブルの作成

AppSyncのリゾルバとしてDynamoDBのテーブルを使用するので作成します。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core')
import { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema } from '@aws-cdk/aws-appsync'
import { Table, AttributeType } from '@aws-cdk/aws-dynamodb'

import { definition } from './schema'

const tableName = 'items'

export class AppSync101Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
    const graphQLApi = new CfnGraphQLApi(this, 'itemsApi', {
      name: 'items-api',
      authenticationType: 'API_KEY'
    })
    new CfnApiKey(this, 'ItemsApiKey', {
      apiId: graphQLApi.attrApiId
    })

    const Schema = new CfnGraphQLSchema(this, 'ItemsSchema', {
      apiId: graphQLApi.attrApiId,
      definition,
    })

    const dynamoTable = new Table(this, 'items', {
      partitionKey: {
        name: 'id',
        type: AttributeType.STRING
      },
      tableName,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    })
  }
}

const app = new cdk.App()
new AppSync101Stack(app, 'AppSync101Stack')
app.synth()

IAM ロールの作成

AppSyncからDynamoDBに対して接続できるようにIAM ロールを作成します。 DynamoDBへのフルアクセスを許可していますが、本番で使う場合は十分に権限を検討してください。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core')
import { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema } from '@aws-cdk/aws-appsync'
import { Table, AttributeType } from '@aws-cdk/aws-dynamodb'
import { Role, ServicePrincipal, ManagedPolicy } from '@aws-cdk/aws-iam'

import { definition } from './schema'

const tableName = 'items'

export class AppSync101Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
    const graphQLApi = new CfnGraphQLApi(this, 'itemsApi', {
      name: 'items-api',
      authenticationType: 'API_KEY'
    })
    new CfnApiKey(this, 'ItemsApiKey', {
      apiId: graphQLApi.attrApiId
    })

    const Schema = new CfnGraphQLSchema(this, 'ItemsSchema', {
      apiId: graphQLApi.attrApiId,
      definition,
    })

    const dynamoTable = new Table(this, 'items', {
      partitionKey: {
        name: 'id',
        type: AttributeType.STRING
      },
      tableName,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    })
    
    const dataSourceIamRole = new Role(this, 'dataSourceIamRole', {
      assumedBy: new ServicePrincipal('appsync.amazonaws.com')
    })
    dataSourceIamRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBFullAccess'))
  }
}

const app = new cdk.App()
new AppSync101Stack(app, 'AppSync101Stack')
app.synth()

データソースの作成とリゾルバのアタッチ

DynamoDB テーブルもできIAM ロールの準備もできたので、最後にリゾルバのアタッチを行いましょう。
スキーマで定義したクエリとミューテーション分だけリゾルバを作成していきます。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core')
import { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema, CfnDataSource, CfnResolver } from '@aws-cdk/aws-appsync'
import { Table, AttributeType } from '@aws-cdk/aws-dynamodb'
import { Role, ServicePrincipal, ManagedPolicy } from '@aws-cdk/aws-iam'

import { definition } from './schema'

const tableName = 'items'

export class AppSync101Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
    const graphQLApi = new CfnGraphQLApi(this, 'itemsApi', {
      name: 'items-api',
      authenticationType: 'API_KEY'
    })
    new CfnApiKey(this, 'ItemsApiKey', {
      apiId: graphQLApi.attrApiId
    })

    const Schema = new CfnGraphQLSchema(this, 'ItemsSchema', {
      apiId: graphQLApi.attrApiId,
      definition,
    })

    const dynamoTable = new Table(this, 'items', {
      partitionKey: {
        name: 'id',
        type: AttributeType.STRING
      },
      tableName,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    })

    const dataSourceIamRole = new Role(this, 'dataSourceIamRole', {
      assumedBy: new ServicePrincipal('appsync.amazonaws.com')
    })
    dataSourceIamRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBFullAccess'))

    const dataSource = new CfnDataSource(this, 'dataSource', {
      apiId: graphQLApi.attrApiId,
      name: 'ItemsDynamoDataSource',
      serviceRoleArn: dataSourceIamRole.roleArn,
      type: 'AMAZON_DYNAMODB',
      dynamoDbConfig: {
        tableName,
        awsRegion:this.region
      }
    })
    const getItemResolver = new CfnResolver(this, 'getItemResolver', {
      apiId: graphQLApi.attrApiId,
      typeName: 'Query',
      fieldName: 'getItem',
      dataSourceName: dataSource.name,
      requestMappingTemplate: `{
        "version": "2017-02-28",
        "operation": "GetItem",
        "key": {
          "id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
        }
      }`,
      responseMappingTemplate: `$util.toJson($ctx.result)`
    })
    getItemResolver.addDependsOn(Schema)

    const addItemResolver = new CfnResolver(this, 'addItemResolver', {
      apiId: graphQLApi.attrApiId,
      typeName: 'Mutation',
      fieldName: 'addItem',
      dataSourceName: dataSource.name,
      requestMappingTemplate: `{
        "version": "2017-02-28",
        "operation": "PutItem",
        "key": {
          "id": { "S": "$util.autoId()" }
        },
        "attributeValues": {
          "name": $util.dynamodb.toDynamoDBJson($ctx.args.name)
        }
      }`,
      responseMappingTemplate: `$util.toJson($ctx.result)`
    })
    addItemResolver.addDependsOn(Schema)

    const deleteItemResolver = new CfnResolver(this, 'deleteItemResolver', {
      apiId: graphQLApi.attrApiId,
      typeName: 'Mutation',
      fieldName: 'deleteItem',
      dataSourceName: dataSource.name,
      requestMappingTemplate: `{
        "version": "2017-02-28",
        "operation": "DeleteItem",
        "key": {
          "id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
        }
      }`,
      responseMappingTemplate: `$util.toJson($ctx.result)`
    })
    deleteItemResolver.addDependsOn(Schema)
  }
}

const app = new cdk.App()
new AppSync101Stack(app, 'AppSync101Stack')
app.synth()

デプロイ

最後にデプロイして完了です。

$ npm run deploy
AppSync101Stack: deploying...
AppSync101Stack: creating CloudFormation changeset...

✅ AppSync101Stack

クエリを実行する

デプロイしたAppSync APIにクエリを投げてみます。 まずは、AWSマネジメントコンソールログインします。
次に画像のようにミューテーションを書きます。

準備ができたので実行します。

データができたので、次はクエリを実行します。 先ほどと同じ要領でクエリを書きます。

準備ができたので実行します。

問題なく動作していますね。

さいごに

AWS CDKでAppSyncとDynamoDBを作成しましたが、非常にサクサク書くことができました。
また、AppSyncやDynamoDBについても少し詳しく慣れたので非常によかったです。