пятница, 23 августа 2019 г.

Terraform. Creating dynamic VPC

Задача - создавать выделенные окружения ECS для каждого приложения. Выделенные имеется ввиду что каждое приложение это полностью свою структура на базе выделенной VPC и иже с ней. И что самое забавное, адреса VPC должны быть уникальны, то есть не допускается перекрытие VPC CIDR.

Не сложно, казалось бы сделать первую часть, просто используя terraform workspace. А вот со второй задачей было интересно.

Алгоритм я кодил такой - делаем запрос в регион по тэгам на наличие каких-то уже живых VPC и если они есть - получаю их cidr_block. Рядом делаю массив с помощью утилитки prips, который содержит все возможные VPC CIDR для моих приложений. далее удаляю из этого массива со всеми возможными CIDR те, что уже заняты (созданные ранее, живые) и первый минимальный CIDR - мой кандидат. Чуть более подробнее:
у меня выбрался один большой блок для DEV-env = 10.69.64.0/19
для экономии адресов было решено, что достаточно будет иметь для каждого приложения VPC CIDR класса /26, что дает возможность (AWS тут ограничивает минимальным /28) создать до 4-х подсетей в такой vpc класса /28), а это либо пара пар private-public, либо до 4-х private или public subnets. так же этот алгоритм позволяет переиспользовать освободившиеся VPC CIDR а не расти вверх до упора.

Идеи под реализации были такие:
1. использовать null_resource + local_file. То есть вызывается null_resource, который запускает шел-код, которому передаются переменные окружения из текущего состояния Terraform (CIDR, WORKSPACE и так далее). далее на выходе этого скрипта - первый свободный CIDR из нашего большого 10.69.64.0/19. И все было бы хорошо, но я не смог переубедить терраформ не убивать каждый раз то что он создал. Это потому что null_resource + local_file - это динамические объекты и они каждый раз при новом запуске не известны. Ни какие триггеры не помогли мне в этом.

вариант 2 оказался рабочий и даже мне понравился больше
2. Я нашел, что в 12-м терраформе есть data-source тип external (https://www.terraform.io/docs/providers/external/data_source.html). Вот на его базе это и получилось реализовать так как мне нужно было для решения задачи. Я покажу ключевые элементы что бы было понятно:
variable.tf:
-------------------------------------
data "external" "app-cidr" {
  program = ["${path.module}/getFirstFreeVpcCidr.sh"]
  query = {
    PRIPS         = "${path.module}/prips"
    VPC_CIDR_FULL = "${lookup(var.vpc-cidr, var.env)}"
    APP_CIDR_MASK = "${lookup(var.app-vpc-mask, var.env)}"
    REGION        = "${var.region}"
    STAGE_ENV     = "${var.env}"
    WORKSPACE_NAME = terraform.workspace
  }
}
--------------------------------------
здесь я указываю где лежит скрипт, который реализует логику. Тут особенность этого data-source в том, что ему на вход нужен JSON и выдавать он должен JSON. Переменные окружения передаем в блоке query {} а в скрипте эти переменные придется преобразовать
Вот весь скрипт:
-------------------------
#!/bin/bash
set -e
eval "$(jq -r '@sh "PRIPS=\(.PRIPS) VPC_CIDR_FULL=\(.VPC_CIDR_FULL) APP_CIDR_MASK=\(.APP_CIDR_MASK) REGION=\(.REGION) STAGE_ENV=\(.STAGE_ENV) WORKSPACE_NAME=\(.WORKSPACE_NAME)"')"

[[ -z "${PRIPS}" ]] && exit 1
[[ -z "${VPC_CIDR_FULL}" ]] && exit 1
[[ -z "${APP_CIDR_MASK}" ]] && exit 1
[[ -z "${REGION}" ]] && exit 1
[[ -z "${STAGE_ENV}" ]] && exit 1

declare -a VPC_CIDR_CURR
## check for exists vpc. If exists return current CIDR-block
VPC_CIDR_CURR=($(aws ec2 describe-vpcs --filters "Name=tag-key,Values=Environment" "Name=tag-value,Values=${WORKSPACE_NAME}-vpc" --region=${REGION} --query "Vpcs[*].CidrBlock" --output text))

if [ -z ${VPC_CIDR_CURR} ]; then

  VPC_CIDR_CURR=($(aws ec2 describe-vpcs --filters "Name=tag-key,Values=Environment" "Name=tag-value,Values=*-${STAGE_ENV}-vpc" --region=${REGION} --query "Vpcs[*].CidrBlock" --output text))

  #for test purpose: add element to array
  #VPC_CIDR_CURR=("${VPC_CIDR_CURR[@]}" "10.69.64.0/27")

  declare -a ALL_AVAIL_SUBNETS
  ### prips -i
  ### The offset is a value of adresses into app-vpc
  APP_CIDR="$( echo ${VPC_CIDR_FULL}|cut -d/ -f-1 )${APP_CIDR_MASK}"
  COUNT_IPS_INTO_APP_CIDR=$( ${PRIPS} ${APP_CIDR}|wc -l )
  #echo "Count - $COUNT_IPS_INTO_APP_CIDR"
  ALL_AVAIL_SUBNETS=($( ${PRIPS} -i ${COUNT_IPS_INTO_APP_CIDR} ${VPC_CIDR_FULL} ))

  for i in ${VPC_CIDR_CURR[@]}
  do
    l=$(echo $i|cut -d/ -f-1)
    ALL_AVAIL_SUBNETS=( ${ALL_AVAIL_SUBNETS[@]/$l/} )
  done

  FIRST_FREE_CIDR=${ALL_AVAIL_SUBNETS[@]:0:1}
  APP_CIDR="${FIRST_FREE_CIDR}${APP_CIDR_MASK}"
else
  APP_CIDR=${VPC_CIDR_CURR}
fi

echo "${APP_CIDR}" >> $LOG
jq -n --arg APP_CIDR "${APP_CIDR}" '{"app-cidr":$APP_CIDR}'
---------------------------------

Здесь я получаю переменные окружения из query{} блока. Преобразую их в башевские для работы. далее проверяю, есть ли уже vpc с таким именем как текущий workspace. Если есть, то выдаю его CIDR. Именно это не дает терраформу убить то что было создано уже.
Если такой vpc еще нет, то вычисляем первый свободный блок и отдаем его JSON-м.

теперь терраформ знает нужный ему CIDR. У меня модульный сценарий и есть другие, заранее оговоренные CIDR для предопределенных окружений (dev/stage/prod). Что бы оставить их рабочими я добавляю наш только что вычисленный CIDR в переменную типа MAP:

variable "vpc-cidr" {
  type    = map
  default = {
    dev              = "10.69.0.0/19"
    staging          = "10.69.64.0/19"
    prod             = "10.69.128.0/19"
 }
}


locals {
  vpc-cidr = merge(var.vpc-cidr,{"${terraform.workspace}" = "${data.external.app-cidr.result.app-cidr}"})
  vpcCidr = lookup(local.vpc-cidr, terraform.workspace)
}

## UPDATE after release 0.12.7 (fixed lookup error)
locals {
  vpcCidr = lookup(var.vpc-cidr, terraform.workspace, data.external.app-cidr.result.app-cidr)
}

и далее в модуле уже применяю как обычно:
resource "aws_vpc" "vpc" {
  cidr_block           = local.vpcCidr
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name        = "${terraform.workspace}-vpc"
    Environment = terraform.workspace
  }
}

Комментариев нет: