Рассказываем про наш опыт импорта и адаптации конфигураций инфраструктуры, ранее развернутой вручную в AWS, в формат Terraform. Зачем? Причин может быть много: и отказоустойчивость, и упрощение горизонтального и вертикального масштабирования, и многие другие. С них и начнем эту статью. Без каких-либо процессов автоматизации управления инфраструктурой вы неизбежно столкнетесь с известными ограничениями: невозможно быстро пересоздать инфраструктуру; нет истории изменений, произведенных в инфраструктуре; нет контроля используемых версий ПО; … И это не просто вопрос удобства. Подобные ограничения влияют на критичные для бизнеса показатели: на скорость устранения возникающих проблем в инфраструктуре, на отказоустойчивость… в конечном счете — на uptime. Это понимание привело к расцвету подхода IaC. На уровне программных служб вопрос решается системами управления конфигураций (Ansible, Chef и т.п.), однако они не покрывают более низкий — аппаратный — слой, а ведь он тоже требует внимания. Многие из вас уже знакомы на практике с решениями и для этого уровня, такими как Terraform. Оно выглядит уместным, когда мы создаем инфраструктуру с нуля. Но будет ли оно таким же, если перед нами production, состоящий из десятков (сотен, …) компонентов? Ведь потребуются колоссальные трудозатраты на написание сценариев развертывания для каждого элемента. В этой статье мы подойдем к решению вопроса с другой стороны и попробуем импортировать уже существующую production-инфраструктуру, а затем — адаптировать полученные конфигурации для возможности бесшовного перехода на автоматизированный способ управления. Для реализации поставленной задачи — импорта конфигураций — были рассмотрены различные инструменты (в частности, сам Terraform, CLI-утилиту AWS, Terraforming), но мы остановились на утилите Terraformer. Основные причины — полнота и корректность выходных данных, а также удобство использования. Разработчик данного решения при выполнении аналогичной задачи столкнулся с ограничениями, упомянутыми во введении. В результате и появился этот проект, реализующий функции, которых нам так не хватало. Стоит отметить, что процедура импортирования и адаптации планировалась сразу в нескольких регионах, не связанных друг с другом. Работы при этом необходимо было провести итеративно, чтобы в один момент времени затрагивать только один регион. Решение — реализовать полностью готовое окружение, предварительно проверив его работу в тестовой среде. Поэтому будем использовать Docker-контейнер как удобный способ для быстрого разворачивания окружения с привязкой к конкретным версиям ПО. Собирать и запускать Docker-образа будем посредством утилиты werf, воспользовавшись её синтаксисом (stapel) вместо Dockerfile. В этом нет какого-либо архитектурного ограничения (обычный docker тоже подойдет) — все дело в более удобном/привычном подходе.  Возьмем базовый образ на основе Ubuntu и выполним в него установку Terraform и плагина Terraformer. Также потребуется добавить конфигурацию и файл с учетными данными доступа в AWS. Вот что получается в случае stapel-манифеста для werf (несложно переписать и на Dockerfile при такой необходимости): Итак, собираем образ, указав переменные с учетными данными пользователя AWS IAM. Значения можно явно указывать каждый раз при запуске команды вручную или же определить в CI-среде: Теперь запускаем контейнер, используя собранный образ и те же самые значения переменных. Если указать иные значения, то будет ошибка несоответствия конфигураций. Поскольку мы взаимодействуем с действующей инфраструктурой через слои абстракции, необходимо запускать исключительно то, что было собрано: Этап инициализации стартовой конфигурации Terraform для установки плагина провайдера можно включить в сам образ. Конечно, это увеличит его объем, но и предоставит полностью рабочее решение в рамках конкретного образа, используемого для проведения работ, — при любой итерации запуска (что важно в рамках задачи). Для предустановки плагина провайдера создадим файл (providers.tf) и поместим его в рабочую директорию в образе: Инициализируем конфигурацию Terraform. Если вы устанавливаете плагин провайдера на этапе сборки, то команду инициализации требуется включить последним шагом, при этом запуск вручную уже не потребуется. Выполняем импорт ресурсов. В данном примере используем следующие ключи: aws название провайдера; --path-pattern="{output}/" путь к файлам конфигураций, сгенерированным утилитой; --compact=true запись конфигураций всех типов ресурсов в единый файл. Файлы с описанием ресурсов, переменных, состояний будут записаны в разные файлы; --regions=eu-central-1 регион провайдера; --resources=elasticache,rds типы импортируемых ресурсов. Меняем рабочую директорию на каталог, созданный утилитой импорта, и запускаем импорт в данной директории (для инициализации импортированной конфигурации ресурсов): Выполняем предварительное планирование применения импортированной конфигурации: При планировании возникли несоответствия между файлом состояния, полученным при импорте, и действующей конфигурацией ресурсов. Поэтому Terraform сообщает, что собирается модифицировать два ресурса aws_db_instance. Это может происходить, например, из-за разницы в доступном наборе инструкций утилиты импорта и Terraform'а. Это вполне штатная ситуация, т.к. используемые утилиты не принадлежат одному вендору, и одна является абстракцией поверх другой. Кроме того, возможно и некоторое отставание по наличию доступного функционала. В данном случае в импортированной конфигурации ресурсов и файле состояния tfstate отсутствуют по два параметра в двух ресурсах aws_db_instance: delete_automated_backups skip_final_snapshot Для устранения конфликтов выясним актуальные значения указанных параметров через штатные интерфейсы AWS: web-gui или aws-cli. По результатам этого анализа дополним файл конфигурации ресурсов: … и выполним повторное планирование применения импортированной конфигурации: После внесенных изменений планирование выдает желаемый результат: На этом предварительная подготовка к адаптации ресурсов завершена и можно применять конфиг. Однако могут возникать ситуации, когда требуется дополнить конфиг ресурсов. Вернемся в исходную директорию и выполним импорт конфигурации для другого типа ресурсов, которые тоже требуется добавить в планируемый конфиг. Важно, что директория path-pattern должна отличаться от предыдущей, т.к. Terraformer не изменяет наименования файлов, в которые записывает результаты импорта, а значит при использовании той же директории перезапишет уже существующие файлы. В данном примере добавим к уже импортированной конфигурации ресурсы EC2, которые имеют общий тег NodeRole=node, указав это в ключе --filter: Переходим в директорию с импортированным конфигом и выполняем инициализацию: Выполняем планирование: По аналогии с предыдущим планированием приводим конфиг к нужному состоянию: … и делаем слияние файлов конфигов resources.tf простым переносом блоков конфигураций. Слияние файлов состояний tfstate делается через встроенную функцию Terraform — state mv. Важно, что добавление состояния в общий файл производится только для одного ресурса за раз. Поэтому при наличии нескольких таких ресурсов необходимо повторить операцию для каждого из них. Пришло время применить конфигурацию. Обязательно следует обратить внимание на то, что Terraform не планирует ничего изменять или удалять. Ведь даже самые, казалось бы, безобидные изменения могут вызвать перезапуск инстанса. В некоторых ситуациях возможно и полное пересоздание ресурса. Поэтому идеальным состоянием перед применением конфигурации является именно Plan: 0 to add, 0 to change, 0 to destroy. Для удобства можно загрузить файл tfstate в хранилище S3. Дальнейшее взаимодействие с этим файлом состояния будет производиться непосредственно механизмами Terraform. Преимущество такого варианта хранения — функция блокировки файла во время использования. Благодаря этому становится возможной одновременная работа команды инженеров над объемной инфраструктурой без риска конфликтов при запуске критичных операций. И, конечно же, размещение состояния в S3 предполагает надежность хранения и версионирование файла. Миграция файла состояния tfstate начинается с создания отдельного каталога, в котором будут описаны файлы конфигурации, предназначенные только для создания ресурсов, обеспечивающих функции хранения состояния. Изначально создадим конфиг провайдера: Затем — конфиг ресурса aws_s3_bucket. Данный ресурс создает выделенное хранилище в S3: Особенностями представленной конфигурации являются следующие параметры: bucket название каталога для хранения; lifecycle { prevent_destroy = true } запрет на удаление ресурса; versioning { enabled = true } включение версионирования файла terraform.tfstate на стороне S3-хранилища; server_side_encryption_configuration настройки шифрования. Также добавим в конфиг ресурс aws_dynamodb_table. Это таблица AWS DynamoDB, в которой будет храниться информация о блокировках файла terraform.tfstate: name название таблицы для записи информации о блокировках; billing_mode = "PAY_PER_REQUEST" тип оплаты. Значение PAY_PER_REQUEST позволяет оплачивать по количеству обращений к таблице; hash_keyattribute {  name = "LockID"  type = "S"} указание первичного ключа таблицы. Инициализируем конфигурацию, после чего выполняем планирование и применение: Переходим в директорию с конфигурацией ресурсов AWS и создаем в ней файл backend.tf следующего содержания: Если попытаться выполнить какие-либо действия с текущей конфигурацией ресурсов, то возникнет ошибка: Для ее устранения требуется повторно запустить инициализацию. При этом Terraform обратится в созданное ранее хранилище S3 и запросит файл terraform.tfstate. Поскольку хранилище еще пустое, он предложит нам перенести локальный state-файл в хранилище S3: И, наконец, планируем окончательную версию конфигурации: Теперь конфигурация обозначенных нами ресурсов успешно адаптирована. Можно сохранить ее в Git-репозитории, тем самым обеспечив версионирование и (в некотором виде) децентрализацию хранения. Файл состояния также хранится в хранилище S3 с версионированием и включением блокировки на время сессии. Выполнение перечисленных действий обеспечит необходимый минимум для воссоздания инфраструктуры в минимальные сроки при возникновении инцидента с полной потерей окружения. Конечно же, это всего лишь часть мер, которые следует принять для обеспечения отказоустойчивости проекта. В частности, не стоит забывать про динамические данные, их восстановление, а также возможность миграции между различными провайдерами. Существует достаточно много способов выполнить импорт конфигурации ресурсов того или иного облачного провайдера. Как показала практика, без кастомизации всё равно не обходится, и способ решения приходится подгонять под конкретные инструменты. Тем не менее, утилита Terraformer помогла нам выполнить поставленную задачу. Конечно, понадобилось время на исследования, реализацию и отладку, но, когда процесс был отработан, мы импортировали конфигурации ресурсов нескольких кластеров в установленные сроки и без каких-либо проблем со стороны инфраструктуры. При планировании подобных работ необходимо изначально проанализировать (а может быть, даже проверить пару примеров на практике) целесообразность подобного импорта с точки зрения трудоемкости. Вероятно, для небольшого количества ресурсов будет проще и быстрее написать конфиги Terraform с нуля, либо импортировать по одному штатными средствами, а после адаптировать их. Но когда предстоит работа с большим количеством однородных ресурсов, автоматизация помогает реализовать, казалось бы, невыполнимую задачу. Читайте также в нашем блоге: «Мониторим основные сервисы в AWS с Prometheus и exporter’ами для CloudWatch»; «Как мы сэкономили 2000 USD на трафике из Amazon S3 с помощью nginx-кэша».