我试用了AWS AppSync + RDS
本文是DWANGO Advent Calendar 2018中第七天的文章。
因为决定在内部使用GraphQL作为API,所以我们来分享一下使用AWS AppSync + RDS来进行开发和部署的方法以及可能存在的限制。这里暂时不涉及GraphQL的内容等等。
太长没读。
-
- AppSync + RDS をしたければ、Serverless Aurora か Lambda をデータソースとして利用する
-
- AppSync + RDS (というか AppSync + Lambda) は割と制約が厳しい
BatchInvoke が 5 つまでしか Batching してくれないため N+1 問題に悩まされる
ただし、他の AWS リソースとの連携は楽で良い
GraphQL Code Generator は TypeScript の型定義が自動生成でき便利
有什么限制
在构建API服务器时的要求如下。
-
- バックエンドは RDS で、MySQL 5.7 を利用してる
JSON 型などを利用している
RDS のテーブル定義がしっかりと固まっていない (別プロジェクトとして進行中)
社内向け API であり、負荷はそこまで高くならない
选择在 GraphQL 服务器中采用哪种实现。
GraphQL 的服务器实现因语言不同而各有不同,可以在 graphql.org 或 awesome-graphql 等地方找到很多介绍。我们决定采用 TypeScript 作为语言,并对服务器实现感到困惑,不知道是选择 Apollo 还是 AWS AppSync。但由于周围有很多使用 AWS 资源的服务,我们决定首先尝试后者,因为它看起来有较好的协作和支持。
此外,Prisma 提供的 Prisma Server 也看起来很不错,可以几乎不写代码,只需提供 GraphQL 模式即可启动 GraphQL 服务器。但是,目前(截至2018年12月),它无法与现有的 MySQL 服务器兼容,所以我们决定暂时搁置。
AWS AppSync 是什么?
这是 AWS 提供的全托管 GraphQL 服务,具有实时数据同步和离线同步的特点。由于是全托管服务,无需担心扩展性和管理问题,并且易于与其他 AWS 服务进行集成,这是其优点。
20180523 AWS黑带线上研讨会 AWS AppSync
如何将AWS AppSync与RDS结合使用
在AWS AppSync中,将作为后端的数据存储称为数据源。目前(截至2018/12),可以作为数据源的有DynamoDB、AWS Lambda、Amazon Elasticsearch Service、GraphQL等,并为每个提供了相应的教程。然而,暂时还没有直接支持RDS(Serverless Aurora则最近宣布提供支持1)。
由于Lambda是支持的数据源,所以可以通过Lambda来访问RDS。在官方提供的AppSync示例项目“Blog App 2”中,也使用了Lambda和RDS的组合。如果想要使用RDS,但又无法使用Serverless Aurora,那么将其与Lambda结合使用似乎是一个好选择(需要注意Lambda + RDS有一些限制)。
开发
Lambda機器: 寫什麼內容?
在AWS AppSync中,我们以每个解析器为单位,通过定义解析器映射模板来定义请求/响应对。这些模板用于定义从数据源获取的数据与GraphQL定义的类型之间的映射,并使用Apache Velocity进行编写。
在示例项目中的Blog App中,我们使用请求映射模板来组装SQL查询,并将其发送到Lambda,Lambda会执行并返回结果到RDS,然后使用响应映射模板执行与GraphQL定义相符的转换。

