image
У нас было 4 Amazon-аккаунта, 9 VPC и 30 мощнейших девелоперских окружений, стейджей, регрессий — всего более 1000 EC2 instance всех цветов и оттенков. Раз уж начал коллекционировать облачные решения для бизнеса, то надо идти в своем увлечении до конца и продумать, как все это автоматизировать.

Привет! Меня зовут Кирилл Казарин, я работаю инженером в компании DINS. Мы занимаемся разработкой облачных коммуникационных решений для бизнеса. В своей работе мы активно используем Terraform, с помощью которого мы гибко управляем нашей инфраструктурой. Поделюсь опытом работы с этим решением.

Статья длинная, поэтому запаситесь попкорном чаем и вперед!

И еще один нюанс — статья писалась на основе версии 0.11, в свежей 0.12 многое изменилось но основные практики и советы по прежнему актуальны. Вопрос миграции с 0.11 на 0.12 заслуживает отдельной статьи!

Что такое Terraform


Terraform — это популярный инструмент компании Hashicorp, который появился в 2014 году.

Эта утилита позволяет управлять вашей облачной инфраструктурой в парадигме Infrastructure as a Code на очень дружественном, легко читаемом декларативном языке. Его применение обеспечивает вам единый вид ресурсов и применение практик работы с кодом для управления инфраструктурой, которые за долгое время уже выработаны сообществом разработчиков. Terraform поддерживает все современные облачные платформы, позволяет безопасно и предсказуемо изменять инфраструктуру.

При запуске Terraform читает код и, используя представленные провайдерами облачного сервиса плагины, приводит вашу инфраструктуру к описанному состоянию, совершая необходимые вызовы к API.

Наш проект полностью находится в Amazon, развернут на базе AWS-сервисов, и поэтому я пишу о применении Terraformа именно в этом ключе. Отдельно замечу, что он может применяться не только для Amazon. Он позволяет управлять всем, у чего есть API.

Помимо этого мы управляем настройками VPC, IAM-политиками и ролями. Мы управляем таблицами маршрутизации, сертификатами, сетевыми ACL. Мы управляем настройками нашего web application firewall, S3-бакетами, SQS-очередями – всем, что может использовать наш сервис в Amazon. Я пока не встречал фичи у Amazon, которую нельзя было бы Terraform-ом описать с точки зрения инфраструктуры.

Получается немаленькая инфраструктура, руками это просто убьешься поддерживать. Но с Terraform это оказывается удобно и просто.

Из чего состоит Terraform


Провайдеры — это плагины для работы с API того или иного сервиса. Я насчитал их более 100. В их числе провайдеры для Amazon, Google, DigitalOcean, VMware Vsphere, Docker. Я даже нашел у них в этом официальном списке провайдера, который позволяет вам управлять правилами для Cisco ASA!

Помимо прочего вы можете управлять:

  • Дашбордами, датасорсами и алертами в Grafana.
  • Проектами в GitHub и GitLab.
  • RabbitMQ.
  • Базами данных, пользователями и правами в MySQL.

И это только официальные провайдеры, неофициальных провайдеров еще больше. В ходе экспериментов я натыкался на GitHub на сторонний, не включенный в официальный список провайдер, который позволял работать с DNS от GoDaddy, а также с ресурсами Proxmox.

В рамках одного Terraform проекта вы можете использовать разные провайдеры и, соответственно, ресурсы разных поставщиков услуг или технологии. Например, вы можете управлять инфраструктурой в AWS, с внешним DNS — от GoDaddy. А завтра ваша компания купила стартап который хостился в DO или Azure. И пока вы решаете мигрировать это в AWS или нет, вы также можете это поддержать с помощью того же инструмента!

Ресурсы. Это сущности облака, которые вы можете создавать, используя Terraform. Их список, синтаксис и свойства зависят от используемого провайдера, по сути — от используемого облака. Или не только облака.

Модули. Это сущности, при помощи которых Terraform позволяет вам шаблонизировать вашу конфигурацию. Тем самым шаблоны позволяют сделать ваш код меньше, позволяют переиспользовать его. Ну и помогают комфортно с ним работать.

Почему мы выбрали Terraform


Для себя мы выделили 5 основных причин. Возможно с вашей точки зрения не все они покажутся значимыми:

  • Terraform — это Сloud Agnostic multiple cloud support утилита (спасибо за ценное замечание в комментариях). Когда мы выбирали этот инструмент, думали: — А что будет, если завтра или через неделю к нам придет менеджмент и скажет: "Ребята, а мы подумали — давайте-ка будем разворачиваться не только в Amazon. У нас есть какой-то проект, где нам нужно будет инфраструктуру завести в Google Cloud. Или в Azure — ну, мало ли". Мы решили, что нам хотелось бы иметь инструмент, который не будет жестко привязан к какому-либо облачному сервису.
  • Открытый код. Terraform — это опенсорсное решение. У репозитория проекта рейтинг больше 16 тысяч звезд, это неплохое подтверждение репутации проекта.

    Мы не раз и не два сталкивались с тем, что в некоторых версиях бывают баги или не совсем понятное поведение. Наличие открытого репозитория позволяет убедиться в том, что это действительно баг, и мы можем решить проблему, просто обновив движок или версию плагина. Или что это баг, но «Ребята, подождите, буквально через два дня выйдет новая версия и мы его пофиксим». Или: «Да, это что-то непонятное, странное, с ним разбираются, но есть work-around». Это очень удобно.
  • Контроль. Terraform как утилита находится полностью под вашим контролем. Его можно установить на ноутбук, на сервер, он может быть легко встроен в ваш пайплайн, который может быть сделан на базе любого инструмента. Мы например используем его в GitLab CI.
  • Проверка состояния инфраструктуры. Terraform умеет и хорошо проверяет состояния вашей инфраструктуры.

    Предположим, вы начали использовать Terraform в своей команде. Вы создаете описание какого-то ресурса в Amazon, например Security Group, применяете — она у вас создается, все хорошо. И тут — бац! Ваш коллега, который вчера вернулся из отпуска и еще не в курсе, что вы тут все так красиво устроили, или вообще коллега из другого отдела заходит и настройки этой Security Group меняет ручками.

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

    Когда Terraform просматривает ваш код, он параллельно с этим обращается к API облачного провайдера, получает от него состояние объектов и сравнивает: «А сейчас там то же самое, что я делал до этого, о чем я помню?» Потом сравнивает это с кодом, смотрит, что нужно еще изменить. И, например, если в его стейте, в его памяти, и в вашем коде всё одинаково, но там есть изменения — он вам покажет, и предложит его откатить. На мой взгляд, тоже очень хорошее свойство. Таким образом, это еще один шаг, лично для нас, к тому, чтобы получить иммутабельную инфраструктуру.
  • Еще одна очень важная фича — это модули, о которых я упоминал, и counts. Об этом я чуть попозже расскажу. Когда буду сравнивать с инструментами.

А также вишенка на торте: у Terraform-а есть довольно большой список встроенных функций. Эти функции, несмотря на декларативный язык, позволяют нам реализовать некоторую, не сказать чтобы программную, но логику.

Например, некоторые авто-вычисления, split строк, приведение к нижнему и верхнему кейсу, удаление символов этой строки. Мы это довольно активно используем. Они сильно облегчают жизнь, особенно когда вы пишите модуль, который потом будет переиспользован в разных окружениях.

Terraform vs CloudFormation


В сети часто сравнивают Terraform с CloudFormation. Мы тоже этим вопросом задавались, когда выбирали его. И вот результат нашего сравнения.

Сравнение Terraform CloudFormation
Multiple cloud support За счет использования различных провайдеров-плагинов может работать с любым крупным облачным провайдером.
Жестко привязан к Amazon.
Отслеживание изменений Если у вас произошло изменение
не в коде TF, а на ресурсе, который он создал, TF сможет это обнаружить и позволит Вам исправить ситуацию
Аналогичная функция появилась
лишь в ноябре 2018 года.
Условия Нет поддержки условий (только
в виде тернарных операторов).
Условия поддерживаются.
Хранение
состояний
Позволяет здесь выбрать несколько видов бэкэнда, например локально
на вашей машине (это поведение по умолчанию), на файловой шаре,
в S3 и где-нибудь еще.

Это порой бывает полезно, потому что tfstate Terraform представлен в виде большого текстового файла JSON- подобной структурой. И бывает порой полезно в него залезть, почитать — и как минимум иметь возможность сделать его бекап, потому что мало ли что. Лично мне, например, спокойнее от того, что это находится в каком-то контролируемом мной месте.
Хранение состояния только где-то внутри AWS
Импорт ресурсов Terraform позволяет легко импортировать ресурсы. Вы можете взять все ресурсы под свой контроль. Вам достаточно написать код, который будет характеризовать этот объект, или использовать Terraforming.
Она ходит в тот же Amazon, забирает оттуда информацию о стейте окружения и потом вываливает в виде кода.
Он машинно-сгенерированный, не оптимизированный, но это хороший первый шаг чтобы начать миграцию. А потом вы просто даете команду на импорт. Terraform сравнит, заведет это в свое состояние окружение — и теперь он им управляет.
CloudFormation такого не умеет. Если
у вас что-то было сделано до этого руками, вы либо это грохнете и пересоздайте с помощью CloudFormation, либо живите так дальше. К сожалению, без вариантов.

Как начать работу с Terraform


Вообще говоря, начать довольно просто. Вот кратко первые шаги:

  1. Прежде всего, создайте Git-репозиторий и сразу начните хранить там все ваши изменения, эксперименты, вообще всё.
  2. Прочитайте Getting-started guide. Он маленький, простенький, довольно подробный, хорошо описывает то, как вам начать работать с этой утилитой.
  3. Напишите немного демонстрационного, рабочего кода. Можно даже скопировать какой то пример чтобы потом с ним поиграться.

Наша практика работы с Terraform


Исходники


Вы начали ваш первый проект и храните все в одном большом main.tf файле. Вот типовой пример (честно взял первое попавшееся с GitHub).

Ничего плохого, но размер кодовой базы со временем имеет свойство расти. Так же растут зависимости между ресурсами. Через какое то время файл становится огромным, сложным, нечитаемым, плохо сопровождаемым — и неосторожным изменением в одном месте можно натворить бед.

Первое, что я рекомендую — выделить так называемый core-репозиторий, или core-стейт вашего проекта, вашего окружения. Как только вы начнете создавать инфраструктуру при помощи Terraform, или ее импортировать — вы сразу столкнетесь с тем, что у вас есть некоторые сущности, которые будучи один раз развернутыми, настроенными, крайне редко меняются. Например, это настройки VPC, или сам VPC. Это сети, базовые, общие Security-groups типа SSH-access — можно собрать довольно большой список.

Нет смысла держать это в том же репозитории, что и сервисы, которые вы часто меняете. Выделите их в отдельный репозиторий и состыкуйте через такую фичу Terraformа, как remote state.

  • Вы уменьшаете кодовую базу того участка проекта, с которым вы часто работаете непосредственно.
  • Вместо одного большого tfstate-файла, который хранит в себе описание состояния вашей инфраструктуры два файла меньшего размера, и в конкретный момент времени вы работаете с одним из них.

В чем тут хитрость? Когда Terraform строит план, то есть обсчитывает, калькулирует то, что он должен изменить, применить — он пересчитывает полностью этот стейт, сверяется с кодом, сверяется с состоянием в AWS. Чем ваш стейт больше, тем план будет дольше строиться.

