GitOps уже давно перестал быть модным словом и превратился в рабочий стандарт: инфраструктура и конфигурации описываются в Git, любые изменения проходят через pull request, а развёртывание делают роботы, а не люди по SSH. Даже если у вас не Kubernetes, а обычный стек на VDS и часть на виртуальном хостинге, связка Git + Terraform + Ansible + CI/CD отлично работает и сильно уменьшает количество неожиданностей на проде.
В этой статье разберём, как собрать практически применимый GitOps-конвейер для инфраструктуры на VDS:
- какую роль играют Terraform, Ansible и CI/CD и где границы ответственности;
- как разложить всё по репозиториям и каталогам;
- как реализовать промоут окружений (dev → stage → prod);
- как хранить и прокатывать секреты;
- как организовать откаты и контроль изменений.
Что такое GitOps в контексте Terraform + Ansible + CI/CD
Классический GitOps — это когда текущее состояние инфраструктуры и сервисов полностью описано в Git. Любое изменение делается через коммит и проходит одинаковый путь:
- Разработчик или админ делает изменение в репозитории.
- Создаётся pull/merge request, запускаются проверки, план изменений и тесты.
- После ревью мёрж в основную ветку триггерит автоматическое применение изменений.
Ключевая идея GitOps: «истина об инфраструктуре» живёт в Git, а не в голове админа и не только в облаке. Ручные правки в консолях и по SSH — враги воспроизводимости.
В реальном мире для GitOps по инфраструктуре обычно используют три основных компонента:
- Terraform — управляет ресурсами: VDS, сети, балансировщики, базы, DNS, объектное хранилище и т.д.
- Ansible — настраивает систему и ПО внутри созданных серверов: пакеты, конфиги, юниты systemd, деплой приложений.
- CI/CD — связывает всё вместе: запускает
terraform plan/apply,ansible-playbook, тесты, проверки форматирования, lint.
В итоге мы получаем цепочку:
Git → CI (plan, тесты) → review → CI (apply) → обновлённая инфраструктура
Границы ответственности: Terraform против Ansible
Самая частая проблема начинающих с GitOps — попытка сделать «всё одним инструментом». Важно провести чёткую границу и договориться об ответственности заранее.
Terraform — про ресурсы и внешний контур
Terraform хорош там, где есть явный API и состояние, которое можно описать как набор ресурсов:
- виртуальные серверы (VDS) и их параметры;
- сети, подсети, firewall-правила, балансировщики;
- облачные базы данных и кластеры;
- объектное хранилище, bucket'ы, права доступа;
- DNS-записи, сертификаты, прочие окружные сервисы.
Terraform должен отвечать на вопросы:
- Сколько у нас серверов и с какими характеристиками?
- Какие у них IP, какие firewall-правила и маршруты?
- Какие DNS-записи указывают на наши сервисы?
Ansible — про конфигурацию и софт внутри VDS
Ansible отвечает за то, что происходит внутри машины после её появления:
- установка пакетов и зависимостей (nginx, php-fpm, PostgreSQL, Redis и т.п.);
- раскладка конфигураций по шаблонам Jinja2;
- создание пользователей, прав, настроек SSH;
- деплой приложений, миграции БД, перезапуски сервисов.
Хороший критерий: если это можно выполнить по SSH и это меняет систему изнутри — почти всегда это задача Ansible, а не Terraform.
CI/CD — клей между ними
CI/CD-платформа (GitHub Actions, GitLab CI, Jenkins, Gitea Actions и т.п.):
- запускает
terraform fmt/validate/planпри каждом PR; - запускает
terraform applyтолько после мёржа и с контролем окружения; - запускает
ansible-lint, тесты ролей, dry-run (--check); - по тэгу или мёржу в ветку окружения разворачивает Ansible на нужных VDS.
Такой конвейер снижает соблазн «быстренько накатить по SSH» и делает изменения предсказуемыми.

