Плотное кодирование и объяснители кода

Моя роль в компании-работодателе часто описывается как «дым коромыслом». То есть, проект, в котором они участвуют, или, что чаще, являются субподрядчиками, испытывает трудности, и конечный заказчик просит оказать ему дальнейшую помощь, чтобы все пошло по тому пути, на который он изначально рассчитывал. Как правило, меня впервые привлекают к таким проектам — это «всплеск» поддержки автоматизации.

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

Одним из моих первых принципов при создании новой функциональности является попытка сделать это таким образом, чтобы ее можно было легко деактивировать или отменить. Эта проектная группа, как и другие, которым я помогал, использует Terraform, но не особенно модульным или функционально изолированным способом. Все развертывания состоят из файлов main.tf, vars.tf и outputs.tf и случайных файлов «шаблонов» (обычно это простые документы HERE с простыми действиями по подстановке переменных). Хотя они (к счастью) используют некоторые провайдеры данных, они не очень дисциплинированы в том, где они их внедряют. Они встраивают их в один или оба файла main.tf и vars.tf данного сервиса. Я? Мне обычно нравится, когда все мои поставщики данных находятся в файлах типа data.tf, так как это способствует согласованности и сохраняет тип содержимого в различных отдельных файлах «чистым» с точки зрения предлагаемой функциональности.

Аналогично, если я использую шаблонизированный контент, я предпочитаю поставлять его в виде файлов, которые достаточно экстернализируют контент, чтобы на нем могли работать соответствующие линтеры. Такой вид экстернализации не только позволяет легче линковать такие файлы, но и, поскольку она устраняет эффекты инкапсуляции, облегчает отладку или расширение экстернализированного содержимого.

В одном из недавних проектов мне было поручено помочь заказчику автоматизировать развертывание конечных точек VPC в его учетных записях AWS. Заказчик пытался в максимально возможной степени предотвратить выход трафика проекта за пределы VPC.

Когда я приступил к задаче кодирования, заказчик не мог сказать мне, какие именно службы он хотел или нуждался в таком включении. Зная, что каждая такая конечная точка обслуживания связана с периодическими расходами, и не желая, чтобы они случайно сорвали банк, я решил написать код таким образом, чтобы без участия оператора развернуть все конечные точки обслуживания AWS в их VPC, но в то же время позволить им легко все отменить, когда придут первые (шокирующие) счета.

Код, который я предоставил, работал хорошо. Однако, несмотря на то, что команда, работающая в компании, была знакома с фреймворком, мой код вызвал у них некоторое недоумение. Они попросили меня проанализировать код для них. Зная историю проекта — как с точки зрения недостатка документации, так и с точки зрения текучести кадров — я решил написать пояснительный документ. Ниже приводится это объяснение.

Во-первых, я предоставил свое содержимое в виде четырех дополнительных файлов вместо того, чтобы внедрить свой код в существующий набор файлов main.tf, vars.tf и outputs.tf. Это позволило им полностью отключить функциональность, просто уничтожив файлы, которые я доставил, вместо того, чтобы проводить файловую хирургию в их обычном наборе файлов. Поскольку мой клиент работает в нескольких разделах AWS, это облегчает работу с различиями в API разделов и позволяет откатить изменения, если API раздела развертывания старше API раздела разработки. Набор файлов, который я предоставил, состоял из файлов endpoints_main.tf, endpoints_data.tf, endpoints_vars.tf и endpoints_services.tpl.hcl. Соответственно, эти файлы содержат: основную функциональность; определения поставщиков данных; определение переменных, используемых в файлах «main» и «data»; и HCL-форматированный шаблон-файл конечных точек по умолчанию.

Самый основной/простой в объяснении файл — это файл шаблона конечных точек по умолчанию, endpoints_services.tpl.hcl. Файл состоит из map-объектов, заключенных в большую структуру списка. Объекты map-объектов состоят из пар атрибутов name и type. Значения имен были получены путем выполнения:

aws ec2 describe-vpc-endpoint-services 
  --query 'ServiceDetails[].{Name:ServiceName,Type:ServiceType[].ServiceType}' | 
sed -e '/[$/{N;s/[n *"/"/;}' -e '/^[][]*]$/d' | 
tr '[:upper:]' '[:lower:]'
Войти в полноэкранный режим Выйти из полноэкранного режима

А затем измените буквальные имена регионов на строку «${endpoint-region}». Это изменение позволяет функции templatefile() в Terraform подставлять нужное значение при чтении файла шаблона, что делает автоматизацию переносимой как для регионов, так и для разделов. Содержимое файла-шаблона также инкапсулируется функцией jsondecode() Terraform. Эта инкапсуляция необходима для того, чтобы функция templatefile() могла правильно считать файл (чтобы произошла замена переменных).

