Terraformer — Infrastructure To Code

image
Хотел бы рассказать про новый CLI tool который я написал для решения одной старой проблемы.

Проблема


Terraform уже давно стал стандартом в Devops/Cloud/IT сообществе. Вещь очень удобная и полезная чтоб заниматся infrastructure as code. Есть много прелестей в Terraform, а так же много вилок, острых ножей и граблей.
С Terraform очень удобно делать новые вещи и потом ими управлять, менять или удалять. А что делать тем у кого есть огромная инфраструктура в облаке и не создано через Terraform? Переписывать и пересоздавать все облако как то дорого и небезопасно.
Я сталкивался с такой проблемой на 2 работах, самый простой пример когда хочешь что все было в гите виде терраформ файлов, а у тебя 250+ бакетов и писать их в для терраформа руками как то много.
Есть issue еще с 2014 года в terrafom которую закрыли в 2016 с надеждой что будет import.

Вообщем все как на картинке только с право на лево

Предупреждения: Автор пол жизни живет не в России и пишет на русском мало. Осторожно ошибки в орфографии.

Решения


1. Есть готовое и старое решения для AWS terraforming. Когда я пытался через него получить мои 250+ бакетов то понял что там все плохо. AWS уже давно накидали много новых опций, а terraforming про них не знает и вообще у него ruby темплет выглядят скудно. После 2 вечером я послал Pull request чтоб добавить больше возможностей туда и понял что такое решения не подходит вообще.
Как работает terraforming он берет с SDK AWS данные и генерирует tf и tfstate через темплет.
Тут 3 проблемы:
1. Всегда там будет отставания в обновлениях
2. tf файлы иногда выходят битые
3. tfstate собирается отдельно от tf и не всегда сходится
Вообще сложно получит результат при котором `terraform plan` скажет что не изменений

2. `terraform import` — встроенная команда в terraform. Как работает?
Пишешь пустой TF файл с именем и видом ресурса, потом запускаешь `terraform import` и передаешь ID ресурса. terraform обращается к провайдеру получает данные и делает tfstate файл.
Тут 3 проблемы:
1. Получаем только tfstate файл, а tf пустой надо руками писать или конвертировать с tfstate
2. Умеет работать только с одним ресурсов каждый раз и не поддерживает все ресурсы. И что мне сново делать с 250+ бакетами
3. Надо знать ID ресурсов — то есть надо обматывать его это в код который достает список ресурсов
Вообще результат частичный и не масштабируется хорошо

Мое решения

Требования:
1. Возможность создать файлы tf и tfstate по ресурсам. Например скачать все бакеты/security group/load balancer и что `terraform plan` возвращал что нет изменений
2. Надо 2 облака GCP + AWS
3. Глобальное решения которое легко обновлять каждый раз и не тратить время на каждый ресурс по 3 дня работы
4. Сделать Open source — проблема у всех такая

Язык Go — потому я люблю, и на нем есть библиотека для создания HCL файлов которая используется в terraform + много кода в terraform который может быть полезный

Путь


Попытка первая
Начал простой вариант. Обращения в облако через SDK за нужным ресурсом и конвертирования его в поля для terraform. Попытка умерла сразу на security group потому что мне не понравилось 1.5 дня конвертировать только security group (а ресурсов много). Долго и потом поля могут менять/добавлятся

