Terraform: Как создать конечные точки и методы API Gateway из списка объектов? - PullRequest
0 голосов
/ 15 октября 2019

Я хочу создать модуль terraform (v0.12 +), который выводит шлюз API AWS с лямбда-интеграцией. Я не совсем понимаю, как (или если это вообще возможно) перебирать список карт для динамического вывода ресурсов.

Пользователь должен иметь возможность создавать экземпляр модуля следующим образом:

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 я хочу вывести три ресурса:

  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, но я не смог найти никаких примеров использования таких выражений для объявления ресурсов.

1 Ответ

1 голос
/ 16 октября 2019

Давайте начнем с написания объявления этой переменной endpoints, поскольку остальная часть ответа зависит от того, как он определен следующим образом:

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

Выше сказано, что endpoints являетсянабор предметов, что означает, что порядок предметов не имеет значения. Порядок не имеет значения, потому что мы все равно будем создавать отдельные объекты в API для каждого из них.

Следующий шаг - выяснить, как перейти от данной структуры данных к структуре, представляющей собой карту, в которой каждыйключ уникален и где каждый элемент сопоставляется с одним экземпляром ресурсов, которые вы хотите создать. Чтобы сделать это, мы должны определить, какое отображение мы намереваемся, и я думаю, что здесь будет:

  • Один aws_api_gateway_resource для каждого отдельного path.
  • Один aws_api_gateway_methodдля каждой отдельной пары path и method.
  • Один aws_api_gateway_integration для каждой отдельной пары path и method.
  • Один aws_api_gateway_integration_response для каждого отдельного path/method / status_code` triple.
  • Один aws_api_gateway_method_response для каждого отдельного path/ method / status_code` triple.

Так что, похоже, нам здесь нужно три коллекции: first - это набор всех путей, second - карта от пары 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 выражений вместе с flatten и setproduct функции, являются нашим основным инструментом для проецирования данных из структуры, которая удобна для вызывающей стороны для предоставления во входной переменной структуры (структур), которые нам нужны для for_each выражений.

AОднако PI Gateway имеет особенно сложную модель данных, поэтому для выражения всех его возможностей на языке Terraform может потребоваться гораздо больше проекций и других преобразований, чем для других служб. Поскольку OpenAPI уже определяет гибкий декларативный язык для определения API-интерфейсов REST, а API-шлюз уже изначально поддерживает его, может быть более простым и гибким сделать, чтобы ваша переменная 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)
}

Даже если вы все еще хотите, чтобы ваша переменная endpoints была более высокого уровнямодели, вы также можете рассмотреть возможность использования языка Terraform для конструирования схемы OpenAPI путем получения структуры данных из var.endpoints и, наконец, передачи ее в jsonencode.

...