Поскольку я хотел, чтобы использование этого шаблон-файла было поведением fallback (по умолчанию), мне нужно было объявить его использование как fallback. Это было сделано в секции locals {} файла endpoint_data.tf:

locals {
  vpc_endpoint_services = length(var.vpc_endpoint_services) == 0 ? jsondecode(
    templatefile(
      "./endpoint_services.tpl.hcl",
      {
        endpoint_region = var.region
      }
    )
  ) 
}  
Войти в полноэкранный режим Выход из полноэкранного режима

В приведенном выше примере мы используем троичную оценку для установки значения локально скопированной переменной vpc_endpoint_services. Если значение глобально скопированной переменной vpc_endpoint_services равно «0», то используется файл шаблона, в противном случае используется содержимое глобально скопированной переменной vpc_endpoint_services. Использование файла-шаблона осуществляется с помощью функции templatefile() для чтения файла и замены всех вхождений «${endpoint-region}» в файле значением глобально скопированной переменной «region».

Примечание: Окружающая функция jsondecode() используется для преобразования файла-потока из формата, заданного ранее с помощью функции jsonencode() в начале файла. Мне не нравится, когда приходится прибегать к такого рода хитростям, но без них функция templatefile() выдавала бы ошибку при попытке заполнить переменную vpc_endpoint_services. Если у кого-то из читателей есть идея, как достичь желаемой функциональности менее сложным способом, пожалуйста, прокомментируйте.

Больше всего объяснений потребовала логика в разделе:

data "aws_vpc_endpoint_service" "this" {
  for_each = {
    for service in local.vpc_endpoint_services :
      "${service.name}:${service.type}" => service
  }

  service_name = length(
    regexall(
      var.region,
      each.value.name
    )
  ) == 1 ? each.value.name : "com.amazonaws.${var.region}.${each.value.name}"

  service_type = title(each.value.type)
}
Войти в полноэкранный режим Выход из полноэкранного режима

Этот раздел использует источник данных aws_vpc_endpoint_service из Terraform. Мой код присваивает ему идентификатор ссылки «this». Не слишком оригинальная или заслуживающая внимания метка, но, если нет необходимости в нескольких таких ссылках, сойдет.

Функция for_each перебирает значения, хранящиеся в локально скопированной объектной переменной vpc_endpoint_services. По мере выполнения цикла каждый словарь-объект — пары атрибутов имени и типа — присваивается переменной цикла service. В свою очередь, цикл итеративно экспортирует переменную each.value.name и each.value.type.

Я мог бы установить переменную service_name равной значению переменной each.value.name, однако я хотел сделать жизнь пользователя автоматизации менее обременительной. Вместо того, чтобы указывать полный путь к имени сервиса, можно указать короткое имя. Использование функции regexall() для проверки наличия значения глобально скопированной переменной region в значении переменной each.value.name позволяет использовать функцию length() как часть троичного определения для переменной service_name. Если возвращаемая длина равна «0», то к передаваемому оператором имени сервиса добавляется полностью квалифицированный путь сервиса, обычно действительный для региона раздела; если возвращаемая длина равна «1», то используется значение, уже хранящееся в переменной each.value.name.

Аналогично, я не хотел, чтобы оператор заботился о том, в каком случае он указывает тип сервиса. Поэтому я позволил функции title(function) Terraform позаботиться об установке нужного регистра для значения переменной each.value.type.

Затем значения service_type и service_name возвращаются при вызове data-provider из блока locals {} файла endpoint_main.tf:

locals {
  […elided…]
  # Split Endpoints by their type
  gateway_endpoints = toset(
    [
      for e in data.aws_vpc_endpoint_service.this :
        e.service_name if e.service_type == "Gateway"
    ]
  )

  interface_endpoints = toset(
    [
      for e in data.aws_vpc_endpoint_service.this :
        e.service_name if e.service_type == "Interface"
    ]
  )
  […elided…]
}
Вход в полноэкранный режим Выход из полноэкранного режима

Локально скопированные переменные gateway_endpoints и interface_endpoints представляют собой списки-переменные. Каждая из них заполняется путем взятия имени сервиса, возвращаемого из data.aws_vpc_endpoint_service.this data-provider, если значение selected-for service_type совпадает. Этот список-переменных затем итеративно обрабатывается в соответствующих строфах ресурса «aws_vpc_endpoint» «interface_services» и ресурса «aws_vpc_endpoint» «gateway_services».

Оцените статью
devanswers.ru
Добавить комментарий