Попытка вторая
Основано на идеи описанной тут. Просто взять и сконвертировать tfstate в tf. Все данные там есть и поля те же. Как получить полный tfstate для много ресурсов? Тут на помощь пришла команда `terraform refresh`. terraform берет все ресурсы в tfstate и по ID вытаскивает по ним данные и пишет все в tfstate. То есть создать пустой tfstate только с именами и ID, запустить `terraform refresh` то получаем полные tfstate. Ура!
Теперь займемся рекурсивной порнографием написание конвертора для tfstate в tf. Для тех кто никогда не читал tfstate то это JSON, но особенный.
Вот его важная часть attributes

 "attributes": {
                            "id": "default/backend-logging-load-deployment",
                            "metadata.#": "1",
                            "metadata.0.annotations.%": "0",
                            "metadata.0.generate_name": "",
                            "metadata.0.generation": "24",
                            "metadata.0.labels.%": "1",
                            "metadata.0.labels.app": "backend-logging",
                            "metadata.0.name": "backend-logging-load-deployment",
                            "metadata.0.namespace": "default",
                            "metadata.0.resource_version": "109317427",
                            "metadata.0.self_link": "/apis/apps/v1/namespaces/default/deployments/backend-logging-load-deployment",
                            "metadata.0.uid": "300ecda1-4138-11e9-9d5d-42010a8400b5",
                            "spec.#": "1",
                            "spec.0.min_ready_seconds": "0",
                            "spec.0.paused": "false",
                            "spec.0.progress_deadline_seconds": "600",
                            "spec.0.replicas": "1",
                            "spec.0.revision_history_limit": "10",
                            "spec.0.selector.#": "1",


Тут есть:
1. id — string
2. metadata — array размером 1 и в нем объект с полями который описан ниже
3. spec — hash размером 1 и в нем key, value
Короче веселый формат, все может быть в глубь тоже на несколько уровней

                   "spec.#": "1",
                            "spec.0.min_ready_seconds": "0",
                            "spec.0.paused": "false",
                            "spec.0.progress_deadline_seconds": "600",
                            "spec.0.replicas": "1",
                            "spec.0.revision_history_limit": "10",
                            "spec.0.selector.#": "1",
                            "spec.0.selector.0.match_expressions.#": "0",
                            "spec.0.selector.0.match_labels.%": "1",
                            "spec.0.selector.0.match_labels.app": "backend-logging-load",
                            "spec.0.strategy.#": "0",
                            "spec.0.template.#": "1",
                            "spec.0.template.0.metadata.#": "1",
                            "spec.0.template.0.metadata.0.annotations.%": "0",
                            "spec.0.template.0.metadata.0.generate_name": "",
                            "spec.0.template.0.metadata.0.generation": "0",
                            "spec.0.template.0.metadata.0.labels.%": "1",
                            "spec.0.template.0.metadata.0.labels.app": "backend-logging-load",
                            "spec.0.template.0.metadata.0.name": "",
                            "spec.0.template.0.metadata.0.namespace": "",
                            "spec.0.template.0.metadata.0.resource_version": "",
                            "spec.0.template.0.metadata.0.self_link": "",
                            "spec.0.template.0.metadata.0.uid": "",
                            "spec.0.template.0.spec.#": "1",
                            "spec.0.template.0.spec.0.active_deadline_seconds": "0",
                            "spec.0.template.0.spec.0.container.#": "1",
                            "spec.0.template.0.spec.0.container.0.args.#": "3",


Вообщем кто хочет задачку на программирования для собеседования то просто попросите написать парсер на это дело :)
После долгих попыток написать парсер без багов я нашел часть его в коде terraform причем самую важную часть. И все вроде работало норм

Попытка три
terraform provider — это бинарки в которых есть код со всеми ресурсами и логикой для работы с API облаков. У каждого облака есть свой provider и сам terraform только вызывает их через свой протокол RPC между двумя процессами.
Теперь я решил обращаться напрямую к terraform providers через RPC вызовы. Так вышло красиво и дало возможность менять terraform providers на более новые и получать новые возможность не меняя код. Еще оказалось не все поля в tfstate должны быть в tf, а как это узнать? Только спросить provider об этом. Потом началась еще одна рекурсивной порнографием по сборки регулярных выражений канитель с поиском полей внутри tfstate на всех уровнях в глубь.

В конце получилось полезный CLI tool у которого общая инфраструктура для всех terraform providers и можно легко добавить нового. Также добавления ресурсов занимает мало кода. Плюс всякие плюшки типа соединения между ресурсами. Конечно было много разных проблем которые все не описать.
Назвал зверушку Terrafomer.

Финал


Мы с помощью Terrafomer сгенерировали 500–700 тысяч строк кода tf + tfstate c двух облаках. Смогли взять легаси вещи и начать их трогать только через terraform как в лучших идеях infrastructure as code. Просто магия когда берешь огромное облако и получаешь через команду его в виде terraform файлов рабочих. А дальше grep/replace/git и так далее.

Вычесал и привел в порядок, получил разрешения. Выпустил на гитхаб для всех в четверг (02.05.19). github.com/GoogleCloudPlatform/terraformer
Получил уже 600 звезд, 2 pull requests добавления поддержки openstack и kubernetes. Хорошие отзывы. Вообще проект полезный для людей
Советую всем кто хочет начать работать с Terraform и не переписывать все для этого.
Буду рад pull requests, issues, stars.

Демо
243961.svg

© Habrahabr.ru