LoginSignup
3
3

More than 3 years have passed since last update.

AWSCDK に AppSync の High-level constructs が来たぞ!

Last updated at Posted at 2019-12-19

はじめに

タイトルに書いたとおり AWSCDK 1.19.0 で AppSync の High-level constructs がきて嬉しくなっちゃったので書きます!

AWSCDKとは

CloudFormaiton(以下CFn)のテンプレートをTypeScriptやPythonといったプログラミング言語で書くことができるフレームワークです。
詳しくは公式サイトを見てください。

AWS AppSyncとは

AWSでGraphQLを使うならAppSync!って感じのサービスです。
GraphQLのDataSourceにDynamoDBやLambdaを指定することができ、GraphQLのAPIから操作することができます。
詳しくは公式サイトを見てください。

書いていくぞ!

今までAWSCDKでAppSyncを書こうと思うと Cfnxxxx といったLow-level constructsで書かなければいけませんでした。
そのため必要なプロパティをすべて指定しないといけなかったりと手間がかかりました…
今回のアップデートでHigh-level constructsが来たので、どういう風に書き方が変わったのか書いていきます。

GraphQLのスキーマとなるgraphqlファイル

schema.graphql
type Post {
  id: String,
  title: String,
  create_time: String
}
input InputPost {
  id: String
  title: String,
  create_time: String
}
type Query {
  all: [Post]
  query(id: String!, start: String!, end: String!): [Post]
}
type Mutation {
  save(input: InputPost!): Post
  delete(id: ID!): Post
}
type Schema {
  query: Query
  mutation: Mutation
}

Stackとなるソースコード

stack.ts
import cdk = require("@aws-cdk/core")
import { join } from "path"

import { UserPool, SignInType } from "@aws-cdk/aws-cognito"
import {
  GraphQLApi,
  FieldLogLevel,
  UserPoolDefaultAction,
  MappingTemplate
} from "@aws-cdk/aws-appsync"
import {
  TableProps,
  AttributeType,
  BillingMode,
  Table
} from "@aws-cdk/aws-dynamodb"

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

    const userPool = new UserPool(this, "UserPool", {
      userPoolName: "DemoAPIUserPool",
      signInType: SignInType.USERNAME
    })

    const table = new Table(this, "CDKPostTable", {
      tableName: "CDKPostTable",
      partitionKey: {
        name: "id",
        type: AttributeType.STRING
      },
      sortKey: {
        name: "create_time",
        type: AttributeType.STRING
      },
      billingMode: BillingMode.PROVISIONED,
      readCapacity: 1,
      writeCapacity: 1
    })

    const api = new GraphQLApi(this, "PostAPI", {
      name: "PostAPI",
      logConfig: {
        fieldLogLevel: FieldLogLevel.ALL
      },
      userPoolConfig: {
        userPool,
        defaultAction: UserPoolDefaultAction.ALLOW
      },
      schemaDefinitionFile: join(__dirname, "schema.graphql")
    })

    const datasource = api.addDynamoDbDataSource("PostAPIDataSource", "", table)

    datasource.createResolver({
      typeName: "Query",
      fieldName: "all",
      requestMappingTemplate: MappingTemplate.dynamoDbScanTable(),
      responseMappingTemplate: MappingTemplate.dynamoDbResultList()
    })
    datasource.createResolver({
      typeName: "Mutation",
      fieldName: "save",
      requestMappingTemplate: MappingTemplate.dynamoDbPutItem(
        PrimaryKey.partition("id").is("input.id"),
        Values.projecting("input")
      ),
      responseMappingTemplate: MappingTemplate.dynamoDbResultItem()
    })
    datasource.createResolver({
      typeName: "Mutation",
      fieldName: "delete",
      requestMappingTemplate: MappingTemplate.dynamoDbDeleteItem("id", "id"),
      responseMappingTemplate: MappingTemplate.dynamoDbResultItem()
    })

    const queryMappingTemplate = `
      {
        "version": "2017-02-28",
        "operation": "Query",
        "query": {
          "expression": "#id = :id AND #createTime BETWEEN :start AND :end",
          "expressionNames": {
            "#id": "id"
            "#createTime": "create_time"
          },
          "expressionValues": {
            ":id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
            ":start": $util.dynamodb.toDynamoDBJson($ctx.args.start),
            ":end": $util.dynamodb.toDynamoDBJson($ctx.args.end)
          }
        }
      }
    `
    datasource.createResolver({
      typeName: "Query",
      fieldName: "query",
      requestMappingTemplate: MappingTemplate.fromString(queryMappingTemplate),
      responseMappingTemplate: MappingTemplate.dynamoDbResultList()
    })
  }
}

説明していく

