Terraform:如何从对象列表创建 API 网关端点和方法?

Kwh*_*ejr 2 terraform aws-api-gateway terraform-provider-aws

我想创建一个 terraform (v0.12+) 模块,该模块输出一个带有 Lambda 集成的 AWS API 网关。我不太明白如何(或者甚至可能)遍历映射列表以动态输出资源。

用户应该能够像这样实例化模块:

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"
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

endpoints界面上,我想输出三个资源:

  1. aws_api_gateway_resource哪里path_part = endpoint[i].path
  2. aws_api_gateway_method哪里http_method = endpoint[i].method
  3. aws_api_gateway_integration,需要一个参考 endpoint[i].lambda,等

Terraform 的for_each属性似乎不足以处理这个问题。我知道 Terraform 也支持for循环和for / in循环,但我找不到任何使用此类表达式进行资源声明的示例。

Mar*_*ins 13

让我们首先写出该endpoints变量的声明,因为答案的其余部分取决于它的定义方式:

variable "endpoints" {
  type = set(object({
    path   = string
    method = string
    lambda = string
  })
}
Run Code Online (Sandbox Code Playgroud)

上面说的endpoints是一组对象,这意味着项目的顺序并不重要。排序无关紧要,因为无论如何我们都将在 API 中为每个对象创建单独的对象。

下一步是弄清楚如何从给定的数据结构移动到一个映射结构,其中每个键都是唯一的,并且每个元素映射到要生成的资源的一个实例。为此,我们必须定义我们想要的映射,我认为这里是:

  • 一个aws_api_gateway_resource为每个不同的path
  • 一个aws_api_gateway_method用于每个不同的pathmethod对。
  • 一个aws_api_gateway_integration用于每个不同的pathmethod对。
  • 一个aws_api_gateway_integration_response为每个不同的path/ method/status_code三倍。
  • 一个aws_api_gateway_method_response为每个不同的path/ method/status_code三倍。

所以看起来我们在这里需要三个集合:第一个是所有路径的集合,第二个是从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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

定义了这两个派生集合后,我们现在可以写出资源配置:

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
}
Run Code Online (Sandbox Code Playgroud)

您可能还需要一个aws_api_gateway_deployment. 为此,重要的是要确保它依赖于我们上面定义的所有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,
  ]
}
Run Code Online (Sandbox Code Playgroud)

撇开 API 网关的细节不谈,这种情况的一般程序是:

  • 定义您的输入。
  • 弄清楚如何从您的输入获取每个资源所需的每个实例具有一个元素的集合。
  • 编写local表达式来描述从输入到重复集合的投影。
  • Write resourceblocks wherefor_each引用适当的本地值作为其重复值。

for表达式以及flattensetproduct函数是我们从结构中投影数据的主要工具,该结构便于调用者在输入变量中提供for_each表达式所需的结构。

API Gateway 有一个特别复杂的数据模型,因此在 Terraform 语言中表达其所有可能性可能需要比其他服务更多的投影和其他转换。因为OpenAPI已经定义了一种灵活的声明式语言来定义 REST API 并且 API Gateway 已经在本地支持它,所以让您的endpoints变量采用标准的 OpenAPI 定义并将其直接传递给 API Gateway可能会更加直接和灵活,从而获得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)
}
Run Code Online (Sandbox Code Playgroud)

即使你仍想你的endpoints变量是一个更高层次的模型,你也可以考虑使用Terraform语言来构建从获得的数据结构提供的OpenAPI架构var.endpoints并最终将它传递给jsonencode

  • 我只能给你一票,这是不公平的。 (3认同)
  • 据我所知,API Gateway 需要这些响应对象才能知道如何将 Lambda 函数结果转换为 HTTP 响应,从而允许您设置 HTTP 正文和标头。然而,我的 API 网关经验来自于他们引入“代理”集成的想法之前,这可能避免了通过仅提供默认响应转换来创建这些响应对象的需要。如果这是真的,那么您也许可以从本示例中省略这些资源。 (2认同)