Мы пришли к этой практике тогда, когда у нас построение плана на все окружение в продакшене стал занимать 20 минут. За счет того, что мы вытащили в отдельный core всё, что мы не подвержено частым изменениям, мы сократили время построения плана вдвое. У нас есть идея, как это можно сократить дальше, разбив уже не только на core и non-core, но еще и по подсистемам, потому что они у нас связаны и обычно меняются вместе. Тем самым мы, скажем, 10 минут превратим в 3. Но мы пока в процессе реализации такого решения.

Кода меньше — читать легче


С небольшим кодом легче разобраться и удобнее работать. Если у вас большая команда и в ней люди с разным уровнем опыта — вынесите то, что вы меняете редко, но глобально, в отдельную репу, и предоставьте к ней более узкий доступ.

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

Выделение core-репозитория происходит в несколько шагов.

Этап 1. Создайте отдельный репозиторий. Храните в нем весь код, отдельно — и описываете те сущности, которые должны быть переиспользованы в стороннем репозитории при помощи такого вывода. Скажем, мы создаем ресурс AWS subnet, в котором описываем, где он располагается, какая зона доступности, адресное пространство.

resource "aws_subnet" "lab_pub1a" {
 vpc_id            = "${aws_vpc.lab.id}"
 cidr_block        = "10.10.10.0/24"
 Availability_zone = "us-east-1a"
 ...
}
output "sn_lab_pub1a-id" {
 value = "${aws_subnet.lab_pub1a.id}"
}

А потом говорим, что мы в output отправляем id этого объекта. Можно сделать по output на каждый параметр, который вам необходим.

В чем здесь хитрость? Когда вы описываете значение, Terraform отдельно сохраняет его в tfstate core. И когда вы будете к нему обращаться, ему не нужно будет синхронизировать, пересчитать — он сможет сразу из этого стейта вам это дело отдать. Дальше, в репозитории, который non-core, вы описываете такую связь с удаленным state: у вас есть remote state такой-то, он лежит в S3-bucket таком-то, такой-то ключ и регион.

Этап 2. В non-core проекте создаем ссылку на стейт core проекта, чтобы мы могли обратиться к экспортированным через output параметрам.

data "terraform_remote_state" "lab_core" {
 backend = "s3"
 config {
   bucket  = "lab-core-terraform-state"
   key     = "terraform.tfstate"
   region  = "us-east-1"
 }
}

Этап 3. Начинаем использовать! Когда мне нужно развернуть новый сетевой интерфейс для инстанса в какой-то конкретной подсетке, я говорю: вот data remote state, в нем найди имя этого стейта, в нем найди вот этот вот параметр, который, собственно, совпадает, вот с этим именем.

resource "aws_network_interface" "fwl01" {
  ...   
  subnet_id = "${data.terraform_remote_state.lab_core.sn_lab_pub1a-id}"
}

И когда я буду строить план изменений в моем не core-репозитории, вот это значение для Terraform станет для него константой. Если вы захотите его изменить — придется делать это в репозитории вот этого конечно, core. Но так как это меняется редко, то это особо вас не тревожит.

Модули


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

Модуль — это крайне удобная вещь в силу того, что вы редко создаете один ресурс просто так, в вакууме, обычно он с чем-то логически связан.

module "AAA" {
  source = "..."
  count = "3"
  count_offset = "0"
  host_name_prefix = "XXX-YYY-AAA"
  ami_id = "${data.terraform_remote_state.lab_core.ami-base-ami_XXXX-id}"
  subnet_ids = ["${data.terraform_remote_state.lab_core.sn_lab_pub1a-id}",
                "${data.terraform_remote_state.lab_core.sn_lab_pub1b-id}"]
  instance_type = "t2.large"
  sgs_ids = [ "${data.terraform_remote_state.lab_core.sg_ssh_lab-id}",
              "${aws_security_group.XXX_lab.id}" ]
  boot_device = {volume_size = "50" volume_type = "gp2"}
  root_device = {device_name = "/dev/sdb" volume_size = "50" volume_type = "gp2" encrypted = "true"}
  tags = "${var.gas_tags}"
}

Например: когда мы разворачиваем новый EC2-инстанс, мы делаем для него сетевой интерфейс и attachment, мы часто делаем для него Elastic IP-адрес, мы делаем route-53 запись, и что-то еще. То есть, у нас как минимум получаются 4 сущности.

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

В Terraform есть фича Count, это позволяет еще сильнее сократить ваш стейт. Можно одним куском кода описать большую пачку инстансов. Скажем, мне нужно развернуть 20 однотипных машин. Я не буду писать 20 кусков кода даже из шаблона, я напишу 1 кусочек кода, укажу в нем Count и число — сколько мне нужно сделать.

Например, есть некоторые модули, которые ссылаются на шаблон. Я передаю только специфические параметры: ID subnet; AMI, с которой развернуть; тип инстанса; настройки секьюрити-групп; что-нибудь еще, и указываю, сколько мне таких штук сделать. Отлично, взял их и развернул!

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

Условно можно модули разделить на два типа — ресурсные и инфраструктурные. С точки зрения кода отличия нет, это скорее более высокоуровневые понятия, которые вводит сам оператор.
Ресурсные модули дают шаблонизированную и параметризованную, логически связанную совокупность ресурсов. Пример выше — это типичный ресурсный модуль. Как с ними работать:

  • Указываем путь к модулю — источник его конфигурации, через директиву Source.
  • Указываем версию — да, и эксплуатация по принципу “latest and greatest” тут не лучший вариант. Вы же не включаете каждый раз в свой проект последнюю версию библиотеки? Но об этом чуть позже.
  • Передаем в него аргументы.