スタックとなるTSファイルの中身を説明していきます。
今回の例ではGraphQLでDynamoDBにデータを読み書きするAppSyncを定義しています。
またその認証にCognitoを使っています。

AppSync以外のリソース定義

CognitoとDynamoDBを定義します。

// CognitoのUserPool定義
const userPool = new UserPool(this, "UserPool", {
  userPoolName: "DemoAPIUserPool",
  signInType: SignInType.USERNAME
})

// DynamoDBのTable定義
const table = new Table(this, "CDKPostTable", {
  tableName: "CDKPostTable",
  partitionKey: {
    name: "id",
    type: AttributeType.STRING
  },
  sortKey: {
    name: "create_time",
    type: AttributeType.STRING
  },
  billingMode: BillingMode.PROVISIONED,
  readCapacity: 1,
  writeCapacity: 1
})

AppSync本体の定義

AppSync本体の定義をします。

propaty description
name API名
logConfig 残すログのレベル
userPoolConfig CognitoUserPoolの情報
schemaDefinitionFile GraphQLのSchemaファイルのパス
const api = new GraphQLApi(this, "PostAPI", {
  name: "PostAPI",
  logConfig: {
    fieldLogLevel: FieldLogLevel.ALL
  },
  userPoolConfig: {
    userPool,
    defaultAction: UserPoolDefaultAction.ALLOW
  },
  schemaDefinitionFile: join(__dirname, "schema.graphql")
})

DataSource定義

AppSyncのDataSourceを定義します。
先程定義した GraphQLAApiaddDynamoDbDataSource() というメソッドが生えているので、それを使ってDataSourceを定義します。
第1引数にDataSource名、第2引数に説明、第3引数にDynamoDBのTableを渡します。

const datasource = api.addDynamoDbDataSource("PostAPIDataSource", "", table)

Resolver定義

GraphQLの各Query/MutationにResolverを定義します。
先程定義したDataSourceに createResolver() というメソッドが生えているので、それを使ってResolverを定義します。

propaty description
typeName QueryなのかMutationなのか
fieldName GraphQLのフィールド名
requestMappingTemplate リクエスト時のマッピングテンプレート
responseMappingTemplate レスポンス時のマッピングテンプレート
datasource.createResolver({
  typeName: "Query",
  fieldName: "all",
  requestMappingTemplate: MappingTemplate.dynamoDbScanTable(),
  responseMappingTemplate: MappingTemplate.dynamoDbResultList()
})

また MappingTemplate にDynamoDBを操作するときのテンプレートを生成してくれるメソッドがあります。
現時点では Scan / GetItem / PutItem / DeleteItem が用意されています。

MappingTemplate.dynamoDbScanTable()
MappingTemplate.dynamoDbGetItem()
MappingTemplate.dynamoDbPutItem()
MappingTemplate.dynamoDbDeleteItem()

ここで気づく人は気づくと思うんですが、 Query がないんですよね…
そういう場合はMappingTemplateを自分で書く必要があります。
自分でMappingTemplateを書いた場合は MappingTemplate.fromString() を使って定義します。
あくまでも先程紹介したのは便利関数なので、もちろんこうやって自分で書いて定義することもできます。

const queryMappingTemplate = `
  {
    "version": "2017-02-28",
    "operation": "Query",
    "query": {
      "expression": "#id = :id AND #createTime BETWEEN :start AND :end",
      "expressionNames": {
        "#id": "id"
        "#createTime": "create_time"
      },
      "expressionValues": {
        ":id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
        ":start": $util.dynamodb.toDynamoDBJson($ctx.args.start),
        ":end": $util.dynamodb.toDynamoDBJson($ctx.args.end)
      }
    }
  }
`
datasource.createResolver({
  typeName: "Query",
  fieldName: "query",
  requestMappingTemplate: MappingTemplate.fromString(queryMappingTemplate),
  responseMappingTemplate: MappingTemplate.dynamoDbResultList()
})

またレスポンスのMappingTemplateについても用意されています。
配列で返す場合は dynamoDbResultList() 、単体で返す場合は dynamoDbResultItem() を使います。

MappingTemplate.dynamoDbResultList()
MappingTemplate.dynamoDbResultItem()

まとめ

AppSync の High-level constructs が来て何が嬉しいのか、個人的に感じているのは、Resolverの定義のところです。
MappingTemplate.dynamoDbPutItem() といった便利関数が用意されたおかげで、すべてのResolverを手書きする必要がなくなったので、コード量が少なくなりました。
また Cfnxxxx で定義していたときはリソースのデプロイされる順番を addDependsOn() でコード側で担保しなければいけませんでしたが、それも内部でやってくれるようになったので、そこもかなり助かる部分です。(忘れてしまうこと多いので…)

AWSCDK、みなさんもぜひ試してみてください!
ではまた!!!

3
3
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
3
3