通过AWS AppSync+Terraform自动创建无服务器的Web应用程序
首先
因为听到了一个声音,它说如果你要做无服务器,不仅仅要了解API Gateway,还要了解AppSync,所以我试着制作了一个。
也许只要习惯了就会很方便。学习GraphQL和VTL会有一些成本。
全体成员组成
以下是构建的结构。由于使用了AppSync,所以就连Lambda也不需要了。静态内容从S3获取,而AppSync从DynamoDB获取数据。虽然不需要经过CloudFront,但由于AppSync无法处理CORS,为了使静态内容与同一来源,需要将其挂载到CloudFront上。
现在,我们来逐个查看每个Terraform资源。
Terraform资源定义
DynamoDB – 动态数据库
DynamoDB将用户ID作为哈希键创建表。
在这里,注册姓名和年龄。
resource "aws_dynamodb_table" "user" {
name = local.dynamodb_table_name
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
}
resource "aws_dynamodb_table_item" "user" {
count = 1
table_name = aws_dynamodb_table.user.name
hash_key = aws_dynamodb_table.user.hash_key
range_key = aws_dynamodb_table.user.range_key
item = <<ITEM
{
"id": {"S": "00001"},
"name": {"S": "Taro"},
"age": {"S": "35"}
}
ITEM
}
S3储存桶
定义包含静态内容的S3存储桶。
通常情况下,如果要使用CloudFront,最好将其设置为私有以防止直接访问,并设置原始资源访问标识(Origin Access Identity)。但由于这次只是试用,所以我简略了这一步骤。
另外,为了方便起见,本次我们将使用 Vue.js 2.x 的CDN版来创建Web应用的内容。
resource "aws_s3_bucket" "contents" {
bucket = local.bucket_name
acl = "public-read"
website {
index_document = "index.html"
}
}
resource "aws_s3_bucket_object" "index" {
bucket = aws_s3_bucket.contents.id
source = "../contents/index.html"
key = "contents/index.html"
acl = "public-read"
content_type = "text/html"
etag = filemd5("../contents/index.html")
}
resource "aws_s3_bucket_object" "app" {
bucket = aws_s3_bucket.contents.id
source = "../contents/app.js"
key = "contents/app.js"
acl = "public-read"
content_type = "text/javascript"
etag = filemd5("../contents/app.js")
}
AppSync 同步应用
这次的关键是AppSync。
可以通过设置 API 密钥作为标头来访问。在这种情况下,建议同时定义 aws_appsync_api_key。可以使用 aws_appsync_api_key.test.key 来引用密钥信息,将其输出会更好。
resource "aws_appsync_graphql_api" "test" {
name = local.appsync_graphql_api_name
authentication_type = "API_KEY"
schema = data.local_file.graphql_schema.content
}
data "local_file" "graphql_schema" {
filename = "./appsync_schema.graphql"
}
resource "aws_appsync_api_key" "test" {
api_id = aws_appsync_graphql_api.test.id
description = "${var.prefix}用APIキー"
}
resource "aws_appsync_datasource" "dynamodb" {
api_id = aws_appsync_graphql_api.test.id
name = local.appsync_dynamodb_datasource_name
service_role_arn = aws_iam_role.appsync.arn
type = "AMAZON_DYNAMODB"
dynamodb_config {
table_name = local.dynamodb_table_name
}
}
resource "aws_appsync_resolver" "createuser" {
api_id = aws_appsync_graphql_api.test.id
field = "createUser"
type = "Mutation"
data_source = aws_appsync_datasource.dynamodb.name
request_template = file("./createuser_request.template")
response_template = file("./createuser_response.template")
}
resource "aws_appsync_resolver" "user" {
api_id = aws_appsync_graphql_api.test.id
field = "user"
type = "Query"
data_source = aws_appsync_datasource.dynamodb.name
request_template = file("./user_request.template")
response_template = file("./user_response.template")
}
在 `aws_appsync_datasource` 中进行 DynamoDB 连接的配置,但需要将访问 DynamoDB 的权限授予服务角色,因此定义如下所示。
resource "aws_iam_role" "appsync" {
name = local.appsync_role_name
assume_role_policy = data.aws_iam_policy_document.appsync_assume.json
}
data "aws_iam_policy_document" "appsync_assume" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "Service"
identifiers = [
"appsync.amazonaws.com",
]
}
}
}
resource "aws_iam_role_policy_attachment" "appsync" {
role = aws_iam_role.appsync.name
policy_arn = aws_iam_policy.appsync_custom.arn
}
resource "aws_iam_policy" "appsync_custom" {
name = local.appsync_policy_name
policy = data.aws_iam_policy_document.appsync_custom.json
}
data "aws_iam_policy_document" "appsync_custom" {
statement {
effect = "Allow"
actions = [
"dynamodb:BatchGetItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
]
resources = [
"*",
]
}
}
在 AWS AppSync GraphQL API 中定义了以下架构。
schema {
query: Query
mutation: Mutation
}
type Query {
user(id: ID!): User
}
type Mutation {
createUser(name: String!): User
}
type User {
id: ID!
name: String!
age: String!
}
现在,数据源和模式的定义已经完成,我们要将其设置到解析器中。
在解析器中,可以使用VTL来控制请求和响应的信息,如下所示。
{
"version" : "2017-02-28",
"operation" : "PutItem",
"key" : {
"id" : { "S" : "$util.autoId()" }
},
"attributeValues" : {
"name" : { "S" : "${context.arguments.name}" },
}
}
$utils.toJson($context.result)
{
"version" : "2017-02-28",
"operation" : "GetItem",
"key" : {
"id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
}
}
$utils.toJson($ctx.result)
完成到这一步,我们可以确认AppSync的运行情况,接下来尝试进行正常性确认,步骤如下:
x-api-key 是之前分配的API密钥。
curl \
-w "\n" \
-H 'Content-Type: application/json' \
-H "x-api-key: xxx-xxxxxxxxxxxxxxxxxxxxxxxxxx" \
-X POST -d '
{
"query": "query { user(id: \"00001\") { id name } }"
}' \
https://xxxxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
curl \
-w "\n" \
-H 'Content-Type: application/json' \
-H "x-api-key: xxx-xxxxxxxxxxxxxxxxxxxxxxxxxx" \
-X POST -d '
{
"query": "mutation { createUser(name: \"Jiro-san\") { name } }"
}' \
https://xxxxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
云前缘
CloudFront的多源定义如下。AppSync的domain_name出现问题是因为Terraform没有一个能够正确获取AppSync domain_name属性的功能…遗憾。
resource "aws_cloudfront_distribution" "appsync" {
origin {
domain_name = trimsuffix(trimprefix(aws_appsync_graphql_api.test.uris["GRAPHQL"], "https://"), "/graphql")
origin_id = local.cloudfront_appsync_origin_id
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"]
}
}
origin {
domain_name = aws_s3_bucket.contents.bucket_regional_domain_name
origin_id = local.cloudfront_s3_origin_id
}
enabled = true
comment = "AppSync用CloudFront"
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.cloudfront_appsync_origin_id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "allow-all"
}
ordered_cache_behavior {
path_pattern = "/contents/*"
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.cloudfront_s3_origin_id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "allow-all"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
只要再走一步,就可以到达这里了。
靜態內容
请确保提到分配的 CloudFront 域名和 API 密钥,以以下方式构建 Vue.js 的静态内容。
虽然称之为GraphQL,但本质上只是向服务器发出JSON的POST请求,所以我们可以使用axios轻松拉取数据。
JavaScript的代码可能没有经过eslint处理,或者错误处理不够严谨,但这只是试验阶段,请不要在意这些问题。
<html>
<head>
<style>
[v-cloak] { display: none }
</style>
<meta charset="utf-8">
<title>Vue TEST</title>
</head>
<body>
<div id="myapp" v-cloak>
<input type="text" v-model="user_id" placeholder="ユーザID(5桁)を入力">
<button v-on:click="check_employee" v-bind:disabled="is_invalid">確認</button>
<table border="1" v-if="user_info">
<tr><th>id</th><th>name</th><th>age</th></tr>
<tr v-model="item"><td>{{ item['id'] }}</td><td>{{ item['name'] }}</td><td>{{ item['age'] }}</td></tr>
</table>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="/contents/app.js"></script>
</body>
</html>
const APIGATEWAY_INVOKE_URL = 'https://[CloudFrontのドメイン]/graphql'
const APPSYNC_API_KEY = '[払い出したAPIキー]'
const app = new Vue({
el: '#myapp',
data: {
user_info: false,
user_id: '',
is_invalid: true,
item: null
},
watch: {
user_id: function (newVal, oldVal) {
this.is_invalid = newVal.length !== 5
}
},
methods: {
check_employee: function () {
const headers = {
'x-api-key': `${APPSYNC_API_KEY}`
}
const body = {
query: `query{
user(id: "${this.user_id}\")
{
id
name
age
}
}`
}
axios
.post(`${APIGATEWAY_INVOKE_URL}`, body, { headers: headers })
.then(response => {
this.item = response.data.data.user
this.user_info = true
})
}
}
})
app.$mount('#app')
完成了!当你从浏览器访问CloudFront的内容时,试试看…
我动了!
虽然还没有深入研究过在GraphQL中可以实现多复杂的事情,但至少看起来比API Gateway的AWS服务集成更加灵活。