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

Posted

技术标签:

【中文标题】Terraform:如何从对象列表创建 API 网关端点和方法?【英文标题】:Terraform: How to create API Gateway endpoints and methods from a list of objects? 【发布时间】:2020-02-12 07:34:37 【问题描述】:

我想创建一个 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"
    
  ]

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 循环,但我找不到任何使用此类表达式进行资源声明的示例。

【问题讨论】:

这通常是三个模块声明,而不是一个模块内的三个迭代。 【参考方案1】:

让我们从写出 endpoints 变量的声明开始,因为其余的答案取决于它的定义方式:

variable "endpoints" 
  type = set(object(
    path   = string
    method = string
    lambda = string
  )

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

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

每个不同的path 对应一个aws_api_gateway_resourceaws_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
    
  

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

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。为此,确保它依赖于所有我们在上面定义的 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,连同flattensetproduct 函数,是我们从结构投影数据的主要工具,方便调用者在输入变量中提供我们需要的结构for_each 表达式。

API Gateway 有一个特别复杂的数据模型,因此在 Terraform 语言中表达它的所有可能性可能需要比其他服务更多的投影和其他转换。因为 OpenAPI 已经定义了一种灵活的声明性语言来定义 REST API 并且 API Gateway 已经原生支持它,所以让您的 endpoints 变量采用标准 OpenAPI 定义并将其直接传递给 API Gateway 会更加直接和灵活,因此无需自己在 Terraform 中实现所有细节即可获得 OpenAPI 模式格式的所有表现力:

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 语言通过从 @987654360 派生数据结构来构造 OpenAPI 架构@ 并最终将其传递给jsonencode

【讨论】:

这是一个很棒且内容丰富的回复。我看到您还包括了我不熟悉的aws_api_gateway_integration_responseaws_api_gateway_method_response 资源。在您的 Terraform 体验中,这些增加了什么?当我使用 Terraform 制作 POC APIG 时,没有这些,lambda 集成工作得很好。 据我所知,API Gateway 需要这些响应对象才能知道如何将 Lambda 函数结果转换为 HTTP 响应,从而允许您设置 HTTP 正文和标头。但是,我的 API Gateway 经验来自于他们引入“代理”集成的概念之前,这可能通过提供默认响应转换来避免创建这些响应对象的需要。如果这是真的,那么您可以从这个示例中省略这些资源。 我只能给你一票是不公平的。 谢谢你!由于 Api 网关数据模型的复杂性太久,我一直在碰壁。这个答案不仅值得更多的支持,还值得一整篇由 hashcorp 推荐的文章。你是一个了不起的人,你创造了如此完整和慷慨的答案。【参考方案2】:

有一个配置文件(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:如何从对象列表创建 API 网关端点和方法?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Terraform 在 API 网关上启用 HEAD 方法

Terraform : for_each 一个一个

如何从 terraform 中的 EC2 实例列表中提取 ID 以在 ALB 中使用?

Terraform:为调用 Lambda 的 AWS API Gateway 创建 url 路径参数?

Terraform:遍历复杂对象列表

Terraform - 如何将列表转换为地图(如何使用 terraform 获取 AMI 标签)