我在AWS CDK中尝试搭建了AppSync
本文是弥生 Advent Calendar 2022 的第18篇文章。
你好,我是永野,一位于2022年7月入职的后端工程师。
我想介绍一下弥生公司的工程师团队,他们正在使用AWS CDK进行AppSync的构建和验证。
另外,本次將略過對於AWS CDK、AppSync和GraphQL的解釋。原因是已經有很多詳細的文章存在於世界中,而且我本身實際上也是第一次接觸上述技術,所以按照「知行合一」的原則,我們主要著重於實際動手操作。
工程师团队是
这是一个用于积累和共享技术信息和专业知识,并培养工程师的虚拟团队。我们每周三进行大约2至3小时的活动。
活动内容由每个小组决定。在我参加的小组中,最近的目标是提升AWS技能,每个人都进行实践操作,调查和验证各种服务,并分享和交流结果和意见。
顺便说一下,不仅限于这项活动,弥生公司可以申请使用AWS学习环境,这样您可以无需担心AWS使用成本,继续进行自学。
这次要实施的内容
我們將使用 AppSync 實施一個具有以下兩個命令的 GraphQL API。
1. 通过ISBN添加图书的操作
使用Google Books API通过发送的ISBN码来获取图书信息,并将其存储到DynamoDB中的操作。使用Lambda作为数据源,在LambdaHandler中进行必要的处理。
2. 所有图书查询
从DynamoDB获取已注册的所有图书数据的命令。
使用DynamoDB作为数据源,在AppSync上直接获取数据并返回给调用者。
事前准备
按照AWS CDK的入门指南所述,使用CDK需要预先安装AWS CLI和Node.JS。由于我们将在Windows机器上进行开发,因此我们将使用适用于Windows的包管理器Winget进行安装。
安装Node.JS
winget install -e --id OpenJS.NodeJS.LTS
安装 AWS CLI。
winget install -e --id Amazon.AWSCLI
認証信息会使用带名称的配置文件进行保存。
另外,由于本次使用AWS SSO进行认证,因此需要运行aws sso configure命令,并对交互提示中出现的问题进行输入。(参考)
aws configure sso --profile enggrp
设定后将保持登录状态,但如果一段时间过去后要使用,请提前使用aws sso login命令进行登录。
aws sso login --profile enggrp
创建CDK项目
现在,事前准备已经完成,我们立即开始创建一个用于使用CDK的项目。
首先,在创建项目的文件夹中,使用cdk init命令来创建项目。
npx cdk@latest init app --language typescript
执行后,CDK项目将在文件夹中创建。
自动生成的文件夹和文件中,将手动添加一些文件,这些文件将是本次使用的全部文件。
(手动添加的内容将在后面的说明中逐步创建)
?eng-grp-cdk-appsync
┣ ?bin
┃ ┗ ?eng-grp-cdk-appsync.ts
┣ ?graphql # 手動追加
┃ ┗ ?schema.graphql # 手動追加
┣ ?lambda # 手動追加
┃ ┣ ?appsync-resolver.ts # 手動追加
┃ ┗ ?graphql.ts # 手動追加(コードジェネレータで生成する)
┣ ?lib
┃ ┗ ?eng-grp-cdk-appsync-stack.ts
┣ ?node_modules
┣ ?test
┃ ┗ ?eng-grp-cdk-appsync.test.ts
┣ ?.gitignore
┣ ?.npmignore
┣ ?cdk.json
┣ ?codegen.ts # 手動追加
┣ ?jest.config.js
┣ ?package-lock.json
┣ ?package.json
┣ ?README.md
┗ ?tsconfig.json
完成项目后,最后一步是对AWS账户进行引导。
npx cdk bootstrap --profile enggrp
安装额外的软件包
我们将添加在这次实现和操作中使用的npm包。
aws-cdk/aws-appsync-alpha可以按如下方式用中文表达:
亚马逊云开发工具包中的 aws-appsync-alpha
在构建AppSync时,我想要使用L2构造。CDK从V2开始,稳定版本的API包含在aws-cdk-lib包中的所有资源中。然而,实验性API不包含在aws-cdk-lib中,而是在另一个包中提供。(参考)由于AppSync的L2构造只有实验性的版本,因此我们需要额外安装它。
npm install @aws-cdk/aws-appsync-alpha
末尾加上-alpha的,即是实验性API。
esbuild 是一个用于快速构建和打包应用程序的工具。
NodejsFunction构造函数可用于将用TypeScript实现的LambdaHandler进行转译和打包。
这次,我们将使用TypeScript来实现Lambda。
使用CDK的NodejsFunction构造函数,可以自动为用TypeScript编写的代码进行转译和捆绑,创建一个Lambda函数。非常方便。
在Docker容器内执行该编译任务也是可行的,但这次我们将在本地安装并使用esbuild。
如果已经安装了esbuild,将自动使用它。
npm install -D esbuild
AWS Lambda 类型
安装用于处理 Lambda 函数处理程序的 TypeScript 类型定义。
npm install -D @types/aws-lambda
亚马逊云服务软件开发工具包(AWS SDK)中的DynamoDB相关包。
为了在本次创建的Lambda函数中使用DynamoDB,我们需要安装DynamoDB操作的客户端和工具。
npm install @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb
axios
在Lambda函数内,安装axios作为HTTP客户端,用于与Google Book API 进行交互。
npm install axios
代码生成器
安装用于创建TypeScript类型定义代码的软件包,以从GraphQL模式中生成。
npm install -D @graphql-codegen/cli @graphql-codegen/typescript
GraphQL的Schema定义
首先,我们将使用AppSync创建GraphQL API的模式定义作为外部文件进行创建。
在使用CDK创建资源时,我们将读取此文件并实现AppSync的模式定义。
此外,在创建Lambda时,我们还将使用此文件生成TypeScript类型定义以从模式定义中生成该类型。
# graphql/schema.graphql
scalar AWSDate
type Book {
id: ID!
title: String!
subtitle: String
isbn: String
authors: [String]
publishedDate:AWSDate
thumbnailLink: String
}
type Mutation {
addBookFromIsbn(isbn: String!): Book
}
type Query {
allBooks: [Book]
}
AWSDate是在AppSync中默认可用的日期类型的标量定义。
只要在模式定义中使用作为类型,就不必重新定义标量,但在这里,我们定义了它以便让后面提到的代码生成器识别。
可以在这里找到可用于AppSync的标量类型的列表。
LambdaHandler的实现
接下来,我们将实现将LambdaHandler部署为Lambda函数。
这次的Lambda函数将根据接收到的ISBN,向Google Books API发送查询请求,并将获取到的图书信息存储到DynamoDB中。
使用GraphQL模式生成TypeScript类型定义
首先,使用一个名为代码生成器的工具,从GraphQL模式生成TypeScript类型定义代码。
创建配置文件
在项目根目录下创建用于决定代码生成器行为的配置文件。
所创建的源代码如下所示。
import { CodegenConfig } from “@graphql-codegen/cli”;const config: CodegenConfig = {
overwrite: true,
schema: “./graphql/schema.graphql”,
generates: {
“./lambda/graphql.ts”: {
plugins: [“typescript”],
},
},
config: {
skipTypename: true,
useTypeImports: true,
scalars: {
AWSDate: “Date”,
},
},
};
export default config;
我稍微来看一下。
指定获取模式信息的路径。
schema: "./graphql/schema.graphql",
本次操作中,我们将指定一个文件作为获取模式信息的来源,该文件中包含了在前面的步骤中创建的GraphQL模式定义。
通常情况下,我们会先创建和发布一个AppSync终端节点,并指定它作为获取数据的来源,然后生成用于各种服务器和客户端的类型定义。但由于本次需同时部署Lambda函数,我们将从本地文件中的定义的模式信息来生成类型。
指定生成的文件路径和用于生成的插件
generates: {
"./lambda/graphql.ts": {
plugins: ["typescript"],
},
},
生成器会通过键来记录生成文件的路径以及使用的插件信息。
换句话说,可以使用多个插件同时生成不同的文件。
关于插件,在Lambda的环境中,我们只使用基于模式的类型定义,因此只使用@graphql-codegen/typescript。
请指定选项设置。
config: {
skipTypename: true,
useTypeImports: true,
scalars: {
AWSDate: "Date",
},
},
我們將指定有關生成的選項設定。
重点在于标量,并且这是指定将定义的标量转换为TypeScript上的哪种类型的指令。
这次我们指示代码生成器将AWSDate转换为Date类型,因为代码生成器无法默认识别它。对于其他类型,由于默认已确定转换的类型,因此不需要特别指定。
执行源代码生成
如果设置文件准备好了,可以使用以下命令来执行代码生成。
npx graphql-codegen --config codegen.ts
运行后将生成一个 lambda/graphql.ts 文件。其中包含可在 TypeScript 中使用的类型定义,因此可以在 LambdaHandler 的源代码中导入并使用。
Lambda处理程序的实现
接下来我们要创建用作Lambda Handler的源代码。
Lambda Handler的源代码将在CDK项目中创建,并在未来通过相对路径被指定在堆栈代码中。
我制作了以下的源代码。
import { DynamoDBClient, PutItemCommand } from “@aws-sdk/client-dynamodb”;
import { marshall } from “@aws-sdk/util-dynamodb”;
import axios from “axios”;
import { randomUUID } from “crypto”;
import { Book, MutationAddBookFromIsbnArgs } from “./graphql”;
const httpClient = axios.create({
baseURL: “https://www.googleapis.com/books”,
});
const dbClient = new DynamoDBClient({});
export const handler: AppSyncResolverHandler< MutationAddBookFromIsbnArgs, Book > = async (event, context) => {
try {
const response = await httpClient.get(
“/v1/volumes”,
{
params: {
q: `isbn:${event.arguments.isbn}`,
},
}
);
const responseData = response.data.items[0];
if (!responseData) {
throw new Error(“无法从GoogleBookApi获取数据”);
}
// 注册到DynamoDb
let data: Book = {
id: randomUUID(),
title: responseData.volumeInfo?.title,
subtitle: responseData.volumeInfo?.subtitle ?? “”,
isbn: event.arguments.isbn,
authors: responseData.volumeInfo?.authors ?? [],
publishedDate: responseData.volumeInfo?.publishedDate ?? null,
thumbnailLink: responseData.volumeInfo?.imageLinks?.thumbnail ?? “”,
};
await dbClient.send(
new PutItemCommand({
TableName: “books”,
Item: marshall(data),
})
);
return data;
} catch (err) {
console.error(err);
throw new Error(`数据注册失败。`);
}
};
interface GoogleApiResponseData {
items: BookItem[];
kind: string;
totalItems: number;
}
interface BookItem {
id: string;
volumeInfo: {
title: string;
subtitle?: string;
authors?: string[];
publishedDate?: Date;
imageLinks?: {
thumbnail?: string;
};
};
}
关于内容,因为与本次主题无关,所以略过不提。关键在于使用代码生成器生成的类型来匹配处理程序的参数和返回值类型定义,以使其与在模式中定义的 addBookFromIsbn 的变异类型定义相匹配。
export const handler: AppSyncResolverHandler<MutationAddBookFromIsbnArgs, Book>
使用AppSyncResolverHandler类型定义一个作为export的函数,将第一个类型参数设为Mutation参数的类型,将第二个类型参数设为返回值的类型。
堆栈的实现
最终我们将在栈的源代码中添加资源定义。下面给出了创建的源代码。
Duration,
Expiration,
RemovalPolicy,
Stack,
StackProps
} from “aws-cdk-lib”;
导入 {
AuthorizationType,
FieldLogLevel,
GraphqlApi,
MappingTemplate
} from “@aws-cdk/aws-appsync-alpha”;
导入 {构造} from “constructs/lib”;
导入 {AttributeType,表} from “aws-cdk-lib/aws-dynamodb”;
导入 {NodejsFunction} from “aws-cdk-lib/aws-lambda-nodejs”;
导入 {RetentionDays} from “aws-cdk-lib/aws-logs”;
导入 {SchemaFile} from “@aws-cdk/aws-appsync-alpha/lib/schema”;
导出类 EngGrpCdkAppsyncStack 扩展 Stack {
构造函数(scope: 构造, id: 字符串,props?: StackProps) {
超(范围,id,道具);
// 创建DynamoDB表
const 表=新表(this,“DynamoDbTable”,{
表名: “书籍”,
分区键: {
名称:“id”,
类型:AttributeType.STRING,
},
移除策略:RemovalPolicy.DESTROY,
});
// 定义Lambda函数
const lambdaHandler = new NodejsFunction(this, “LambdaHandler”, {
entry: “lambda/appsync-resolver.ts”,
logRetention: RetentionDays.ONE_DAY,
});
// 授予DynamoDB读写权限
表.grantReadWriteData(lambdaHandler);
// 定义AppSync资源
const api = new GraphqlApi(this, “GraphqlApi”, {
名称:“eng_group_cdk_demo”,
模式:从Asset.文件(“./graphql/schema.graphql”)的SchemaFile,
授权配置: {
默认授权: {
授权类型: AuthorizationType.API_KEY,
apiKeyConfig: {
过期: Expiration.after(Duration.days(365)),
},
},
},
日志配置: {
fieldLogLevel: FieldLogLevel.ALL,
保留:RetentionDays.ONE_DAY,
},
});
// 创建将查询“allBooks”与DynamoDB关联的数据源
const dataSource1 = api.addDynamoDbDataSource(“DataSource1”, 表);
// 定义解析器
dataSource1.createResolver(“Resolver1”, {
类型名称: “查询”,
字段名称: “allBooks”,
请求映射模板: MappingTemplate.dynamoDbScanTable(),
响应映射模板: MappingTemplate.dynamoDbResultList(),
});
// 创建将Mutation“addBookFromIsbn”与Lambda函数关联的数据源
const dataSource2 = api.addLambdaDataSource(“DataSource2”, lambdaHandler);
// 定义解析器
dataSource2.createResolver(“Resolver2”, {
类型名称: “突变”,
字段名称: “addBookFromIsbn”,
});
};
};
我们将进行分解并逐步探讨。
添加DynamoDB的定义
定义一个名为”书”的表格。
const table = new Table(this, "DynamoDbTable", {
tableName: "books",
partitionKey: {
name: "id",
type: AttributeType.STRING,
},
removalPolicy: RemovalPolicy.DESTROY,
});
增加Lambda函数的定义
通过使用NodejsFunction构造函数,可以将使用TypeScript实现的LambdaHandler源代码进行Transpile和Bundle,并将其部署为Lambda函数。
在entry中,将使用相对于项目根目录的路径来记录在前面步骤中创建的Lambda Handler源代码文件(lambda/appsync-resolver.ts)的路径。
const lambdaHandler = new NodejsFunction(this, "LambdaHandler", {
entry: "lambda/appsync-resolver.ts",
logRetention: RetentionDays.ONE_DAY,
});
将DynamoDb表的读/写权限授予Lambda角色
由于Lambda函数需要与DynamoDB进行交互,因此我们将赋予Lambda函数角色对于我们创建的book表的读写权限。虽然到目前为止我们还没有具体定义角色,但是如果在Lambda函数定义时省略了角色的指定,系统会自动创建一个角色。
table.grantReadWriteData(lambdaHandler);
新增AppSync GraphQL API的定义
在GraphQL API中,需要定义模式。
本次将通过读取在前面步骤中创建的定义文件(graphql/schema.graphql)来进行设置。
另外,本次所使用的 API 認證方式為 API 金鑰(API Key)方式。此外,還可以與 AWS Cognito 進行整合,進行認證操作。
const api = new GraphqlApi(this, "GraphqlApi", {
name: "eng_group_cdk_demo",
schema: SchemaFile.fromAsset("./graphql/schema.graphql"),
authorizationConfig: {
defaultAuthorization: {
authorizationType: AuthorizationType.API_KEY,
apiKeyConfig: {
expires: Expiration.after(Duration.days(365)),
},
},
},
logConfig: {
fieldLogLevel: FieldLogLevel.ALL,
retention: RetentionDays.ONE_DAY,
},
});
创建将allBooks查询与DynamoDB关联的解析器。
为了解决基于模式定义的allBooks查询与指定数据源的DynamoDB的book表,我们定义一个解析器。
在Resolver中,需要使用VTL语言定义一种叫作Mapping Template的方法,用于转换GraphQL请求和响应以及后端数据的方式。然而,AppSync的L2构造函数可以在简单的情况下生成Mapping Template,而无需编写任何代码。这非常简便。
const dataSource1 = api.addDynamoDbDataSource("DataSource1", table);
dataSource1.createResolver("Resolver1", {
typeName: "Query",
fieldName: "allBooks",
requestMappingTemplate: MappingTemplate.dynamoDbScanTable(),
responseMappingTemplate: MappingTemplate.dynamoDbResultList(),
});
创建一个将addBookFromIsbn的mutation与DynamoDB相关联的解析器。
定义一个解析器,用于解析将addBookFromIsbn变异定义为模式并指定为数据源的Lambda函数。
如果在数据源中使用Lambda函数,则可以使用Direct Lambda Resolvers,从而省去了映射模板定义,因此无需指定模板。
const dataSource2 = api.addLambdaDataSource("DataSource2", lambdaHandler);
dataSource2.createResolver("Resolver2", {
typeName: "Mutation",
fieldName: "addBookFromIsbn",
});
执行资源创建
因为到此为止,我们已经准备好了必要的源代码,所以我们可以使用cdk deploy命令在AWS上实际创建资源。
npx cdk deploy --profile enggrp
执行命令后,会在提示符上显示资源的变更差异以及“是否要部署这些更改(y/n)?”的消息。如果没有问题,请输入“y”并按下回车。
只要没有发生错误,使用此定义,资源将在AWS上构建。
从本地终端发送请求
最后,作为确认操作,我们将从本地终端发送请求。
我们将使用Powershell Core的Invoke-RestMethod命令来发送请求。
首先,使用AWS CLI确认所创建的GraphQL API的终端节点URI和API密钥。
# API IDとUrlを確認
aws appsync list-graphql-apis --query "graphqlApis[?name=='eng_group_cdk_demo'].{apiId:apiId,uri:uris.GRAPHQL}" --profile enggrp
# API IDを使用してAPI KEYを確認
aws appsync list-api-keys --api-id ${取得したAPI ID} --profile enggrp
在PowerShell中执行以下脚本时,请嵌入确认的URI和API密钥
$uri = "${確認したAPI URL} "
$headers = @{
"X-API-KEY" = "${確認したAPI KEY} "
}
$contentType = "application/json"
$allBooksQuery = @{"query" = @"
query {
allBooks {
id
title
subtitle
isbn
authors
publishedDate
thumbnailLink
}
}
"@
} | ConvertTo-Json
$addBookFromIsbn = @{"query" = @"
mutation {
addBookFromIsbn(isbn:"@isbn") {
id
title
subtitle
isbn
authors
publishedDate
thumbnailLink
}
}
"@
} | ConvertTo-Json
# 2つのISBNコードで書籍登録Mutationを実行
Invoke-RestMethod -Method Post -Uri $uri -ContentType $contentType -Headers $headers -Body ($addBookFromIsbn -replace "@isbn","9784822262969") > $null
Invoke-RestMethod -Method Post -Uri $uri -ContentType $contentType -Headers $headers -Body ($addBookFromIsbn -replace "@isbn","9784295010654") > $null
# 全書籍取得Queryを実行して結果を表示
(Invoke-RestMethod -Method Post -Uri $uri -ContentType $contentType -Headers $headers -Body $allBooksQuery) | ConvertTo-Json -Depth 10
执行后,我确认了allBooks查询返回了以下结果。
“data”: {
“allBooks”: [
{
“id”: “751494de-0769-454a-a4dc-466c479d7bd5”,
“title”: “Amazon Web Services基础从网络和服务器搭建开始”,
“subtitle”: “触摸学习云基础设施”,
“isbn”: “9784822262969”,
“authors”: [
“玉川憲”,
“片山暁雄”,
“今井雄太”
],
“publishedDate”: “2014-07-22”,
“thumbnailLink”: “http://books.google.com/books/content?id=zMzgoAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api”
},
{
“id”: “0aac1d4a-f622-4b60-a75c-c9b1e151562d”,
“title”: “改版彻底攻略AWS认证解决方案架构师 – 课本[SAA-C02]对应”,
“subtitle”: “”,
“isbn”: “9784295010654”,
“authors”: [
“鸟谷部昭宽”,
“宫口光平”,
“菖蒲淳司”
],
“publishedDate”: “2021-01-08”,
“thumbnailLink”: “http://books.google.com/books/content?id=a7USEAAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api”
}
]
}
}
aws dynamodb scan --table-name "books" --profile enggrp
我也确认了数据库中有数据被插入。
“Items”: [
{
“thumbnailLink”: {
“S”: “http://books.google.com/books/content?id=zMzgoAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api”
},
“isbn”: {
“S”: “9784822262969”
},
“subtitle”: {
“S”: “通过实践学习云基础设施”
},
“id”: {
“S”: “751494de-0769-454a-a4dc-466c479d7bd5”
},
“publishedDate”: {
“S”: “2014-07-22”
},
“title”: {
“S”: “Amazon Web Services从基础开始的网络和服务器构建”
},
“authors”: {
“L”: [
{
“S”: “玉川憲”
},
{
“S”: “片山暁雄”
},
{
“S”: “今井雄太”
}
]
}
},
{
“thumbnailLink”: {
“S”: “http://books.google.com/books/content?id=a7USEAAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api”
},
“isbn”: {
“S”: “9784295010654”
},
“subtitle”: {
“S”: “”
},
“id”: {
“S”: “0aac1d4a-f622-4b60-a75c-c9b1e151562d”
},
“publishedDate”: {
“S”: “2021-01-08”
},
“title”: {
“S”: “完全攻略AWS认证解决方案架构师-Associate教科书[SAA-C02]支持版”
},
“authors”: {
“L”: [
{
“S”: “鸟谷部昭寛”
},
{
“S”: “宫口光平”
},
{
“S”: “菖蒲淳司”
}
]
}
}
],
“Count”: 2,
“ScannedCount”: 2,
“ConsumedCapacity”: null
}
作者在本文的结尾留下了一段话。
如果你搜索如何构建AppSync,你会经常看到Amplify CLI,但是我们发现如果使用L2构造,CDK也能够相当简单地实现。如果能够如此简单地实现,选择GraphQL作为API的门槛会大大降低吧。从现在开始,我打算将其嵌入到实际应用中并尽可能熟练使用。