Мы привязываемся к версии модуля, и берем просто последнюю — инфраструктура должна быть версионной (ресурсы не могут быть версионными, а код может). Ресурс может быть создан удален или пересоздан. Все! Так же мы должны четко знать, какой версии у нас создан каждый кусок инфраструктуры.

Инфраструктурные модули довольно просты. Они состоят из ресурсных, и включают стандарты компании (например теги, списки стандартных значений, принятые дефолты и так далее).

Что касается нашего проекта и нашего опыта — мы давно и прочно перешли на использование ресурсных модулей для всего, чего только можно, с очень жестким процессом версионирования и ревью. И сейчас активно внедряем практику инфраструктурных модулей на уровне лаб и стейджинга.

Рекомендации по использованию модулей

  1. Если можете не писать, а использовать готовые — не пишите. Особенно если в этом вы новичок. Доверьтесь готовым модулям или хотя бы посмотрите как это сделали до вас. Однако, если у вас все же есть необходимость писать свое — не используйте внутри обращение к провайдерам и будьте аккуратны с провиженерами.
  2. Проверьте, что Terraform Registry не содержит уже готовый ресурсный модуль.
  3. Если пишете свой модуль — спрячьте специфику под капот. Конечный пользователь не должен волноваться о том, что и как вы реализуете внутри.
  4. Делайте input параметров и output значений из вашего модуля. И лучше, если это будут отдельные файлы. Так удобней.
  5. Если пишите свои модули — храните их в репозитории и версионируйте. Лучше отдельный репозиторий под модуль.
  6. Не используйте локальные модули — они не версионируемые и не переиспользуемые.
  7. Избегайте использования описания провайдеров в модуле, потому что подключение credentials может быть настроено и применяться по разному у разных людей. Кто-то использует переменные окружения для этого, а кто то подразумевает хранение своих ключей и секретов в файлах с прописыванием путей для них. Это надо указывать уровнем выше.
  8. Осторожно используйте local provisioner. Он исполняется локально, на той машине, на которой запускается Terraform, но среда исполнения у разных пользователей может быть разная. До тех пор пока вы не встроите это в CI, вы можете натыкаться на различные артефакты: например local exec и запуск ansible. А у кого-то другой дистрибутив, другой shell, другая версия ansible, или вообще Windows.

Признаки хорошего модуля (вот чуть подробнее):

  • Хорошие модули имеют документацию и описание примеров. Если каждый оформлен в виде отдельного репозитория — это легче сделать.
  • Не имеют жестко заданных значений параметров (например регион AWS).
  • Используют разумные значения по умолчанию, оформленные в виде defaults. Например модуль для EC2 инстанса по умолчанию не будет создавать вам виртуальную машину с типом m5d.24xlarge, использует для этого что-то из минимальных t2 или t3 типов.
  • Код «чист» — структурирован, снабжен комментариями, не запутан излишне, оформлен в едином стиле.
  • Очень желательно чтобы он был снабжен тестами, хоть это и сложно. К этому мы к сожалению, еще сами не пришли.

Тегирование


Теги — это важно.

Тегирование — это биллинг. У AWS есть инструменты, которые позволяют вам посмотреть, сколько денег вы тратите на свою инфраструктуру. И нашему менеджменту очень хотелось иметь инструмент, в котором они могли это посмотреть детерминировано. Например, сколько денег потребляют такие-то компоненты, или такая-то подсистема, такая-то команда, такое-то окружение

image

Тегирование — это документирование вашей системы. С его помощью вы упростите себе поиск. Даже просто в AWS-консоли, где эти теги аккуратненько выведены себе на экран — вам становится проще понимать, к чему относится тот или иной тип инстанса. Если приходят новые коллеги, вам проще это объяснить, показав: «Смотри, это вот — сюда». Мы начинали создавать тэги следующим образом — создавали массив тегов для каждого вида ресурсов.

Пример:

variable "XXX_tags" {
 description = "The set of XXX tags."
 type = "map"
 default = {
             "TerminationDate" = "03.23.2018",
             "Environment" = "env_name_here",
             "Department" = "dev",
             "Subsystem" = "subsystem_name",
             "Component" = "XXX",
             "Type" = "application",
             "Team" = "team_name"
           }
}

Так получилось, что у нас в компании не одна наша команда использует AWS, и есть некоторый список обязательных тегов.

  1. Team — какая команда использует сколько ресурсов.
  2. Department — аналогично с департаментом.
  3. Environment — ресурсы бьются по «окружениям», но вы, например, можете заменить его на проект или что то подобное.
  4. Subsystem — подсистема к которой относится компонент. Компоненты могут относиться к одной подсистеме. Например, мы хотим посмотреть, сколько у нас эта подсистема и ее сущности стали потреблять. Вдруг она, допустим, за предыдущий месяц сильно выросла. Нам нужно прийти к разработчикам и сказать: «Ребята, дорого стоит. Бюджет вот уже впритык, давайте уже как-то оптимизировать логику».
  5. Type — тип компонента: балансировщик, хранилище, приложение или база данных.
  6. Component — сам компонент, его название во внутренней нотации.
  7. Termination date — время когда он должен быть удален, в формате даты. Если его удаление не предвидится, ставим “Permanent”. Мы ввели его, потому что в девелоперских окружениях, и даже в некоторых стейджовых у нас есть стейдж для стресс-тестирования, который поднимается на стресс-сессии, то есть мы не держим эти машинки регулярно. Мы указываем дату, когда ресурс должен быть уничтожен. Дальше к этому можно прикрутить автоматизацию на базе лямбды, каких-то внешних скриптов, которые работают через AWS Command Line Interface, которые будут по крону уничтожать эти ресурсы автоматически.

Теперь — том, как тегировать.

