使用 AppSync 和 DynamoDB 构建无服务器数据 API
介绍
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
介绍
以下教程将指导您如何使用 AWS DynamoDB 作为后端数据存储,并使用 AWS AppSync 创建 GraphQL 接口,从而构建一个无服务器数据 API。所有步骤和代码示例均以 YAML 格式编写。我尽可能使用 AWS SAM 语法,在不支持的情况下则使用 CloudFormation。SAM 本身并不提供 AppSync 的语法,但它的优点在于您仍然可以使用它来构建和部署包含 CloudFormation 的模板。这意味着,虽然您无法使用 SAM 为其他服务提供的简写方式,但您仍然可以继续使用相同的 SAM 设置和命令。我认为 SAM 完全可以像支持 API Gateway 一样支持 AppSync,我希望 AWS 未来能够添加此功能。
您需要在计算机上安装 AWS SAM CLI 才能完成本教程。如果您尚未安装,可以按照 AWS 在此链接中提供的说明进行操作。
我尽量简化了这个示例,但它仍然非常强大。该应用程序允许通过 GraphQL 接口创建、更新、删除和选择 JSON 文档。文档存储在 DynamoDB 的 Map 数据类型中,并使用分区键和排序键进行索引。通过巧妙地构建这些键,它可以应用于许多场景。您可以使用复合键。DynamoDB 中的每个项目(记录)最大可达 400kb,因此每个项目也可以存储相当多的数据。
1) DynamoDB - 创建您的表。
此表将作为 API 的后端数据存储。APIName 是传递给模板的参数,它将被复用作表名和 API 名称。由于 DynamoDB 是 NoSQL 数据库,因此无需预先定义模式,只需定义主键列即可。用于存储 JSON 的列将在向表中写入第一条记录时添加。
DynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Ref APIName
ProvisionedThroughput:
WriteCapacityUnits: 5
ReadCapacityUnits: 5
AttributeDefinitions:
- AttributeName: "pk1"
AttributeType: "S"
- AttributeName: "sk1"
AttributeType: "S"
KeySchema:
- AttributeName: "pk1"
KeyType: "HASH"
- AttributeName: "sk1"
KeyType: "RANGE"
2) IAM
您需要创建多个 AWS IAM 策略和角色,以允许 AppSync 访问 DynamoDB 表并记录到 CloudWatch。
2.1) AppSync 到 DynamoDB
此策略将允许附加主体查询已创建的 DynamoDB 表。CloudFormation 子函数允许您从多个输入构建单个字符串。如果您希望在子函数中引用变量,则需要将其用 ${} 包裹起来。遵循最小权限原则,我仅包含了 AppSync 解析器所需的操作,不包含其他操作。
PolicyDynamoDB:
Type: AWS::IAM::ManagedPolicy
Properties:
Path: /service-role/
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:DeleteItem
Resource:
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${DynamoDBTable}
使用 sts:AssumeRole,通过创建新角色将创建的策略附加到 AppSync 服务。
RoleAppSyncDynamoDB:
Type: AWS::IAM::Role
Properties:
ManagedPolicyArns:
- !Ref PolicyDynamoDB
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- appsync.amazonaws.com
2.2) 日志记录到 CloudWatch
要允许 AppSync 访问 CloudWatch,您需要使用 `sts:AssumeRole` 将提供的 `AWSAppSyncPushToCloudWatchLogs` 策略附加到 AppSync 服务。这是一个 AWS 托管策略,您可以在创建新角色时使用它。AppSync 服务承担该角色后,即可创建日志组和日志流,并将事件记录到 CloudWatch。
RoleAppSyncCloudWatch:
Type: AWS::IAM::Role
Properties:
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- appsync.amazonaws.com
IAM 资源和 DynamoDB 表已创建完成。现在我们可以开始创建 AppSync 资源了。
3) AppSync
现在我们可以开始配置 AppSync 资源了。您需要创建以下资源才能拥有一个功能齐全的 API:
- API 标头 - 用于存放以下 API 组件,包含名称、身份验证和监控级别等详细信息。
- API模式——这里使用GraphQL模式定义语言(SDL)对API定义进行建模。
- 数据源 – 数据源是提供有关数据存储位置的详细信息的组件。
- 解析器——解析器将模式的各个部分与匹配的数据源连接起来。它们还使用 Apache Velocity 模板语言 (VTL) 提供两者之间必要的转换。
- API密钥 - 在本教程中,我使用API密钥来控制对API的访问。您也可以使用IAM或Cognito。
确实需要指定的内容很多,我认为 SAM 可以像 API Gateway 那样,对其中一些进行抽象化处理。通过控制台,你可以使用向导创建一个新的 AppSync API,并将其指向 DynamoDB 表,这实际上只是以 IAC 格式获取类似的东西。而目前,你必须使用 CloudFormation 单独指定这些内容。
3.1) API 标头
此资源使用传入的参数(与 DynamoDB TableName 参数使用的参数相同)创建 API 标头。LogConfig 部分使用我们之前创建的角色设置 CloudWatch 日志记录。我已将其设置为最高日志级别,但如果日志量过大,您可以通过调整 ExcludeVerboseContent 和 FieldLogLevel 属性的参数来降低日志级别。将 FieldLogLevel 设置为 NONE、ERROR 或 ALL 可以减少写入的日志量。
GraphQLApi:
Type: AWS::AppSync::GraphQLApi
Properties:
Name: !Ref APIName
AuthenticationType: API_KEY
LogConfig:
CloudWatchLogsRoleArn: !GetAtt RoleAppSyncCloudWatch.Arn
ExcludeVerboseContent: FALSE
FieldLogLevel: ALL
3.2) API架构
GraphQL schema 是所有 GraphQL 平台的基础。它可以直接嵌入到 YAML 模板中,也可以存储在 S3 中并在模板中引用。为了简单起见,我将其保留在模板中。
在 schema 中,您可以指定数据类型以及 mutation 和 query 接口。query 用于从 API 读取数据,而 mutation 用于操作底层数据。每个 mutation 和 readData 查询都作用于单个记录。readAllPKData 查询将返回特定 pk1 值的所有项。根据您对 JSON 项的索引方式,例如,您可以使用此查询返回特定父键的所有子记录。
GraphQLApiSchema:
Type: AWS::AppSync::GraphQLSchema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
Definition: |
schema {
query: Query
mutation: Mutation
}
type Data {
data: [AWSJSON]
pk1: String
sk1: String
}
type DataCollection {
items: [Data]
nextToken: String
}
input WriteDataInput {
pk1: String!
sk1: String!
data: [AWSJSON]!
}
input UpdateDataInput {
pk1: String!
sk1: String!
data: [AWSJSON]!
}
type Mutation {
writeData(input: WriteDataInput!): Data
updateData(input: UpdateDataInput!): Data
deleteData(pk1: String!, sk1: String!): Data
}
type Query {
readData(pk1: String!, sk1: String!): Data
readAllPKData(pk1: String!): DataCollection
}
3.3)数据源
API 创建完成后,我们可以将 DynamoDB 表作为解析器的数据源。本教程仅指定了一个数据源,但 GraphQL 的优势在于,您可以在同一个 API 中使用多个数据源。AppSync 还支持 Aurora、AWS Elasticsearch Service 和 Lambda 作为原生数据源。您还可以通过 Lambda 数据源引用其他 AWS 服务。
GraphQLDataSource:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
Name: !Ref APIName
Type: AMAZON_DYNAMODB
ServiceRoleArn: !GetAtt RoleAppSyncDynamoDB.Arn
DynamoDBConfig:
TableName: !Ref DynamoDBTable
AwsRegion: !Sub ${AWS::Region}
3.4)解析器
解析器包含将每个查询和变更映射到底层数据源的逻辑,以及所需的任何转换或逻辑。查询或变更的名称在 `FieldName` 参数中指定。每个解析器只能附加一个数据源。解析器使用名为 Apache Velocity Template Language (VTL) 的脚本语言来编码所有逻辑。`RequestMappingTemplate` 参数指定请求和数据源之间的任何转换。`ResponseMappingTemplate` 参数指定数据源和响应之间的任何转换。
AppSyncResolverReadData:
Type: AWS::AppSync::Resolver
DependsOn: GraphQLApiSchema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Query
FieldName: readData
DataSourceName: !GetAtt GraphQLDataSource.Name
RequestMappingTemplate: >
{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"pk1": $util.dynamodb.toDynamoDBJson($ctx.args.pk1),
"sk1": $util.dynamodb.toDynamoDBJson($ctx.args.sk1),
},
}
ResponseMappingTemplate: $util.toJson($context.result)
AppSyncResolverReadAllPKData:
Type: AWS::AppSync::Resolver
DependsOn: GraphQLApiSchema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Query
FieldName: readAllPKData
DataSourceName: !GetAtt GraphQLDataSource.Name
RequestMappingTemplate: >
{
"version" : "2017-02-28",
"operation" : "Query",
"query" : {
"expression": "pk1 = :pk1",
"expressionValues" : {
":pk1" : $util.dynamodb.toDynamoDBJson($ctx.args.pk1),
}
}
}
ResponseMappingTemplate: $util.toJson($context.result)
AppSyncResolverWriteData:
Type: AWS::AppSync::Resolver
DependsOn: GraphQLApiSchema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: writeData
DataSourceName: !GetAtt GraphQLDataSource.Name
RequestMappingTemplate: >
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"pk1": $util.dynamodb.toDynamoDBJson($ctx.args.input.pk1),
"sk1": $util.dynamodb.toDynamoDBJson($ctx.args.input.sk1),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
"condition": {
"expression": "attribute_not_exists(#pk1) AND attribute_not_exists(#sk1)",
"expressionNames": {
"#pk1": "pk1",
"#sk1": "sk1",
},
},
}
ResponseMappingTemplate: $util.toJson($context.result)
AppSyncResolverUpdateData:
Type: AWS::AppSync::Resolver
DependsOn: GraphQLApiSchema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: updateData
DataSourceName: !GetAtt GraphQLDataSource.Name
RequestMappingTemplate: >
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"pk1": $util.dynamodb.toDynamoDBJson($ctx.args.input.pk1),
"sk1": $util.dynamodb.toDynamoDBJson($ctx.args.input.sk1),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
}
ResponseMappingTemplate: $util.toJson($context.result)
AppSyncResolverDeleteData:
Type: AWS::AppSync::Resolver
DependsOn: GraphQLApiSchema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: deleteData
DataSourceName: !GetAtt GraphQLDataSource.Name
RequestMappingTemplate: >
{
"version": "2017-02-28",
"operation": "DeleteItem",
"key": {
"pk1": $util.dynamodb.toDynamoDBJson($ctx.args.pk1),
"sk1": $util.dynamodb.toDynamoDBJson($ctx.args.sk1),
},
}
ResponseMappingTemplate: $util.toJson($context.result)
3.5) API密钥
API 的访问权限由 API 密钥控制。AppSync 还支持通过 AWS Cognito 和 IAM 进行访问。密钥的有效期由一个参数控制,该参数以 Unix 时间戳 (Epoch 时间戳) 为单位。您只需传入一个数字,该数字对应于您希望密钥过期的 Unix 时间戳即可。
AppSyncAPIKey:
Type: AWS::AppSync::ApiKey
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
Expires: !Ref APIKeyExpiration
4)参数
部署时必须提供 APIName 和 APIKeyExpiration。APIName 用于生成 API 名称和 DynamoDB 源表名称。
Parameters:
APIName:
Type: String
APIKeyExpiration:
Type: Number
5)输出
要使用 API,您需要 API 密钥和 GraphQL URL。这些信息将在 SAM 部署完成后输出。
Outputs:
APIKey:
Description: API Key
Value: !GetAtt AppSyncAPIKey.ApiKey
GraphQL:
Description: GraphQL URL
Value: !GetAtt GraphQLApi.GraphQLUrl
6)测试
使用 SAM 模板输出的 API 密钥和 URL,您可以调用 API。以下是一些格式化的输入示例,希望能对您有所帮助。
6.1)突变
使用此示例向 API 写入数据。下一节中将使用查询变量来指定输入数据。
mutation ($WriteDataInput: WriteDataInput!, $UpdateDataInput: UpdateDataInput!) {
writeData(input: $WriteDataInput) {
pk1
sk1
data
}
updateData(input: $UpdateDataInput) {
pk1
sk1
data
}
deleteData(pk1: "DBS", sk1: "6") {
data
pk1
sk1
}
}
6.2)查询变量
使用查询变量构建输入数据。
{
"UpdateDataInput": {
"pk1": "DBS",
"sk1": "3",
"data": [
"{\"M\":{\"answer\":{\"S\":\"0\"},\"choice\":{\"S\":\"Add read replicas to the database.\"}}}",
"{\"M\":{\"answer\":{\"S\":\"0\"},\"choice\":{\"S\":\"Put an Elasticache Redis cache in front of the database.\"}}}",
"{\"M\":{\"answer\":{\"S\":\"1\"},\"choice\":{\"S\":\"Put an Amazon SQS queue in front of the database.\"}}}",
"{\"M\":{\"answer\":{\"S\":\"0\"},\"choice\":{\"S\":\"Put an Elasticache Memcached cache in front of the database.\"}}}"
]
},
"WriteDataInput": {
"pk1": "DBS",
"sk1": "4",
"data": [
"{\"M\":{\"answer\":{\"S\":\"0\"},\"choice\":{\"S\":\"Add read replicas to the database.\"}}}",
"{\"M\":{\"answer\":{\"S\":\"0\"},\"choice\":{\"S\":\"Put an Elasticache Redis cache in front of the database.\"}}}",
"{\"M\":{\"answer\":{\"S\":\"1\"},\"choice\":{\"S\":\"Put an Amazon SQS queue in front of the database.\"}}}",
"{\"M\":{\"answer\":{\"S\":\"0\"},\"choice\":{\"S\":\"Put an Elasticache Memcached cache in front of the database.\"}}}"
]
}
}
6.3)查询
请使用以下示例通过 API 读取数据。
query {
readData(pk1: "DBS", sk1: "1") {
pk1
sk1
data
}
readAllPKData(pk1: "DBS") {
nextToken
items {
data
pk1
sk1
}
}
}
7)结论
您可以在此 GitHub 仓库中找到完整的 template.yaml 文件:
https://github.com/thomasmilner/serverlessdataapi
我要感谢https://twitter.com/sbstjn?s=20和他的代码库,它们为我完成这项工作提供了极好的参考。
https://github.com/sbstjn/appsync-example-dynamodb
如有任何意见或疑问,请随时联系我。我非常乐意提供帮助。
文章来源:https://dev.to/aws-builders/build-a-serverless-data-api-with-appsync-and-dynamodb-3jmi