Структура репозиториев: mono-repo или несколько?
Есть два основных подхода к организации Git-репозиториев для GitOps с Terraform и Ansible. Выбор сильно влияет на скорость работы команды и сложность сопровождения.
Подход 1: mono-repo для инфраструктуры
Всё, что относится к инфраструктуре и конфигурации, лежит в одном репозитории. Возможная структура:
infra/
terraform/
envs/
dev/
main.tf
variables.tf
stage/
prod/
modules/
vds_web/
vds_db/
ansible/
inventories/
dev/
hosts.ini
group_vars/
stage/
prod/
roles/
base/
nginx/
app/
playbooks/
site.yml
app-deploy.yml
ci/
github-actions/
gitlab-ci/
Плюсы:
- всё в одном месте, проще искать и согласовывать изменения;
- жёсткая связь между ресурсами (Terraform) и конфигурацией (Ansible) через один PR;
- единые практики, линтеры и шаблоны пайплайнов.
Минусы:
- репозиторий растёт; нужен порядок в директориях и правила по веткам;
- командам приложений иногда неудобно, если они не работают с инфраструктурой напрямую.
Подход 2: отдельный repo для Terraform и отдельный для Ansible
Второй вариант — разделить репозитории:
infra-terraform— только Terraform-код для ресурсов;infra-ansible— роли, плейбуки, инвентори.
Плюсы:
- чистое разделение обязанностей между командами;
- разные циклы изменений: Terraform трогают реже, Ansible — чаще;
- меньше конфликтов в Git при параллельной работе.
Минусы:
- сложнее увязать версии инфраструктуры и конфигураций;
- нужно продумывать, как передавать адреса/хосты из Terraform в Ansible (artefact, inventory generator, объектное хранилище и т.п.).
Для небольших и средних проектов на VDS часто удобнее mono-repo: проще стартануть и навести порядок. Для больших команд можно начинать с mono-repo, а потом, при росте, выделять отдельные части.
Организация окружений: dev, stage, prod
GitOps без чёткого разделения окружений быстро превращается в хаос. Важно, чтобы было понятно, какой код на каком окружении крутится и как изменения «едут» по цепочке dev → stage → prod.
Окружения по каталогам
Один из самых понятных вариантов — каталоги по окружениям. В Terraform:
terraform/envs/dev/
terraform/envs/stage/
terraform/envs/prod/
Внутри у каждого окружения свой набор *.tf-файлов и свой backend для состояния. Аналогично в Ansible — отдельные инвентори и переменные:
ansible/inventories/dev/
ansible/inventories/stage/
ansible/inventories/prod/
CI/CD читает путь или параметр окружения и применяет только нужную часть. Можно завязать запуск на теги, label'ы в PR или manual job для прод-окружения.
Окружения по веткам
Другой вариант — каждая ветка Git отражает окружение:
main— prod;stage— stage;dev— dev.
Тогда:
- изменения сначала попадают в
dev, там тестируются; - после проверки делается merge
dev → stage, затемstage → main; - каждая ветка имеет свой backend для
terraform.tfstateи свои переменные для Ansible.
Так проще мыслить «промоутом» изменений, но сложнее с PR между ветками, и merge-конфликты возникают чаще. На практике часто комбинируют подходы: каталоги для окружений внутри репозитория и правило, что в main живёт только prod-конфиг, а для экспериментальных фич создаются временные ветки.
CI/CD-пайплайн для Terraform: plan и apply с контролем
CI/CD-пайплайн для Terraform обычно делят на два отдельных этапа: план и применение. Это даёт прозрачность для ревью и уменьшает риск «неожиданного» apply на проде.
Этап plan при каждом PR
Типичная логика:
- при открытии или обновлении PR запускается job Terraform;
- выполняется
terraform fmt -checkиterraform validate; - для целевого окружения (dev/stage/prod) делается
terraform plan; - результат (
plan) публикуется в комментарии к PR или в артефактах.
Важно: состояние Terraform (terraform.tfstate) хранить не в Git, а во внешнем backend (объектное хранилище, специализированный сервис состояния). Это основа для корректной работы GitOps и одновременного доступа из нескольких runner'ов.
Этап apply после мёржа
После мёржа PR в ветку, привязанную к окружению, запускается terraform apply. Здесь есть два важных момента:
- ручное подтверждение для prod — полезно вставить «manual job» или «environment protection», чтобы apply на проде запускался только после явного approve в CI;
- одновременный доступ к состоянию — убедитесь, что backend блокирует параллельные операции (remote backend с lock); не запускайте два
applyсразу.
Неплохая практика — сохранять план как артефакт и использовать именно его на этапе apply. Так вы избежите ситуации, когда между план и применением кто-то успел влить ещё один PR и итоговый diff уже не тот, что смотрел ревьюер.
CI/CD-пайплайн для Ansible: lint, check, deploy
С Ansible подход похож, но есть свои особенности: он не хранит состояние, его задача — привести сервер к описанному виду. Поэтому особенно важно качество плейбуков и ролей.
Проверки при PR
Базовый набор проверок:
ansible-lintдля ролей и плейбуков;- синтаксическая проверка плейбуков:
ansible-playbook playbooks/site.yml --syntax-check; - опционально —
ansible-playbook ... --check --diffпротив тестового инвентори или локального окружения.
Это помогает отловить типовые ошибки (опечатки в переменных, неправильные модули, кривые шаблоны) раньше, чем они попадут на прод.
Деплой на окружения
После мёржа в ветку окружения CI делает отдельный job для Ansible:
- подтягивает инвентори для окружения (например,
ansible/inventories/prod/hosts.ini); - запускает нужные плейбуки (
site.yml,app-deploy.ymlи т.п.); - логирует результат и, при ошибках, даёт быстрый доступ к логам.
Здесь важно следить, чтобы никто не правил конфигурации руками на серверах. Любые внеплановые правки надо оформлять отдельным PR, иначе GitOps-развёртывание их просто затрёт при следующем запуске.
Интеграция Terraform и Ansible: инвентори и переменные
Задача GitOps-пайплайна — сделать так, чтобы Ansible знал о том, какие VDS и ресурсы созданы Terraform, и мог по ним отрабатывать. Обычно это решают через динамический или генерируемый статический инвентори.
Вариант 1: dynamic inventory на основе Terraform state
Можно использовать динамический инвентори, который читает Terraform state (через CLI или API backend'а) и формирует список хостов и групп. Общая схема:
- Terraform создаёт VDS и записывает в state их IP/hostname.
- Динамический инвентори-скрипт или плагин вытаскивает эти данные и строит структуру групп.
- Ansible подключается к этим хостам и применяет роли.
Плюс: меньше ручной синхронизации. Минус: больше «магии» вокруг state, нужно аккуратно работать с правами доступа и версионированием схемы инвентори.
Вариант 2: генерация статического инвентори
Более простой и прозрачный подход:
- Terraform после
applyгенерирует файл инвентори в понятном для Ansible формате (INI или YAML); - этот файл складывается как артефакт CI или в отдельное хранилище (например, в выделенную ветку git с артефактами или в объектное хранилище);
- Ansible-пайплайн использует этот инвентори для запуска плейбуков.
С точки зрения управления изменениями удобно, если генерируемый инвентори не коммитится вручную, а создаётся только в CI: так вы точно знаете, что все хосты в нём соответствуют текущему состоянию Terraform.
Если вы хотите чуть более продвинутую схему шифрования инвентори и конфигов, можно посмотреть в сторону подходов с sops и age, подробнее разбирали это в материале о GitOps и шифровании секретов sops + age.

Секреты в GitOps: как не сжечь всё к чертям
GitOps подразумевает, что конфиги живут в Git, но это не значит, что в репозиторий нужно класть открытые пароли и ключи. Есть несколько действительно рабочих подходов, которые сочетаются с Terraform и Ansible.
Переменные окружения CI/CD
Самый простой уровень — хранить чувствительные данные в зашифрованном хранилище переменных CI/CD:
- доступ к переменным строго ограничен окружением (dev/stage/prod);
- Terraform и Ansible подхватывают их как
TF_VAR_*или через extra-vars; - секреты не попадают в логи (важно следить за маскированием в настройках CI).
Минус — сложнее отслеживать изменения секретов и делать ревью: всё живёт «внутри CI», а не в Git-истории.
Зашифрованные файлы в репозитории
Более GitOps-ориентированный подход — хранить секреты в Git, но в зашифрованном виде:
- Ansible Vault для
group_varsиhost_vars; - утилиты вроде sops для шифрования YAML/JSON с ключами;
- файлы с секретами шифруются, а ключ доступа лежит вне Git (в CI/CD, в хранилище ключей и т.п.).
Тогда изменения в секретах также проходят через PR, ревью и историю Git (без раскрытия значений). Удобный паттерн — хранить шифровальные ключи на уровне окружений и давать к ним доступ только тем пайплайнам, которые разворачивают соответствующее окружение.
Откаты в GitOps: Terraform и Ansible
Одно из важных преимуществ GitOps — понятный откат к предыдущему состоянию. Никаких ручных восстановлений «как было неделю назад» — всё через revert и повторный запуск пайплайна.
Откат Terraform
С Terraform откат — это в первую очередь «отмотать конфиг к предыдущему коммиту»:
- находите коммит, где инфраструктура была в рабочем состоянии;
- делаете revert или отдельный PR, который приводит код к тому состоянию;
- CI снова делает
terraform plan/applyи приводит ресурсы к этому описанию.
Важно понимать, что не все изменения обратимы без потерь (например, удалённый диск с данными Terraform не вернёт). Поэтому для критичных вещей нужен отдельный план бэкапов, снапшотов и восстановления данных.
Откат Ansible
С Ansible сложнее: он не хранит состояние, а просто приводит систему к описанному в плейбуках виду. Стратегия отката:
- описать «старое» состояние в виде кода (ролей, шаблонов) и иметь его в истории Git;
- сделать revert коммита, который сломал конфигурацию;
- запустить плейбук снова — Ansible приведёт систему к предыдущему стабильному состоянию.
Для приложений дополнительно используют стратегии blue/green и canary-деплои, но это уже тема отдельного разговора. С точки зрения инфраструктуры на VDS гораздо важнее, чтобы все изменения конфигурации действительно проходили через Git и Ansible, а не через ручные правки.
Практические советы по внедрению GitOps на Terraform + Ansible
Чтобы GitOps-подход не остался на бумаге, полезно соблюдать несколько простых, но жёстких правил. С ними связка Terraform + Ansible начинает реально спасать от ночных приключений на проде.
1. Никаких ручных правок мимо Git
Любое изменение инфраструктуры или конфигурации должно жить в Git. Если нужно «быстро пофиксить конфиг на проде», делаем это:
- правим код в репозитории;
- открываем PR, запускаем проверки;
- мержим и даём CI всё развернуть.
В исключительных случаях (аварийные вмешательства) нужно как можно быстрее описать ручные изменения кодом и зафиксировать в Git, чтобы не получить «дрейф конфигурации» между серверами.
2. Один источник истины на ресурс
То, что описано в Terraform, не должно параллельно конфигурироваться Ansible. Например, не стоит одновременно управлять пользователями или сетевыми правилами и там, и там. Чётко делите зоны ответственности, иначе получите состояние, которое невозможно воспроизвести.
3. Маленькие, частые изменения вместо гигантских PR
Чем меньше объём изменений, тем проще их ревьюить и откатывать. Terraform и Ansible отлично поддерживают инкрементальную эволюцию: добавили один VDS — один PR, внедрили новую роль — отдельный PR, обновили версию PostgreSQL — отдельный PR плюс миграции.
4. Автоматические проверки как «ворота»
Не пускайте изменения в основную ветку, если не прошли:
terraform fmt/validateиterraform planбез ошибок;ansible-lintи--syntax-checkпо ключевым плейбукам;- базовые unit-/интеграционные тесты приложений, если они связаны с конфигурацией.
CI/CD должен быть не формальностью, а реальным фильтром проблем. В долгую это экономит часы отладки и значительно снижает риск поломать прод разовым неудачным коммитом.
Итоги
GitOps — это не только про Kubernetes. Для классической инфраструктуры на VDS сочетание Terraform + Ansible + CI/CD даёт те же ключевые преимущества:
- прозрачная история изменений инфраструктуры и конфигураций в Git;
- повторяемые деплои и минимизация ручных действий на серверах;
- быстрый откат за счёт revert'ов и повторного запуска пайплайнов;
- чёткое разделение зон ответственности между Terraform (ресурсы) и Ansible (конфигурации внутри).
Начать можно с малого: вынести инфраструктуру VDS в Terraform, обернуть текущие плейбуки Ansible в простой CI-пайплайн и договориться в команде, что всё теперь проходит через PR. Дальше добавятся динамический инвентори, автоматизированное управление секретами, промоут окружений и другие «приятные сложности», но базовый GitOps-подход уже начнёт работать на вас и делать инфраструктуру предсказуемой.
Дополнительно инфраструктурные DNS-записи и записи A/CNAME для сервисов имеет смысл тоже втащить в Terraform — это упрощает жизнь при работе с доменами и SSL-сертификаты. Подробно про такой сценарий есть в статье о GitOps и управлении DNS через Terraform.


