
Статья не является пошаговой инструкцией по разворачиванию Atlantis. Хотелось показать неочевидные места эксплуатации и варианты их решения.
Всем привет и добро пожаловать!
Всё началось с желания организовать и автоматизировать работу с Terraform, впоследствии с Terragrunt, выстроить управляемый процесс, централизовать, настроить понятный RBAC, в общем, сделать так, «чтобы было красиво». Было сделано несколько подходов, и были рассмотрены разные варианты. Отправной точкой стало структурирование кода, его декомпозиция и рефакторинг, и постепенно дошло до автоматизации самого процесса применения (или просто plan/apply). К этому моменту основная часть кода сконцентрировалась в монорепозитории в self-hosted GitLab, общая и повторяемая логика отделилась во внутренние приватные модули, а сам код вырос в объёмах: более 4 000 .tf файлов, не учитывая внешних и внутренних модулей, более 900 проектов (каталогов), более 30 000 ресурсов, более 30 уникальных провайдеров Terraform. Конечно, все эти условия в той или иной степени повлияли на конечный выбор.
В этой статье расскажу, как мы перешли от ручного управления Terraform и Terragrunt к автоматизированному процессу на базе Atlantis, какие альтернативы рассматривали, почему выбрали именно Atlantis и с какими неочевидными особенностями столкнулись при его эксплуатации.
Atlantis не единственное решение для автоматизации Terraform и Terragrunt, существует несколько альтернативных вариантов. Приведу пример некоторых из них:
-
Terrateam. Модное современное решение, но на практике в нём оказалось много подводных камней. У них есть хорошая статья — Atlantis Alternative: Why teams are moving to Terrateam OSS, где подсвечиваются проблемы Atlantis, и то, как Terrateam их «хорошо» решает. В целом можно согласиться, потому что Atlantis не идеальное решение, вокруг него приходится выстраивать инфраструктуру собственных «велосипедов». Некоторые запросы на фичи висят более трех лет, например, Drift Detection, и вот только недавно в версии v0.45.0 был релиз Alpha Drift Detection API. Не всё так прекрасно и у Terrateam, по факту большинство перечисленного для self-hosted в статье функционала платное (например, RBAC), все процессы и инструкции ориентированы в основном на GitHub, реализации и настройки для GitLab приходится искать или изучать исходники, некоторый функционал не работает в GitLab, например, workflow step OIDC (очень нужный и полезный функционал с assume role). Веб-интерфейс Terrateam оказался очень «глючным» и «тормозным», и это не проблема оптимизации настроек или выделяемых ресурсов под сервис: сами разработчики признают, что проблемы есть. Но надо отдать должное, продукт развивается, у них классное сообщество, разработчики стараются оперативно решать проблемы. Чисто визуально и концептуально продукт понравился.
-
OpenTaco (ex Digger). По описанию и статьям хороший инструмент с богатым функционалом. К сожалению, потестировать его так и не получилось. Сразу отпал, так как поддержка GitLab в качестве CI-бэкенда — это только EE-фича (GitLab feature). При этом в официальной документации про GitLab нет информации, только отдельные упоминания в коде и документации в самом репозитории продукта. Ориентирован в основном на GitHub.
-
HashiCorp Cloud Platform (HCP) Terraform (ex Terraform Cloud). Инструмент хороший, но бесплатный тариф ограничен 500 управляемыми ресурсами, а на текущем объёме лицензия выходила платной — по этой причине не рассматривался.
-
Классический GitLab CI/CD. Вполне рабочий вариант, можно реализовать всю логику через обычные пайплайны. Это был запасной вариант.
В итоге выбор пал на Atlantis — полностью open source приложение для автоматизации работы с Terraform и Terragrunt через pull requests и merge requests. Atlantis разворачивается в собственной инфраструктуре, не требует платной подписки и поддерживает GitLab наравне с GitHub. Недостающую функциональность можно реализовать самостоятельно — а достраивать, как выяснилось, придётся много.
Разворачивать Atlantis можно и в Kubernetes, но был выделен отдельный EC2-инстанс, потому что организовать безопасность отдельно стоящего сервера понятнее и проще, более контролируемо и предсказуемо, а выдавать разрешения можно более гранулярно. Можно также вводить ограничения на уровне сети, инстанса и операционной системы.
Что касается UI, то в Atlantis встроен свой веб-сервер, но он довольно минималистичен и ограничен, поэтому в качестве входящего шлюза был установлен reverse proxy на базе nginx. Что это даёт:
-
Терминирование TLS. Atlantis умеет TLS, но управлять сертификатами и современными шифрами удобнее на уровне nginx.
-
Аутентификация для веб-интерфейса. Atlantis поддерживает только basic auth. Через nginx можно использовать что угодно: basic auth, oauth2, ограничение по IP, mTLS. При этом важно не закрывать авторизацией путь /events: туда приходят вебхуки от GitHub/GitLab, они авторизуются своим webhook secret.
-
Разделение доступа по путям. Например, можно выделить путь /events и разрешить доступ только для GitHub/GitLab, а для остальной части веб-интерфейса ограничить доступ только корпоративной сетью. Аналогично можно поступить с метриками и открыть доступ к /metrics только для Prometheus/VictoriaMetrics.
-
Rate limiting. Ограничение частоты запросов.
-
Логирование и наблюдаемость. Логи доступа nginx в едином формате в единой системе логирования со всей остальной инфраструктурой.
С примером конфигурации nginx можно ознакомиться тут.
Впоследствии на доступ к UI была добавлена SSO-авторизация через интеграцию nginx и Authelia.
С входящим трафиком разобрались, но остаётся вопрос: с какими правами Atlantis ходит в AWS и откуда эти права берутся. Так сложилось, что при работе с Terraform активно использовались профили AWS (параметр profile / --profile). Также не было желания и потребности кардинально изменять сам код. При этом необходимо было оставить возможность локального использования совместно с AWS SSO. В результате это заставило решать задачу: как профили и учётные данные будут доставляться в Atlantis, поддерживать эфемерную среду, иметь регулируемый TTL, динамически обновляться, без ручных и подготовительных действий, без хардкода в скриптах. Статические ключи сразу не подошли, даже с учетом автоматической ротации. Выбор пал на роли AWS с ограниченными правами и принятием ролей (sts:AssumeRole). Принятие ролей (sts:AssumeRole), генерация учётных данных, политики доступа и TTL были реализованы с помощью AWS Secret Engine в HashiCorp Vault.
С примером конфигурации Vault можно ознакомиться тут.
Доставка профилей и учётных данных в Atlantis была реализована с помощью HashiCorp Vault Agent и его шаблонов.
С примером конфигурации агента можно ознакомиться тут, с примером шаблона агента тут.
Шаблон агента не поддерживает переменные окружения. Чтобы передать их в конфигурацию агента можно выполнить такую команду. А запустить агента можно так.
Дальше будут встречаться три разные конфигурации, и их стоит сразу разделить: Server Configuration — флаги запуска сервера Atlantis; Server Side Repo Config — серверный файл для управления параметрами, которые невозможно корректно выразить с помощью флагов, а также тем, что пользователи могут делать на уровне репозитория; repo-level atlantis.yaml — файл внутри репозитория, описывающий список проектов и их настройки. Генератор, о котором ниже, производит именно третий.
Следующим шагом стало написание генератора конфигурации atlantis.yaml. Было несколько причин, из-за которых пришлось написать собственный генератор:
-
Монорепа с более чем 900 проектами (каталогами). Нужен был гранулированный, управляемый и в то же время динамический подход.
-
Внедрение требовало поэтапного переключения проектов, растянутого по времени. Atlantis не поддерживает wildcard- или glob-паттерны в конфигурации проектов в репозитории (в отличие от того же Terrateam). Формировать конфигурацию на 900 и более проектов, которые к тому же постоянно развиваются, меняются и добавляются, в ручном режиме — это путь боли и страдания.
-
В монорепе используется и Terraform, и Terragrunt. Возникла необходимость в custom workflows, отдельные процессы для каждого из них. Хотелось не управлять этим в ручном режиме, а автоматически определять тип проекта и выставлять под проект конкретный workflow, без участия пользователя. Всё это позволило сделать процесс миграции на Terragrunt незаметным с точки зрения CI.
-
Так сложилось, что для локального выполнения используется tfenv и его файлы .terraform-version с версией Terraform. Это позволяет легко и удобно управлять версиями на компьютере пользователя. Но Atlantis не поддерживает tfenv и не определяет версию на основе файлов .terraform-version, и не планирует — запрос на функционал в issue 629 закрыт (там, собственно, объясняется почему: мейнтейнер счёл tfenv избыточным при наличии встроенного управления версиями). Atlantis определяет версию по одному из источников: required_version в блоке конфигурации Terraform, terraform_version в конфигурации проекта в atlantis.yaml, параметра default-tf-version конфигурации самого Atlantis. В итоге в генератор конфигурации был добавлен функционал чтения версии из файла .terraform-version и добавления соответствующего параметра проекта с версией в atlantis.yaml. Для всего остального в конфигурации был установлен default-tf-version.
-
Для Terragrunt существуют готовые генераторы конфигурации, например, Terragrunt Atlantis Config, а для чистого Terraform живого аналога нет: то, что находится, не развивается уже пару лет и под задачи не подошло. Тем более нет ничего для их комбинации.
С кодом генератора конфигурации можно ознакомиться тут.
Генератор конфигурации добавляется в pre_workflow_hooks, как и рекомендовано в инструкции Dynamic Repo Config Generation.
Другим важным шагом стало внедрение RBAC, так как в Atlantis он фактически отсутствует, для GitLab это в лучшем случае gitlab-group-allowlist или team_authz. Была нужна гибкая модель на plan и apply под разные проекты (каталоги), с поддержкой различных условий, групп, приоритетов. Выбор пал на Open Policy Agent (OPA) и Rego Policy. В Atlantis есть поддержка Rego Policy, но они не дают нужной гибкости и управляемости, да они и про другое. Поэтому был выбран нативный клиент и установлен вместе с Atlantis.
С примером кода установки можно ознакомиться тут.
А для создания и отладки Rego Policy есть хороший playground.
Была сформирована простая политика со своей логикой правил по проектам (каталогам), группам, приоритетам, количеству одобрений (approvals), кому, куда и при каких условиях можно применять код. Состав групп берётся не из статического файла, отдельный шаг в pre_workflow_hooks ходит в GitLab API за участниками групп и сохраняет их рядом с политикой, чтобы решение принималось по актуальному составу. Правило по умолчанию deny. Далее, в зависимости от выполнения условий, применяющему будет разрешено (allow) или не разрешено (deny) выполнить запрос (plan/apply). В комментарий к merge request при allow выводится информация о совпадающем правиле, при deny — о причине отказа.
С примером скрипта, который формирует вводные данные для политики и последующий вывод отчёта, можно ознакомиться тут, а с примером Rego Policy тут.
В ходе эксплуатации Atlantis также были выявлены некоторые особенности работы и поведения.
Первой особенностью стало поведение автообнаружения проектов. В конфигурации сервера стояло autodiscover-mode: auto, и это воспринималось как «искать проекты, только если отсутствует atlantis.yaml». По факту проверка смотрит не на наличие файла, а на длину списка projects в нём: в режиме auto автообнаружение включается каждый раз, когда список пуст, и отсутствующий файл неотличим от файла с пустым списком. Поэтому прогон, в котором генератор не отдал ни одного проекта — фильтры генератора ничего не отобрали или упал скрипт в pre_workflow_hooks, — заканчивается тем, что Atlantis планирует все затронутые в merge request каталоги. Помогло включение параметра fail-on-pre-workflow-hook-error, чтобы упавший генератор ронял merge request, а не оставлял Atlantis без списка проектов.
Это подтвердил разбор механики автоплана:
-
Со стороны GitLab приходит уведомление о пуше. Atlantis не обрабатывает пуш хук —
server/controllers/events/events_controller.go, обрабатываются толькоgitlab.MergeCommentEventиgitlab.MergeEvent. -
RunPreHooks(server/events/pre_workflow_hooks_command_runner.go) клонирует репозиторий (WorkingDir.Clone) и запускает хук сcmd.Dir = repoDir(server/core/runtime/pre_workflow_hook_runner.go). Скрипт кладёт atlantis.yaml в корень клона. -
buildAllCommandsByCfg(server/events/project_command_builder.go) снова вызываетWorkingDir.Clone— тот видит, что репозиторий уже на нужном коммите, и уходит по fast path без повторного клонирования (server/events/working_dir.go). -
parseRepoCfg->HasRepoCfg— обычныйos.Statпо рабочему каталогу (server/core/config/parser_validator.go), untracked-файл виден. -
getMergedProjectCfgs(project_command_builder.go) делает две вещи подряд: еслиlen(repoCfg.Projects) > 0— отбирает проекты по when_modified и затем, еслиp.autoDiscoverModeEnabled(ctx, repoCfg)— добавляет автообнаруженные проекты. -
autoDiscoverModeEnabledрезолвит режим в следующем порядке:autodiscoverизrepos.yaml, затемautodiscoverиз repo-level файла, затем CLI-флаг. -
RepoCfg.AutoDiscoverEnabled(server/core/config/valid/repo_cfg.go):
if autoDiscoverMode == AutoDiscoverAutoMode { // AutoDiscover is enabled by default when no projects are defined return len(r.Projects) == 0}
Ещё одна особенность, с которой пришлось столкнуться, — это race condition в plugin_cache_dir при параллельном запуске проектов. Поэтому пришлось выключить use-tf-plugin-cache — в документации так и рекомендуется.
Terraform очень требовательный во время исполнения к CPU и дисковой подсистеме, поэтому значение parallel-pool-size пришлось определять опытным путём. Начинали со 100 параллельных проектов, пришли к 5.
Atlantis умеет строить индекс зависимостей между проектами и локальными модулями репозитория. Он раскручивает цепочку source = "./..." и планирует каждый проект, который подключает изменённый модуль, в том числе транзитивно, через другие модули. За это отвечает параметр autoplan-modules, по умолчанию false.
Один merge request может включать правки в разные проекты, а применять их разом или по одному не всегда удобно и возможно. Поэтому Atlantis позволяет использовать регулярные выражения при вызове команд plan/apply, например, atlantis apply -p .*runner.* . Это включается отдельно с помощью параметра enable-regexp-cmd.
Atlantis во время работы выводит много разной информации и пишет её в комментарии, если это сложный merge request, то это может значительно «замусорить» комментарии и усложнить работу. С этим помогают бороться два параметра: скрывать предыдущие комментарии к плану — hide-prev-plan-comments и удалять комментарии к плану без изменений hide-unchanged-plan-comments.
Ну и как же без emoji. Можно установить emoji как реакцию, используемую для обозначения обработанных комментариев. Так, например, можно отслеживать, что уведомление из GitLab дошло до Atlantis. За это отвечает параметр emoji-reaction.
Помимо конфигурации сервера Server Configuration, имеется серверная конфигурация репозитория Server Side Repo Config, здесь уже можно управлять конфигурацией на уровне репозитория. Она может быть полезна, когда есть несколько репозиториев под управлением, когда конфигурации для репозиториев отличаются, при этом нужно более централизованно контролировать процесс, что-то разрешать или что-то запрещать на стороне репозитория.
На начальном этапе активно использовались условия approved, mergeable, undiverged в plan_requirements и apply_requirements. Но с активным ростом подключаемых проектов, усложнением логики, увеличением количества эксплуатирующих команд, расширением функционала проведения merge requests, например, внедрением GitLab Code Owners, от них пришлось отказаться: эти условия применяются ко всему репозиторию, а нужны были разные требования для разных каталогов (проектов). Проверку одобрений взяли на себя GitLab (Code Owners и правила merge request) и Open Policy Agent с Rego Policy, а условия «кому и куда можно применять» — Open Policy Agent с Rego Policy и шаги внутри Atlantis workflows.
В целях безопасности на стороне сервера (Server Side Repo Config) репозиторию разрешено задавать в atlantis.yaml только список проектов — тот, что генерируется на лету, — но не собственные workflows: иначе кто угодно смог бы merge request’ом подменить шаги plan/apply.
Работать со сложными merge requests, которые создают/изменяют/удаляют большое количество ресурсов/объектов, разбирать большие планы в окне вывода внутри комментариев оказалось сложно и очень неудобно. Так как в инфраструктуре уже был haste-server (open-source pastebin), то в процессы были добавлены шаги, которые формируют и публикуют вывод плана на сервере, а в комментарии вкладывают ссылки. К сожалению, весь вывод (output) от шага run попадает в fenced code block шаблона и рендерится как plain text. В issue 121 есть открытый запрос на фичу подобного функционала от 2018 года.
У Atlantis есть эндпоинт для публикации метрик в формате Prometheus. Доступ к метрикам был открыт только для Prometheus/VictoriaMetrics средствами reverse proxy nginx. На базе встроенных метрик и стандартных метрик node-exporter был создан дашборд в Grafana для очень модного слова observability. С примером дашборда можно ознакомиться тут.
С примером полной конфигурации сервера Server Configuration можно ознакомиться тут, а тут пример серверной конфигурации репозитория Server Side Repo Config.
Сейчас через Atlantis проходят изменения почти во всех аккаунтах в организации: подключено около 800 каталогов (проектов) из 900. План запускается автоматически на каждый merge request, в процесс встроены проверки и валидации кода.
Закончено далеко не всё. Drift Detection появился только в alpha и в процесс пока не встроен. Подключение оставшихся каталогов (проектов) продолжается до сих пор.
Каждая инфраструктура уникальна, поэтому описанное стоит воспринимать как набор идей: какие-то подойдут как есть, какие-то захочется сделать иначе — и это нормально.
ссылка на оригинал статьи https://habr.com/ru/articles/1073152/