Мы решили, что будем делать для каждого компонента свою тег-мапу, в которой будем перечислять все указанные теги: когда его терминировать, к чему относится. Очень быстро поняли, что это неудобно. Потому что кодовая база у нас растет, поскольку у нас больше 30 компонент, и 30 таких кусков кода — неудобно. Если нужно что-то поменять, то бегаешь и меняешь.

Чтобы хорошо тегировать, мы используем сущность Locals.


locals {
 common_tags = {"TerminationDate" = "XX.XX.XXXX",
                  "Environment" = "env_name",
                  "Department" = "dev",
                  "Team" = "team_name"}
 subsystem_1_tags = "${merge(local.common_tags,
                        map("Subsystem", "subsystem_1_name"))}"
 subsystem_2_tags = "${merge(local.common_tags,
                        map("Subsystem", "subsystem_2_name"))}"
}

В ней вы можете перечислить подмножество, и потом их друг с другом использовать.

Например, мы вынесли некоторые common-теги вот в такую структуру, и дальше — специфичные, по подсистемам. Мы говорим: «Возьми вот этот блок и в него добавь, например, subsystem 1. А для подсистемы 2 добавь subsystem 2». Мы говорим: «Теги, возьми, пожалуйста, общие и к ним добавь type, application, имя, component и кто такой». Получается очень кратко, наглядно и централизованное изменение, если вдруг это потребуется.


module "ZZZ02" {
   count         = 1
   count_offset  = 1
   name          = "XXX-YYY-ZZZ"
   ...
   tags          = "${merge(local.core_tags, map("Type", "application",
                                                 "Component", "XXX"))}"
}


image

Контроль версий


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

module "ZZZ02" {
 source        = "./modules/srvroles/ZZZ"
 name          = "XXX-YYY-ZZZ"
}

Предположим, к вам пришли разработчики и сказали: «Так, нам нужна такая-то сущность в такой-то конфигурации, в нашей инфраструктуре». Вы это написали, сделали в виде локального модуля в репозитории их проекта. Развернули — отлично. Они потестили, сказали: «Пойдет! В продакшн». Приходим в стейдж, стресс-тестирование, продакшн. Каждый раз Ctrl-C, Ctrl-V; Ctrl-C, Ctrl-V. Пока мы добрались до прода, наш коллега взял, скопировал код из лабораторного окружения, перенес в другое место и там поменял. И у нас получается уже несогласованное состояние. При горизонтальном масштабировании, когда у вас столько лабораторных окружений, сколько у нас — это просто адище.

Поэтому хороший путь — заводить под каждый ваш модуль отдельный Git-репозиторий, и потом просто на него ссылаться. Меняем всё в одном месте — хорошо, удобно, контролируемо.

module "ZZZ" {
 source = "git::ssh://git@GIT_SERVER_FQDN/terraform/modules/general-vm/2-disks.git"
 host_name_prefix = "XXX-YYY-ZZZ"

Упреждая вопрос, как же ваш код доезжает до продакшена. Для этого создается отдельный проект, который переиспользует подготовленные и проверенные модули.

Отлично, у нас один источник кода, который централизованно меняется. Я взял, написал, подготовил и поставил себе, что завтра с утра иду разворачивать в продакшн. Построил план, протестировал — отлично, идем. В этот момент мой коллега, руководствуясь исключительно благими побуждениями, пошел и что-то оптимизировал, добавил в этот модуль. И так получилось, что эти изменения ломают обратную совместимость.

Например, он добавил необходимые параметры, которые обязан передать, иначе модуль не соберется. Или он поменял названия этих параметров. Я прихожу с утра, у меня время для изменений строго ограничено, начинаю строить план, и Terraform подтягивает стейт-модули с Git-а, начинает строить план и говорит: «Упс Не могу. Не хватает у тебя, ты переименовал». Я удивляюсь: «Да я же этого не делал, как с этим быть?» А если это ресурс, который создан давно, то после подобных изменений придется пробегать по всем окружениям, как-то менять и приводить к одному виду. Это неудобно.

Это можно поправить, используя Git tags. Мы для себя решили, что будем использовать SemVer-нотацию и выработали простое правило: как только конфигурация нашего модуля достигает некоего стабильного состояния, то есть мы можем это использовать, мы на этот коммит вешаем тег. Если мы вносим изменения и они не ломают обратную совместимость, мы у тега меняем минорный номер, если ломают — меняем мажорный номер.

Так в адресе source привязаться к конкретному тегу и если хотя бы обеспечить что-то, что у вас собиралось раньше — будет собираться всегда. Пусть версия модуля уехала вперед, но в нужный момент мы придем, и когда нам это действительно нужно — поменяем. А то, что и до этого было работающим, хотя бы не сломается. Это удобно. Вот так примерно это выглядит у нас в GitLab.

image

Ветвление


Использование ветвления — еще одна важная практика. Мы для себя выработали правило, что изменения ты должен вносить только из мастера. Но на любое изменение, которое ты хочешь сделать и протестировать — сделай, пожалуйста, отдельную ветку, поиграй с ней, поэкспериментируй, построй планы, посмотри, как собирается. А потом сделай merge-request, и пусть коллега посмотрит на код и поможет.

image

Где хранить tfstate


Не стоит хранить ваш стейт локально. Не стоит хранить ваш стейт в Git-е.

Мы на этом обожглись, когда у кого-то при раскатывании веток не-мастера получается свой tfstate, в котором сохранено состояние — потом он это включает через merge, кто-то добавляет свой, получаются merge-конфликты. Или получается без них, но несогласованное состояние, потому что «у него уже есть, у меня еще нет», и потом все это сидеть исправлять — это неприятная практика. Поэтому мы решили, что будем хранить это в надежном месте, версионируемом, но это будет вне Git-а.

Под это отлично подходит S3: он доступен, у него HA, насколько я помню четыре девятки точно, может быть, пять. Из коробки он дает версионированость, если даже вы свой tfstate сломаете — всегда можно откатиться. И еще он дает очень важную вещь в сочетании с DynamoDB, этому Terraform научился, по-моему, с версии 0.8. В DynamoDB вы заводите табличку, в которой Terraform записывает информацию о том, что он блокирует стейт.

То есть, предположим, я хочу внести какие-то изменения. Начинаю строить план или начинаю его применять, Terraform идет в DynamoDB и говорит, что он в этой табличке вносит информацию о том, что этот state заблокирован; пользователь, компьютер, время. В этот момент мой коллега, который работает удаленно или, может быть, в паре столов от меня, но сосредоточен на работе и не видит, что я делаю, тоже решил, что нужно что-то изменить. Он строит план, но запускает его чуть позже.

Terraform идет в динамку, видит — Lock, обламывается, сообщает пользователю: «Извини, tfstate заблокирован тем-то». Коллега видит, что я сейчас работаю, может ко мне подойти и сказать: «Слушай, у меня чейндж важнее, уступи мне, пожалуйста». Я говорю: «Хорошо», отменяю построение плана, снимаю блок, скорее даже, он автоматически снимается, если вы это делаете корректно, не прерывая по Ctrl-C. Коллега идет и делает. Тем самым мы страхуем себя от ситуации, когда вы вдвоем что-то меняете.

Merge-request


Мы используем ветвление в Git-е. Мы назначаем наши merge-request-ы на коллег. Более того, в Gitlab мы используем практически все доступные нам инструменты для совместной работы, для merge-request-ов или даже просто каких-то пулов: обсуждение вашего кода, его ревью, выставление in-progress или issue, еще чего-то подобного. Это очень полезно, это помогает в работе.

Плюс, в этом случае rollback тоже получается легче, можно вернуться на предыдущий коммит или, если вы, скажем, решили, что будете не только из мастера применять изменения, можно просто переключиться на стабильную ветку. Например, вы сделали ветку с фичей и решили, что будете вносить изменения сначала из фичевой ветки. А потом уже изменения, после того, как все хорошо сработало, вносить в мастер. Вы в своей ветке применили изменения, поняли, что что-то не то, переключились на мастер — никаких изменений нет, сказали apply — он вернулся.

Пайплайны


image

Мы решили что нам необходимо использовать CI процесс для применения наших изменений. Для этого на базе Gitlab CI мы пишем пайплайн, который автоматизирует применение изменений. Пока что у нас их два вида:

