Terraform:如何从对象列表创建 API 网关端点和方法?
Terraform: How to create API Gateway endpoints and methods from a list of objects?
我想创建一个 terraform (v0.12+) 模块输出一个 AWS API 网关与 Lambda 集成。我不太明白如何(或者是否可能)遍历映射列表以动态输出资源。
用户应该能够像这样实例化模块:
module "api_gateway" {
source = "./apig"
endpoints = [
{
path = "example1"
method = "GET"
lambda = "some.lambda.reference"
},
{
path = "example1"
method = "POST"
lambda = "some.lambda.reference"
},
{
path = "example2"
method = "GET"
lambda = "another.lambda.reference"
}
]
}
从endpoints
界面,我要输出三个资源:
- 一个
aws_api_gateway_resource
其中 path_part = endpoint[i].path
- 一个
aws_api_gateway_method
其中 http_method = endpoint[i].method
- 一个
aws_api_gateway_integration
引用了
endpoint[i].lambda
,等等
Terraform 的 for_each
属性 似乎不够强大,无法处理这个问题。我知道 Terraform 还支持 for
循环和 for / in
循环,但我没能找到任何使用此类表达式进行资源声明的示例。
让我们从写出 endpoints
变量的声明开始,因为其余的答案取决于它是如何定义的:
variable "endpoints" {
type = set(object({
path = string
method = string
lambda = string
})
}
上面说的是endpoints
是一组对象,意思是物品的先后顺序并不重要。顺序无关紧要,因为无论如何我们都会在 API 中为每个对象创建单独的对象。
下一步是弄清楚如何从给定的数据结构转移到一个映射结构,其中每个键都是唯一的,每个元素都映射到您要生成的资源的一个实例。为此,我们必须定义我们想要的映射,我认为这里是:
- 每个不同的
path
一个aws_api_gateway_resource
。
- 每个不同的
path
和 method
对一个 aws_api_gateway_method
。
- 每个不同的
path
和 method
对一个 aws_api_gateway_integration
。
- 每个不同的
path
/method
/status_code
三元组一个 aws_api_gateway_integration_response
。
- 每个不同的
path
/method
/status_code
三元组一个 aws_api_gateway_method_response
。
所以看来我们在这里需要三个集合:第一个是所有路径的集合,第二个是从 path
+method
对到描述该方法的对象的映射,第三个是我们要建模的端点和状态代码的每个组合。
locals {
response_codes = toset({
status_code = 200
response_templates = {} # TODO: Fill this in
response_models = {} # TODO: Fill this in
response_parameters = {} # TODO: Fill this in
})
# endpoints is a set of all of the distinct paths in var.endpoints
endpoints = toset(var.endpoints.*.path)
# methods is a map from method+path identifier strings to endpoint definitions
methods = {
for e in var.endpoints : "${e.method} ${e.path}" => e
}
# responses is a map from method+path+status_code identifier strings
# to endpoint definitions
responses = {
for pair in setproduct(var.endpoints, local.response_codes) :
"${pair[0].method} ${pair[0].path} ${pair[1].status_code}" => {
method = pair[0].method
path = pair[0].path
method_key = "${pair[0].method} ${pair[0].path}" # key for local.methods
status_code = pair[1].status_code
response_templates = pair[1].response_templates
response_models = pair[1].response_models
response_parameters = pair[1].response_parameters
}
}
}
定义了这两个派生集合后,我们现在可以写出资源配置:
resource "aws_api_gateway_rest_api" "example" {
name = "example"
}
resource "aws_api_gateway_resource" "example" {
for_each = local.endpoints
rest_api_id = aws_api_gateway_rest_api.example.id
parent_id = aws_api_gateway_rest_api.example.root_resource_id
path_part = each.value
}
resource "aws_api_gateway_method" "example" {
for_each = local.methods
rest_api_id = aws_api_gateway_resource.example[each.value.path].rest_api_id
resource_id = aws_api_gateway_resource.example[each.value.path].resource_id
http_method = each.value.method
}
resource "aws_api_gateway_integration" "example" {
for_each = local.methods
rest_api_id = aws_api_gateway_method.example[each.key].rest_api_id
resource_id = aws_api_gateway_method.example[each.key].resource_id
http_method = aws_api_gateway_method.example[each.key].http_method
type = "AWS_PROXY"
integration_http_method = "POST"
uri = each.value.lambda
}
resource "aws_api_gateway_integration_response" "example" {
for_each = var.responses
rest_api_id = aws_api_gateway_integration.example[each.value.method_key].rest_api_id
resource_id = aws_api_gateway_integration.example[each.value.method_key].resource_id
http_method = each.value.method
status_code = each.value.status_code
response_parameters = each.value.response_parameters
response_templates = each.value.response_templates
# NOTE: There are some other arguments for
# aws_api_gateway_integration_response that I've left out
# here. If you need them you'll need to adjust the above
# local value expressions to include them too.
}
resource "aws_api_gateway_response" "example" {
for_each = var.responses
rest_api_id = aws_api_gateway_integration_response.example[each.key].rest_api_id
resource_id = aws_api_gateway_integration_response.example[each.key].resource_id
http_method = each.value.method
status_code = each.value.status_code
response_models = each.value.response_models
}
您可能还需要 aws_api_gateway_deployment
。为此,重要的是要确保它依赖于 all 我们在上面定义的 API 网关资源,以便 Terraform 将等到 API 完全配置在尝试部署之前:
resource "aws_api_gateway_deployment" "example" {
rest_api_id = aws_api_gateway_rest_api.example.id
# (whatever other settings are appropriate)
depends_on = [
aws_api_gateway_resource.example,
aws_api_gateway_method.example,
aws_api_gateway_integration.example,
aws_api_gateway_integration_response.example,
aws_api_gateway_method_response.example,
]
}
output "execution_arn" {
value = aws_api_gateway_rest_api.example.execution_arn
# Execution can't happen until the gateway is deployed, so
# this extra hint will ensure that the aws_lambda_permission
# granting access to this API will be created only once
# the API is fully deployed.
depends_on = [
aws_api_gateway_deployment.example,
]
}
API 撇开网关细节不谈,这种情况的一般程序是:
- 定义您的输入。
- 弄清楚如何从您的输入获取每个资源所需的每个实例一个元素的集合。
- 写
local
表达式来描述从输入到重复集合的投影。
- 编写
resource
块,其中 for_each
引用适当的本地值作为其重复值。
for
expressions, along with the flatten
and setproduct
函数是我们从结构中投影数据的主要工具,调用者可以方便地将输入变量提供给 for_each
表达式所需的结构。
API Gateway 具有特别复杂的数据模型,因此在 Terraform 语言中表达它的所有可能性可能需要比其他服务可能需要更多的投影和其他转换。因为 OpenAPI 已经定义了一种灵活的声明性语言来定义 REST APIs 并且 API Gateway 已经原生支持它,所以让您的 endpoints
变量采用一个标准的 OpenAPI 定义并将其直接传递给 API 网关,从而获得 OpenAPI 模式格式的所有表现力,而无需自己在 Terraform 中实现所有细节:
variable "endpoints" {
# arbitrary OpenAPI schema object to be validated by API Gateway
type = any
}
resource "aws_api_gateway_rest_api" "example" {
name = "example"
body = jsonencode(var.endpoints)
}
即使您仍然希望 endpoints
变量成为更高级别的模型,您也可以考虑使用 Terraform 语言构造 一个 OpenAPI 模式通过从 var.endpoints
导出数据结构并最终将其传递给 jsonencode
.
有配置文件(json)
#configuration.json
{
"lambda1": {
"name": "my-name",
"path": "my-path",
"method": "GET"
},
"lambda2": {
"name": "my-name2",
"path": "my-path2",
"method": "GET"
},
}
和以下地形
locals {
conf = jsondecode(file("${path.module}/configuration.json"))
name="name"
}
data "aws_caller_identity" "current" {}
resource "aws_lambda_permission" "apigw_lambda" {
for_each = local.conf
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = each.value.name
principal = "apigateway.amazonaws.com"
# More: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html
source_arn = "arn:aws:execute-api:${var.region}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.api.id}/*/${aws_api_gateway_method.methods[each.key].http_method}${aws_api_gateway_resource.worker-path[each.key].path}"
}
resource "aws_api_gateway_rest_api" "api" {
name = local.name
description = "an endpoints...."
endpoint_configuration {
types = ["REGIONAL"]
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_resource" "country-endpoint" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = local.country-code # https.exmaple.com/stage/uk
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_resource" "worker-path" {
for_each = local.conf
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_resource.country-endpoint.id
path_part = each.value.path # https.exmaple.com/stage/uk/path_from_json
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_method" "methods" {
for_each = local.conf
http_method = each.value.method
resource_id = aws_api_gateway_resource.worker-path[each.key].id
rest_api_id = aws_api_gateway_rest_api.api.id
authorization = "NONE"
}
resource "aws_api_gateway_integration" "lambda-api-integration-get-config" {
for_each = local.conf
# The ID of the REST API and the endpoint at which to integrate a Lambda function
resource_id = aws_api_gateway_resource.worker-path[each.key].id
rest_api_id = aws_api_gateway_rest_api.api.id
# The HTTP method to integrate with the Lambda function
http_method = aws_api_gateway_method.methods[each.key].http_method
# AWS is used for Lambda proxy integration when you want to use a Velocity template
type = "AWS_PROXY"
# The URI at which the API is invoked
uri = data.terraform_remote_state.workers.outputs.lambda_invoke[each.key]
integration_http_method = "POST"
}
我想创建一个 terraform (v0.12+) 模块输出一个 AWS API 网关与 Lambda 集成。我不太明白如何(或者是否可能)遍历映射列表以动态输出资源。
用户应该能够像这样实例化模块:
module "api_gateway" {
source = "./apig"
endpoints = [
{
path = "example1"
method = "GET"
lambda = "some.lambda.reference"
},
{
path = "example1"
method = "POST"
lambda = "some.lambda.reference"
},
{
path = "example2"
method = "GET"
lambda = "another.lambda.reference"
}
]
}
从endpoints
界面,我要输出三个资源:
- 一个
aws_api_gateway_resource
其中path_part = endpoint[i].path
- 一个
aws_api_gateway_method
其中http_method = endpoint[i].method
- 一个
aws_api_gateway_integration
引用了endpoint[i].lambda
,等等
Terraform 的 for_each
属性 似乎不够强大,无法处理这个问题。我知道 Terraform 还支持 for
循环和 for / in
循环,但我没能找到任何使用此类表达式进行资源声明的示例。
让我们从写出 endpoints
变量的声明开始,因为其余的答案取决于它是如何定义的:
variable "endpoints" {
type = set(object({
path = string
method = string
lambda = string
})
}
上面说的是endpoints
是一组对象,意思是物品的先后顺序并不重要。顺序无关紧要,因为无论如何我们都会在 API 中为每个对象创建单独的对象。
下一步是弄清楚如何从给定的数据结构转移到一个映射结构,其中每个键都是唯一的,每个元素都映射到您要生成的资源的一个实例。为此,我们必须定义我们想要的映射,我认为这里是:
- 每个不同的
path
一个aws_api_gateway_resource
。 - 每个不同的
path
和method
对一个aws_api_gateway_method
。 - 每个不同的
path
和method
对一个aws_api_gateway_integration
。 - 每个不同的
path
/method
/status_code
三元组一个aws_api_gateway_integration_response
。 - 每个不同的
path
/method
/status_code
三元组一个aws_api_gateway_method_response
。
所以看来我们在这里需要三个集合:第一个是所有路径的集合,第二个是从 path
+method
对到描述该方法的对象的映射,第三个是我们要建模的端点和状态代码的每个组合。
locals {
response_codes = toset({
status_code = 200
response_templates = {} # TODO: Fill this in
response_models = {} # TODO: Fill this in
response_parameters = {} # TODO: Fill this in
})
# endpoints is a set of all of the distinct paths in var.endpoints
endpoints = toset(var.endpoints.*.path)
# methods is a map from method+path identifier strings to endpoint definitions
methods = {
for e in var.endpoints : "${e.method} ${e.path}" => e
}
# responses is a map from method+path+status_code identifier strings
# to endpoint definitions
responses = {
for pair in setproduct(var.endpoints, local.response_codes) :
"${pair[0].method} ${pair[0].path} ${pair[1].status_code}" => {
method = pair[0].method
path = pair[0].path
method_key = "${pair[0].method} ${pair[0].path}" # key for local.methods
status_code = pair[1].status_code
response_templates = pair[1].response_templates
response_models = pair[1].response_models
response_parameters = pair[1].response_parameters
}
}
}
定义了这两个派生集合后,我们现在可以写出资源配置:
resource "aws_api_gateway_rest_api" "example" {
name = "example"
}
resource "aws_api_gateway_resource" "example" {
for_each = local.endpoints
rest_api_id = aws_api_gateway_rest_api.example.id
parent_id = aws_api_gateway_rest_api.example.root_resource_id
path_part = each.value
}
resource "aws_api_gateway_method" "example" {
for_each = local.methods
rest_api_id = aws_api_gateway_resource.example[each.value.path].rest_api_id
resource_id = aws_api_gateway_resource.example[each.value.path].resource_id
http_method = each.value.method
}
resource "aws_api_gateway_integration" "example" {
for_each = local.methods
rest_api_id = aws_api_gateway_method.example[each.key].rest_api_id
resource_id = aws_api_gateway_method.example[each.key].resource_id
http_method = aws_api_gateway_method.example[each.key].http_method
type = "AWS_PROXY"
integration_http_method = "POST"
uri = each.value.lambda
}
resource "aws_api_gateway_integration_response" "example" {
for_each = var.responses
rest_api_id = aws_api_gateway_integration.example[each.value.method_key].rest_api_id
resource_id = aws_api_gateway_integration.example[each.value.method_key].resource_id
http_method = each.value.method
status_code = each.value.status_code
response_parameters = each.value.response_parameters
response_templates = each.value.response_templates
# NOTE: There are some other arguments for
# aws_api_gateway_integration_response that I've left out
# here. If you need them you'll need to adjust the above
# local value expressions to include them too.
}
resource "aws_api_gateway_response" "example" {
for_each = var.responses
rest_api_id = aws_api_gateway_integration_response.example[each.key].rest_api_id
resource_id = aws_api_gateway_integration_response.example[each.key].resource_id
http_method = each.value.method
status_code = each.value.status_code
response_models = each.value.response_models
}
您可能还需要 aws_api_gateway_deployment
。为此,重要的是要确保它依赖于 all 我们在上面定义的 API 网关资源,以便 Terraform 将等到 API 完全配置在尝试部署之前:
resource "aws_api_gateway_deployment" "example" {
rest_api_id = aws_api_gateway_rest_api.example.id
# (whatever other settings are appropriate)
depends_on = [
aws_api_gateway_resource.example,
aws_api_gateway_method.example,
aws_api_gateway_integration.example,
aws_api_gateway_integration_response.example,
aws_api_gateway_method_response.example,
]
}
output "execution_arn" {
value = aws_api_gateway_rest_api.example.execution_arn
# Execution can't happen until the gateway is deployed, so
# this extra hint will ensure that the aws_lambda_permission
# granting access to this API will be created only once
# the API is fully deployed.
depends_on = [
aws_api_gateway_deployment.example,
]
}
API 撇开网关细节不谈,这种情况的一般程序是:
- 定义您的输入。
- 弄清楚如何从您的输入获取每个资源所需的每个实例一个元素的集合。
- 写
local
表达式来描述从输入到重复集合的投影。 - 编写
resource
块,其中for_each
引用适当的本地值作为其重复值。
for
expressions, along with the flatten
and setproduct
函数是我们从结构中投影数据的主要工具,调用者可以方便地将输入变量提供给 for_each
表达式所需的结构。
API Gateway 具有特别复杂的数据模型,因此在 Terraform 语言中表达它的所有可能性可能需要比其他服务可能需要更多的投影和其他转换。因为 OpenAPI 已经定义了一种灵活的声明性语言来定义 REST APIs 并且 API Gateway 已经原生支持它,所以让您的 endpoints
变量采用一个标准的 OpenAPI 定义并将其直接传递给 API 网关,从而获得 OpenAPI 模式格式的所有表现力,而无需自己在 Terraform 中实现所有细节:
variable "endpoints" {
# arbitrary OpenAPI schema object to be validated by API Gateway
type = any
}
resource "aws_api_gateway_rest_api" "example" {
name = "example"
body = jsonencode(var.endpoints)
}
即使您仍然希望 endpoints
变量成为更高级别的模型,您也可以考虑使用 Terraform 语言构造 一个 OpenAPI 模式通过从 var.endpoints
导出数据结构并最终将其传递给 jsonencode
.
有配置文件(json)
#configuration.json
{
"lambda1": {
"name": "my-name",
"path": "my-path",
"method": "GET"
},
"lambda2": {
"name": "my-name2",
"path": "my-path2",
"method": "GET"
},
}
和以下地形
locals {
conf = jsondecode(file("${path.module}/configuration.json"))
name="name"
}
data "aws_caller_identity" "current" {}
resource "aws_lambda_permission" "apigw_lambda" {
for_each = local.conf
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = each.value.name
principal = "apigateway.amazonaws.com"
# More: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html
source_arn = "arn:aws:execute-api:${var.region}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.api.id}/*/${aws_api_gateway_method.methods[each.key].http_method}${aws_api_gateway_resource.worker-path[each.key].path}"
}
resource "aws_api_gateway_rest_api" "api" {
name = local.name
description = "an endpoints...."
endpoint_configuration {
types = ["REGIONAL"]
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_resource" "country-endpoint" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = local.country-code # https.exmaple.com/stage/uk
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_resource" "worker-path" {
for_each = local.conf
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_resource.country-endpoint.id
path_part = each.value.path # https.exmaple.com/stage/uk/path_from_json
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_method" "methods" {
for_each = local.conf
http_method = each.value.method
resource_id = aws_api_gateway_resource.worker-path[each.key].id
rest_api_id = aws_api_gateway_rest_api.api.id
authorization = "NONE"
}
resource "aws_api_gateway_integration" "lambda-api-integration-get-config" {
for_each = local.conf
# The ID of the REST API and the endpoint at which to integrate a Lambda function
resource_id = aws_api_gateway_resource.worker-path[each.key].id
rest_api_id = aws_api_gateway_rest_api.api.id
# The HTTP method to integrate with the Lambda function
http_method = aws_api_gateway_method.methods[each.key].http_method
# AWS is used for Lambda proxy integration when you want to use a Velocity template
type = "AWS_PROXY"
# The URI at which the API is invoked
uri = data.terraform_remote_state.workers.outputs.lambda_invoke[each.key]
integration_http_method = "POST"
}