本来的话,Lambda应该作为一个仅用于向RDS查询并返回结果的层,并且GraphQL/MySQL之间的映射应该在映射模板中进行记录才是正确的。但是,由于以下原因,我们决定在Lambda端包括查询的生成。
-
- 生のクエリを書かずにクエリビルダや ORM を使って楽をしたい
-
- コードを書く量を減らしつつ静的解析の恩恵を受けるために、コードの自動生成を活用したい
-
- AppSync 以外に移行する事になった場合にコードベースを使い回せるようにしておきたい
コードを書くなら他に使いまわして色々と検証できたらという考え
在解析程序中,我们还可以选择是否为每个解析器使用Lambda,或者在所有解析器之间共享一个Lambda。每个选项都有其优缺点,但考虑到代码管理和重用性的便利性,我们决定在所有解析器中共享一个Lambda。
改善您的GraphQL API的十个技巧和技巧(MOB401)- AWS re:Invent 2018
Lambda: 如何书写
在描述解析器时,需要的信息都保存在 $context 变量中,所以将其转换为 JSON,并作为 Payload 传递给 Lambda 就可以了。但是,为了将当前运行的解析器传递给 Lambda,我们还决定一起传递一个可以唯一识别解析器的值(在下面的示例中是 query.item)。
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": { "resolve": "query.item", "context": $utils.toJson($context) }
}
在Lambda中,我们接收到这个参数,并参考context.arguments变量来存储具有参数的解析器的值,以及参考context.source变量来存储存在上级解析器的值。然后执行适当的查询并返回值。以下是代码片段,将解析器的标识符与执行查询的函数进行关联。
async function getItem (src, args) {
return await knex('item')
.where('id', args.id)
.first();
}
const resolvers = {
"query.item": getItem,
// ...
};
module.exports.handler = async (event) => {
const id = event.resolve;
const source = event.context.source;
const args = event.context.arguments;
const result = await resolvers[id](source, args);
return result;
};
响应映射模板仅仅是将其简单转换为JSON对象。
$util.toJson($context.result)
之后会详细介绍,我们决定在CloudFormation中进行请求/响应映射模板的编写,并使用AWS SAM对Lambda进行打包和部署。
选项:利用代码自动生成
这里我们就顺便介绍一下自动生成代码的功能。关于 GraphQL 相关的代码自动生成工具有几种,这次我试用了一个叫 GraphQL Code Generator 的工具。GraphQL Code Generator 可以根据 GraphQL Schema 自动生成 TypeScript 代码,而且针对不同的生成代码类型,还提供了各种插件供用户自由组合,这是它的特点之一。
在版本 0.14.5 中,可以按以下方式进行使用。
type Item {
id: ID!
name: String
}
type Query {
item(id: ID!): Item
}
schema: ./schema.graphql
overwrite: true
generates:
./types.ts:
plugins:
- add:
- /* tslint:disable */
- typescript-common
- typescript-server
- typescript-resolvers
在准备好上述内容后,安装并执行各项操作。
$ yarn add -D \
graphql \
graphql-code-generator \
graphql-codegen-typescript-resolvers \
graphql-codegen-typescript-common \
graphql-codegen-typescript-server \
graphql-codegen-add
$ yarn gql-gen --config gql-gen.yml
生成的代码如下。目前,代码基本上是根据插件的应用顺序生成的。只能应用所需的插件,并且可以在多个配置文件中定义应用了不同插件的不同自动生成文件。
/* tslint:disable */
// ====================================================
// Types
// ====================================================
export interface Query {
item?: Item | null;
}
export interface Item {
id: string;
name?: string | null;
}
// ====================================================
// Arguments
// ====================================================
export interface ItemQueryArgs {
id: string;
}
import { GraphQLResolveInfo, GraphQLScalarTypeConfig } from "graphql";
export type Resolver<Result, Parent = {}, Context = {}, Args = {}> = (
parent: Parent,
args: Args,
context: Context,
info: GraphQLResolveInfo
) => Promise<Result> | Result;
/* 中略 */
export namespace QueryResolvers {
export interface Resolvers<Context = {}, TypeParent = {}> {
item?: ItemResolver<Item | null, TypeParent, Context>;
}
export type ItemResolver<
R = Item | null,
Parent = {},
Context = {}
> = Resolver<R, Parent, Context, ItemArgs>;
export interface ItemArgs {
id: string;
}
}
export namespace ItemResolvers {
export interface Resolvers<Context = {}, TypeParent = Item> {
id?: IdResolver<string, TypeParent, Context>;
name?: NameResolver<string | null, TypeParent, Context>;
}
export type IdResolver<R = string, Parent = Item, Context = {}> = Resolver<
R,
Parent,
Context
>;
export type NameResolver<
R = string | null,
Parent = Item,
Context = {}
> = Resolver<R, Parent, Context>;
}
/* 中略 */
部署
我在部署AWS AppSync时使用了CloudFormation,在部署Lambda函数时单独使用了AWS SAM。将模板分开的原因是考虑到后续可能需要对查询性能进行调优等情况,可能只想替换Lambda部分。
Lambda单独使用没有特别困难,我只是稍微涉及了一下AppSync方面在CloudFormation中的部署。以下是一些要点。
-
- リクエスト/レスポンスマッピングテンプレートは直書き
外部からテキストを埋め込む方法が見つからなかったので
マッピングテンプレートをゴリゴリかかないので特に問題はなし
GraphQL スキーマ定義は、S3 にアップロードした後にそのパスを指定させることができる
GraphQLSchema には S3 上の GraphQL スキーマを読み取る DefinitionS3Location という項目があるので、S3 バケットにテンプレートをアップロードしたのちに、そのパスを Parameter として CloudFormation のデプロイ時に渡すことができる
展示部分模板,具体如下所示。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
APIName:
Type: String
RDSLambdaArnValueName:
Type: String
GraphQLSchemaS3BucketLocation:
Type: String
Resources:
SearchAPI:
Type: "AWS::AppSync::GraphQLApi"
Properties:
Name: !Sub ${APIName}
AuthenticationType: "API_KEY"
SearchAPISchema:
Type: "AWS::AppSync::GraphQLSchema"
DependsOn:
- SearchAPI
Properties:
ApiId: !GetAtt SearchAPI.ApiId
DefinitionS3Location: !Sub ${GraphQLSchemaS3BucketLocation}
AppSyncRDSDataSource:
Type: "AWS::AppSync::DataSource"
DependsOn:
- SearchAPI
Properties:
ApiId: !GetAtt SearchAPI.ApiId
Name: "RDSDataSource"
Type: "AWS_LAMBDA"
ServiceRoleArn:
Ref: AppSyncRDSServiceRole
LambdaConfig:
LambdaFunctionArn:
Fn::ImportValue:
!Ref RDSLambdaArnValueName
尝试使用AWS AppSync。
好地方
-
- 従量課金制なのでお手軽
-
- フルマネージドサービスなので、管理やスケーリングを気にせずに利用できる
-
- 他の AWS リソースとの連携が容易
一部のリゾルバを Elasticsearch Service から利用したい場合などに有用
DynamoDB をデータソースとすると、GraphQL スキーマの自動生成が活用できる
認証方法が豊富に用意されている
Cognito, IAM Role, API Key,…
Cognito を利用すると一瞬で認証機能が作れてとても便利
AWS コンソールの AppSync の画面から即座に API を叩いて結果を確認できる
限制等
-
- クエリ実行時間が 30 秒等の 各種制限
ユーザ独自のスカラー型が利用できない
AppSync にはデフォルトで独自のスカラー型 が用意されているのですが、ユーザ独自のスカラー型はサポートしてないようで、そのような定義を含んだスキーマをインポートするとエラーとなります
GraphQL スキーマに問題があった場合のエラー文言がわかりにくい
CloudFormation 経由で GraphQL スキーマをインポートした際、型周りのエラーでも Schema Creation Status is FAILED with details: Failed to parse schema document – ensure it’s a valid SDL-formatted document. といった文言で済まされる場合があります
GraphQL スキーマの問題を発見するために、何かしらの linter を導入しておくと良いでしょう。現在はとりあえず こちら を利用しています
Batch Invoke が最大 5 つまでしか Batching してくれない
後述
在Lambda数据源上的BatchInvoke限制
详细信息可以在文档中找到,但在Lambda数据源中,可以使用BatchInvoke调用方法来解决N+1问题。这意味着在GraphQL查询的一次请求中对同一个解析器进行多次调用时,可以通过将它们合并为一个调用(批处理)来完成本来需要多次执行的Lambda函数的操作。除了AppSync之外,这个问题可以通过使用DataLoader等库进行处理。
目前,BatchInvoke最多只会处理5个项目。这限制相当严格。下面的文章大约是在今年的8月份,但自从我保存之后,看起来这个限制还没有放宽。
AWS AppSync-意想不到的选择
结束
通过开始学习 GraphQL 并试用 AppSync + RDS,我们进行了一些试验性的探索。由于后端已经确定使用了 RDS,因此我们进行了相应的适配。但如果后端数据存储具有灵活性,那么使用 DynamoDB 或 Elasticsearch Service 等可能会更好。或者,尝试迁移到 Serverless Aurora 也是一个不错的选择(Serverless Aurora 的 Data API 目前似乎是测试版)。
最终,我们并没有太多使用映射模板,而是将相当数量的代码编写到了 Lambda 函数中,这可能是需要反思的地方。这样一来,继续使用 AppSync 的好处是什么呢?可能源于 AWS 生态系统和支持吧。将多个数据源组合在一起是 GraphQL 魅力的一部分,而这在 AWS 资源上能够轻松实现,这确实很好。
最严格的限制是 BatchInvoke 的批处理限制最多为 5 个。虽然官方文档中没有提及,我意识到得有点晚。由于背后是 Lambda(而且更进一步是 RDS),所以需要谨慎设计查询。如果没有这个限制,本来可能会考虑采用,但是因为这个限制,情况变得有点不确定。
AWS AppSync 是一个非常有趣的服务,除了 Serverless Aurora,也不断推出新功能。我们可以在各个地方都能看到它的实际应用。另一方面,在 GraphQL 领域,听说 Apollo 的势头相对较强,所以我们也会在接下来更加深入地了解它。
最近有人宣布,在AWS AppSync中支持Serverless Aurora了。由于这是使用了几乎同时宣布的Serverless Aurora的Data API,所以只能在Serverless Aurora中使用。此外,与Serverless Aurora只支持MySQL 5.6相比,由于使用了5.7及更高版本特有的JSON类型等功能,我们放弃了使用Serverless Aurora。↩
在AWS AppSync的控制台上生成API时,可以使用从示例项目生成的功能。其中一个示例是使用RDS作为后端的Blog App。↩
Lambda + RDS由于冷启动时的延迟和连接数限制等问题而被认为是一种反模式。但是,由于这次是为了内部API,并且预计负载不会太高,我们决定采用这种方式。↩
由于版本低于1.0,使用方法可能会有重大变化,所以请注意。例如,引入配置文件是从0.13到0.14的变化。↩