  • Пайплайн для мастер веток (master pipeline)
  • Пайплайн для всех прочих веток (branch pipeline)

Что делает бранч пайплайн? Он запускает автоматическую верификацию кода (тупо проверку на опечатки, например). А потом запускает построение плана. И коллега, который будет смотреть ваш merge-request, сразу может открыть построившийся план и увидеть не только код — а также то, что вы добавляете. Еще он увидит как это ляжет на вашу инфраструктуру. Это наглядно и полезно.

image

В мастере сюда добавляется еще один шаг. Отличие в том, что у вас план не просто генерируется, он еще и сохраняется в виде артефакта. Еще одна очень полезная фича Terraform-а в том, что план можно сохранить в виде файла, и потом применить его. Скажем, вы сделали merge-request и его отложили. Через месяц про него вспомнили и решили вернуться. У вас код уже далеко уехал вперед. За счет того, что вы храните у себя артефакт плана, вы можете применить именно его и на то, что вы хотели в тот момент.

image

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

Недостатки Terraform


Функции. Несмотря на то что у Terraformа довольно большое число встроенных функций, не все из них так хороши, как нам хотелось бы думать.

Есть в нем неудобные функции, например «Элемент» — у нее в некоторых ситуациях при нехватке опыта поведение может быть не совсем тем, которое вы ожидали.

Например, вы используете модуль, в модуль передается count — сколько развернуть инстансов, и передается, скажем, список подсетей, разбитых по availability-зонам. Передали, применили, увеличили каунт, еще применили. А теперь вы решили передать в него увеличенный список подсетей. У вас появилась сетка, вы еще одну AZ решили задействовать. У вас меняется вторая часть списка, а count с этим списком сопоставляется через элемент.

Скажем, у вас было 4 AZ до этого и 5 инстансов, а потом вы добавили еще одну AZ — он первые 4, которые уже были по порядку, оставит. А про пятую скажет: «А сейчас я ее пересоздам». А вы не хотели! Вы хотели, чтобы у вас только новые приезжали. Такие баги происходят из за особенностей работы Terraform со списками.

Тернарный оператор. Условие — только тернарный оператор. Нам действительно не хватает условий. Хотелось бы все-таки какие-то более привычные If и Else. Жаль, что их нет — возможно, подвезут.

Сложности командной работы. Если у вас большая команда, или большой проект, на большое число окружений, или и то и другое, Terraform-ом вам становится сложновато пользоваться, не применив некоторый CI.

Без CI вы будете вносить изменения из своих локальных окружений со своего компьютера. По нашему опыту это ведет к тому, что вы ветку у себя сделали, завели, поэкспериментировали с ней — и забыли сделать merge, забыли запушить изменения. Это больно.

Например, у вас с коллегой были одинаковые версии на машинах. Потом коллега у себя обновил на единичку версию. Вы на следующий день приходите, начинаете вносить изменения, Terraform идет свериться, видит, что в tfstate требуемая версия Terraformа выше и говорит: «Нет, не могу, обнови меня». Когда у тебя маленькое окно для внесения изменений, то непросто увидеть, что тебе сначала нужно обновить утилиту.

Когда у вас есть CI, есть некоторая единая сущность, например в вашем pipeline контейнер — вы себя страхуете, что у вас не будет такого разъезжания версий утилиты.

Ну и наконец, в мастере может накапливаться сломанный или неиспользованный код. Вам будет каждый раз со своего места лень ждать, пока построится план на все окружение. Вы придете к тому, что будете стараться строить через опцию target применение только на то, что вы изменили. Например, вы добавили некоторый инстанс и говорите: «Terraform apply target instance», или секьюрити-груп. Но в таком случае если у вас что-то сломалось (скажем, устарела какая-то конфигурация), при построении полного плана вы бы это увидели.

Вам придется потратить довольно много сил и времени на то, чтобы привести это в актуальное состояние. Не нужно до такого доводить. Если есть CI — в нем мы просто принудительно говорим, что Terraform будет строить план полностью, вы запушили изменения. И пусть он свой план строит, вы пошли и занялись чем-то еще. Он построил, вы увидели, он у вас есть в виде артефакта, и вы пошли его применять. Это дисциплинирует.

Terraform — не серебряная пуля


Что он не позволит вам сделать:

  • Terraform не позволяет вам описать зависимость между модулями, если они логически на одном уровне. С чем, например, столкнулись мы. У нас есть модуль, описывающий набор некоторых инстансов со всеми сопутствующими параметрами, и есть модуль, который описывает балансировщик. Когда мы хотим одно с другим состыковать, то на вход модуля, описывающего балансировщик, подаем из первого модуля с инстансами генерируемый список айдишников.

    Так вот, пока вы не создали инстанс — этот список еще не появился в Tfstate, и модуль с балансировщиками не сможет собраться, потому что ему нечего будет к себе стыковать. То есть вам придется разворачивать это в два прохода. Сделать зависимость «сначала разверни этот модуль, а потом этот модуль» — не получается.

    Мы сейчас пытаемся это реализовать за счет того что разбиваем нашу инфраструктуру по подсистемам и пишем модуль, во-первых, для ресурса, а во-вторых — модуль для подсистемы. Как раз те самые инфраструктурные модули, в которых уже модули как бы стыкуем на одном уровне. Пока не могу ничем похвастаться, мы лишь обкатываем это решение в лабе и первое с чем столкнулись из неприятного — сложно разрешаемые зависимости версий.
  • Terraform может успешно построить план, но этот план успешно не применяется. И Terraform в этом не виноват. Почему? Потому что он не отслеживает число свободных айпишников в ваших подсетях, которые у вас остались. Он не может его добыть. Например, он не отслеживает, что в некоторых AZ у вас нет каких-то инстанс-типов. Скажем, мы используем North Virginia, и там сейчас есть 6 зон доступности. В одной из них точно доступны не все типы инстансов. Мы с этим столкнулись, выясняли с техподдержкой, они сказали: «Да, это временное явление». Но до какого момента это будет — непонятно. Опять же — план у нас при этом строится, всё хорошо, Terraform ничего об этом не знает.
  • Terraform ничего не знает про ваши лимиты в Амазоне. Скажем, у вас лимит — 200 машин, из них уже 198 развернули, хотите развернуть еще 5. План он вам построит. Но при выполнении плана он сделает две, а еще на три вернет вам ошибку от API Амазона. Увы.
  • Также он не может учесть, что некоторые имена должны быть уникальными. Например, вы хотите сделать S3 bucket. Это глобальный сервис на регион, и даже если в вашем аккаунте вы не создавали сервис с таким именем — не факт, что его не создал кто-то другой. И когда вы будете его создавать с помощью Terraform – он прекрасно построит план, начнет его создавать, а Амазон скажет: «Извини, у кого-то это уже есть». Заранее этого не предусмотреть никак. Только если руками пытаться заранее как-то это сделать, хотя это идет вразрез с практикой.

В любом случае, Terraform — лучшее, что есть сейчас. И мы продолжаем это использовать, он очень сильно нам помогает.

Комментарии (14)


  1. ahanoff
    09.10.2019 14:17

    kakazarin рассматривали ли pulumi?


    1. kakazarin Автор
      09.10.2019 15:07

      Нет. Если честно вот только из Вашего комментария о нем узнал. Посмотрел их гихаб. Первый коммит в сентябре 17 года. Наш проект в то время уже жил в проде и управлялся TF, поэтому без шансов.
      github.com/pulumi/pulumi/releases?after=v0.9.2


  1. ahanoff
    09.10.2019 14:29
    +1

    Terraform — это Сloud Agnostic утилита.

    Нет, это заблуждение. Cloud agnostic это если бы вы просто поменяли provider: aws на что-то другое и у вас все заработает. Но нет, при смене облака, вам придется переписать все.
    Terraform вам дает лишь hcl (hashicorp configuration language), т.е. да не придется учить другой язык, но все равно придется изучить ньюансы другого облака, термины и как все это написать под другой провайдер. Правильный термин — multiple cloud support


    1. kakazarin Автор
      09.10.2019 15:04

      Спасибо, учту и внесу исправление!


  1. kt97679
    09.10.2019 22:14

    Как вы отлаживаете ваш terraform код?


    1. kakazarin Автор
      10.10.2019 11:52

      Поясните пожалуйста что Вы имеете ввиду, то есть конкретней. Если я сейчас правильно понял Ваш вопрос, тогда следующим образом — любой модуль, любое изменение проходит тестирование в Lab на тестовом окружении нашей команды. Потом оно «долетает» до других Lab окружений разработчиков где уже интегрируется с изменениями софта, настроек и тп. Когда мы считаем что изменение готово — оно идет в stage и после — в prod.


      1. kt97679
        10.10.2019 17:49

        Прошу прощения, я немного другое имел в виду. При переходе на terraform 0.12 hashicorp основательно поломали обратную совместимость. В частности я столкнулся с большим количеством ошибок вызванных неправильными типами переменных. Я не нашел способа посмотреть на содержимое и тип переменных, исправлять приходилось буквально методом тыка. terraform 0.12upgrade помогал далеко не всегда. TF_LOG=TRACE тоже не сильно прибавил ясности. Может вы знаете какой-то другой способ отладки, который бы помог в этой ситуации?


  1. Amet13
    09.10.2019 22:20

    Использовали ли для импорта что-то? Например terraformer или terraforming?
    Как у вас устроена структура репозитория? А стейтов? Использовали ли интеграцию с Atlantis? Пишете ли тесты?


    1. kakazarin Автор
      10.10.2019 11:58
      +1

      Используем terraforming. Сейчас уже почти нет, тк изначально все изменения совершаются через TF. Ранее приходилось применять довольно часто. Плюс мы достигли такого уровня «дзен» когда можем описать весь набор ресурсов в виде модуля, импортнуть все связанные AWS сущности внутрь и потом при помощи plan ( как тест/сравнение) довести конфигурацию до нужного вида. То есть миновать шаг «получили машинно сгеренированный код, надо его причесать»
      Про структуру репозиториев — их много. По сути каждый модуль ( вернее то, что его реализует) — это репозиторий. Есть репозитории ресурсных модулей, есть инфраструктурных, есть проекты которые все это исмпользуют. Вас интересуют все они в каком-то «общем» виде или что-то конкретное? Напишите, я постараюсь пояснить.

      Стейты у нас хранятся в S3 бакете с шифрованием и версионированием. Ну и конечно ограничением чрез IAM. 1 бакет на aws аккаунт, стейт каждого проекта имеет уникальное имя. Была сначала идея делать бакет под проект но отказались.

      Интеграции с Atlantis как и самого Atlantis нет. Есть Gitlab + Gitlab CI, есть Ansible (как средство управления конфигурацией того, что было создано TF и мы сейчас все на него переводим), и немного легаси в виде puppet+foreman + RackTables с которыми ранее интегрировались при помощи самописных плагинов для TF.

      Тесты пока не пишем и не понятно будем ли писать


  1. f3ex
    09.10.2019 23:46

    подскажите, как вы используете count для module?
    module "AAA" {
    source = "..."
    count = "3"


    github.com/hashicorp/terraform/issues/953


    1. kakazarin Автор
      10.10.2019 12:04

      Вы приводите ссылку на оооочень старый комит) Давно уже есть поддержка. Используем очень просто — count передается внутрь модуля, во все зависимые сущности, помогая 1 шаблоном создавать нужное число ресурсов. Плюс внутри модуля через связку count, element и еще ряда функций зашита логика связывания этих ресурсов. Так например мы передаем в модуль число хостов, которые должны быть развернуты и список подсетей. Модуль сделает указанное число EC2 инстансов, сетевых интерфейсов, дисков, rt53 записей и пр, свяжет их и раскидает по сеткам.


      1. f3ex
        10.10.2019 12:26

        я вот даже прям сейчас пробую
        $ terraform apply

        Error: Reserved argument name in module block

        on nodes.tf line 4, in module «nodes»:
        4: count = 3

        The name «count» is reserved for use in a future version of Terraform.

        $ terraform -v
        Terraform v0.12.6
        + provider.openstack v1.22.0
        + provider.random v2.2.0
        + provider.template v2.1.2

        module «nodes» {
        source = "./modules/create_server"

        count = 3
        // server_count = "${var.server_count}"

        в моем случае я передаю другую переменную (server_count), которую потом подставляют в count ресурсов модуля, но вот именно count как аргумент модуля не работает


        1. kakazarin Автор
          10.10.2019 13:11

          у вас версия 0.12, я в начале статьи указываю что у нас 0.11 (последняя из этой линейки) и на 0.12 мы пока не переходили. Да, возможно поведение изменилось. У нас это выглядит вот так. Вызываем мы вот так

          module "test-ec" {
             count            = "1"
             count_offset     = "10"
             source           = "git::ssh://FQDN/terraform/modules/vm-general.git?ref=1.0.0"
             host_name_prefix = "${var.hostname_prefix}-srv"
             ami_id           = "${data.aws_ami.XXXXX-ami-al-2018.id}"
          ...
          


          а внутри модуля оно выглядит вот так:
          resource "aws_network_interface" "nic0" {
            count             = "${var.count}"
            subnet_id         = "${element(var.subnet_ids, count.index)}"
            security_groups   = ["${var.sgs_ids}"]
            source_dest_check = true
          ...
          
          data "aws_subnet" "subnet" {
            count = "${var.count}"
            id    = "${aws_network_interface.nic0.*.subnet_id[count.index]}"
          }
          
          
          resource "aws_instance" "instance" {
            count      = "${var.count}"
          ....
          
          resource "aws_ebs_volume" "ebs_volume" {
            count             = "${length(var.ebs_volumes) * var.count}"
          ...
          
          resource "aws_volume_attachment" "ebs_attachment" {
            count       = "${length(var.ebs_volumes) * var.count}"
          ...
          
          


          но мы еще дополнительно в input.tf ( мы разделяем модуль на 3 файла — описание модуля, input для входных данных и output для выходных) описываем cpunt как переменную, которая уже потом, как Вы видите выше обращается в count ресурса.
          Возможно в 0.12 это слово внесли в список служебных и больше такой красивый хак не проходит
          variable "count" {
            description = "Number of EC2 instances."
            default     = 0
          }
          
          variable "count_offset" {
            description = "Offset of instances. It uses for naming."
            default     = 0
          }
          
          


          1. f3ex
            10.10.2019 13:44

            понятно, спасибо. ну подход у вас тот же самый