Автоматизация развёртывания с Ansible: плейбуки вместо bash-скриптов в 2026 году | AdminWiki

Автоматизация развёртывания с Ansible: плейбуки вместо bash-скриптов в 2026 году

24 сентября 2026 16 мин. чтения
Содержание статьи

Bash-скрипт развёртывания перестаёт масштабироваться, когда его запускают повторно на той же машине или раскатывают на 6 и больше серверов: конфиги дублируются, ошибки ловятся вручную, а ревью трёх сотен строк shell никто не делает. Ansible снимает эти три боли: модули проверяют текущее состояние, YAML описывает желаемый результат, роли переиспользуются между окружениями.

Дальше разбираем переход от shell к плейбукам: чек-лист триггеров для миграции, структуру проекта с инвентарём, group_vars и ролями, готовый плейбук для веб-сервера и прокси, пошаговый алгоритм перевода существующего скрипта и защиту прода через check mode, бэкапы и лимиты. Примеры рассчитаны на Ansible 2.16+ и Ubuntu 22.04/24.04; для RHEL-семейства модуль apt меняется на dnf, а шаблоны конфигов и структура каталогов остаются те же.

Короткий ответ: bash оставляйте для одноразовой настройки одной машины, Ansible берите для повторяемых развёртываний и управления конфигурацией, Terraform подключайте, когда появляются облачные ресурсы: виртуальные машины, сети, балансировщики, DNS.

Когда bash-скрипт развёртывания перестаёт масштабироваться

Порог перехода обычно наступает на 5 серверах и втором окружении. До этого скрипт на 40 строк дешевле плейбука: его пишут за десять минут и не задумываются о структуре. После, когда деплой нужен и на stage, и на prod, а правки вносят два инженера, shell превращается в источник инцидентов.

Триггеры для перехода на Ansible

  1. Больше 5 серверов. На 10 машинах разовый обход по SSH занимает минуты, а сверка версий пакетов и конфигов превращается в ручной аудит. Инвентарь Ansible даёт адресную выборку: ansible web -m ping.
  2. Два и больше окружения. dev, stage и prod расходятся в переменных: домен, порты, пути к секретам. В bash это отдельные копии скрипта, в Ansible это group_vars/stage.yml и group_vars/prod.yml.
  3. Требование идемпотентности. Если развёртывание запускается из CI после каждого коммита или по расписанию, повторный прогон обязан завершаться без изменений.
  4. Нужен откат. Перед правкой конфига требуется бэкап и понятный план возврата. Модуль copy с backup: true сохраняет предыдущую версию файла рядом с оригиналом.
  5. Командная работа и аудит. YAML проходит код-ревью, git хранит историю, а запуск пишет PLAY RECAP с числом изменённых задач по каждому хосту.

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

Чем опасна неидемпотентность bash-скриптов

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

#!/usr/bin/env bash
set -euo pipefail

echo "proxy_read_timeout 60s;" >> /etc/nginx/nginx.conf
cp ./site.conf /etc/nginx/conf.d/site.conf
systemctl restart nginx

Здесь два разных риска. Дописывание строки накапливает мусор в конфиге, а копирование затирает ручные правки, которые кто-то внёс на сервере во время разбора инцидента. Перезапуск в конце выполняется всегда, даже когда ничего не изменилось, и рвёт активные соединения.

Ansible решает каждую часть отдельно: lineinfile с параметром regexp меняет строку на месте и не создаёт дублей, copy с backup: true сохраняет предыдущую версию файла, а перезапуск уезжает в handler и срабатывает только при реальном изменении:

- name: Задать proxy_read_timeout
  ansible.builtin.lineinfile:
    path: /etc/nginx/nginx.conf
    regexp: '^proxy_read_timeout'
    line: 'proxy_read_timeout 60s;'
  notify: reload nginx

Для конфигов с переменными используйте template вместо cat и heredoc: Jinja2 подставляет значения из инвентаря, а результат сравнивается с текущим файлом по контрольной сумме.

Что такое идемпотентный плейбук Ansible и как он работает

Идемпотентность означает: повторный запуск даёт тот же результат и ничего не ломает. Ansible достигает этого не проверками в коде, а модулями: каждый модуль сначала читает текущее состояние системы и меняет его, только если оно отличается от описанного.

Модуль apt с name: nginx сверяет список установленных пакетов через dpkg и пропускает установку, если nginx уже стоит. Модуль service с state: started проверяет systemctl is-active и ничего не делает, когда сервис запущен. В отчёте такие задачи помечаются ok, изменённые задачи - changed, а changed=0 на втором прогоне служит главным ориентиром корректного плейбука.

Модули Ansible вместо shell-команд

Что делает bashМодуль AnsibleПочему так лучше
apt-get install -y nginxansible.builtin.apt (dnf для RHEL)ставит пакет только при отсутствии, кэш обновляется по cache_valid_time
cp site.conf /etc/nginx/conf.d/ansible.builtin.copyкопирует при расхождении, backup: true сохраняет старую версию
cat > vhost.conf со heredocansible.builtin.templateподставляет переменные Jinja2 и сравнивает результат с текущим файлом
sed -i 's/old/new/' nginx.confansible.builtin.lineinfile, ansible.builtin.replaceменяет строку по regexp и не дублирует её при повторном запуске
systemctl restart nginxansible.builtin.serviceперезапускает только при changed через handler
useradd -m deployansible.builtin.userсоздаёт пользователя, повторный запуск не вносит изменений
curl -fsSL url -o /tmp/fileansible.builtin.get_urlскачивает при отсутствии файла или смене checksum
tar -xzf archive.tar.gz -C /optansible.builtin.unarchiveраспаковывает, если файлов ещё нет

Модули shell и command не отслеживают состояние и всегда возвращают changed. Их применяют точечно: запуск собственного скрипта, работа с CLI без готового модуля. Тогда добавляйте creates или removes, чтобы задача не выполнялась при наличии результата, и changed_when: false, если команда ничего не меняет.

Как проверить идемпотентность плейбука

Алгоритм из трёх шагов занимает пару минут и снимает большую часть рисков.

# 1. Прогноз без изменений: покажет, что плейбук собирался бы сделать
ansible-playbook -i inventory/hosts.ini site.yml --check --diff

# 2. Первый реальный прогон
ansible-playbook -i inventory/hosts.ini site.yml

# 3. Второй прогон: ожидаем changed=0
ansible-playbook -i inventory/hosts.ini site.yml

PLAY RECAP после второго прогона выглядит так:

PLAY RECAP *******************************************
web01  : ok=6  changed=0  unreachable=0  failed=0  skipped=1  rescued=0  ignored=0
web02  : ok=6  changed=0  unreachable=0  failed=0  skipped=1  rescued=0  ignored=0

Если changed больше нуля, ищите задачу, которая каждый раз перезаписывает файл или дёргает сервис. Частые причины: shell вместо модуля, отсутствие regexp в lineinfile, шаблон с временной меткой, которая меняется на каждом прогоне.

У check mode есть ограничение: часть модулей (command и shell без creates, отдельные модули коллекций) в режиме прогноза пропускается или помечается changed, не выполняясь. Для таких задач ставьте check_mode: false осознанно и только там, где прогноз невозможен.

Структура проекта Ansible: инвентарь, переменные, роли

Плоский файл site.yml с сотней задач работает до первого переиспользования. Каталоги ниже дают основу, которая выдерживает рост: инвентарь описывает машины, group_vars и host_vars хранят данные, роли содержат логику.

ansible/
  ansible.cfg
  inventory/
    hosts.ini
  group_vars/
    all.yml
    web.yml
  host_vars/
    proxy01.yml
  roles/
    nginx/
    proxy/
  site.yml

В ansible.cfg фиксируют пути и поведение, чтобы не дублировать их в каждой команде:

[defaults]
inventory = inventory/hosts.ini
roles_path = roles
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
forks = 10

Инвентарь: статический и динамический

Статический INI-файл подходит, когда список машин меняется редко. Группы в нём совпадают с ролями: web, proxy, db.

[web]
web01 ansible_host=10.10.0.11
web02 ansible_host=10.10.0.12

[proxy]
proxy01 ansible_host=10.10.0.21

[web:vars]
ansible_user=deploy

[all:vars]
ansible_python_interpreter=/usr/bin/python3

Динамический инвентарь нужен в облаке: плагин aws_ec2 из коллекции amazon.aws, gcp_compute из google.cloud или плагин для Yandex Cloud строит список хостов по тегам и метаданным. Проверить инвентарь до запуска плейбука помогут команды:

ansible-inventory --graph
ansible-inventory --list
ansible web -m ping

Переменные: где хранить и как использовать

Приоритет от низкого к высокому: defaults роли, group_vars, host_vars, vars в плейбуке, extra vars из командной строки через -e. Практическое правило: значения по умолчанию держите в defaults роли, общие данные окружения в group_vars, исключения для отдельной машины в host_vars.

# group_vars/web.yml
app_port: 8080
nginx_worker_processes: auto

# host_vars/proxy01.yml
nginx_worker_connections: 4096

# site.yml, переопределение внутри плейбука
vars:
  domain: example.internal

Секреты (пароли баз данных, токены API) в открытом виде не храните: ansible-vault encrypt_string шифрует отдельное значение, ansible-vault encrypt шифрует файл целиком. Ключ передавайте через --vault-password-file или переменную окружения в CI, чтобы он не попадал в историю команд.

Роли: переиспользуемые блоки конфигурации

Роль собирает задачи, шаблоны, handlers и значения по умолчанию в один каталог. Скелет создаётся командой ansible-galaxy init roles/nginx:

roles/nginx/
  tasks/main.yml
  handlers/main.yml
  templates/vhost.conf.j2
  defaults/main.yml
  vars/main.yml
  files/
  meta/main.yml

Подключение роли в плейбуке занимает две строки, а внешние роли из Galaxy перечисляют в requirements.yml и ставят командой ansible-galaxy install -r requirements.yml. Так версия роли фиксируется и воспроизводится на любой машине.

- name: Настроить веб-слой
  hosts: web
  become: true
  roles:
    - nginx

Готовый пример: плейбук для развёртывания веб-сервера и прокси

Сценарий: два веб-сервера с приложением на порту 8080 и один прокси-сервер, который распределяет трафик между ними и держит keepalive-соединения. Набор файлов ниже можно скопировать и запустить на стенде.

Плейбук: установка и настройка Nginx

# roles/nginx/tasks/main.yml
- name: Установить nginx
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true
    cache_valid_time: 3600
  notify: reload nginx

- name: Разложить конфиг виртуального хоста
  ansible.builtin.template:
    src: vhost.conf.j2
    dest: "/etc/nginx/conf.d/{{ domain }}.conf"
    owner: root
    group: root
    mode: "0644"
    backup: true
  notify: reload nginx

- name: Включить автозапуск nginx
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true
# roles/nginx/handlers/main.yml
- name: reload nginx
  ansible.builtin.service:
    name: nginx
    state: reloaded
# roles/nginx/templates/vhost.conf.j2
server {
    listen 80;
    server_name {{ domain }};

    location / {
        proxy_pass http://127.0.0.1:{{ app_port }};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Handler reload nginx сработает только при изменении шаблона. Если файлы на месте, Nginx не перезапускается, лишних обрывов соединений в проде не будет.

Плейбук: настройка прокси-сервиса

На прокси-сервере тот же Nginx работает как reverse proxy с пулом бэкендов. Конфиг виртуального хоста отличается одной строкой: proxy_pass http://backend_pool.

# roles/proxy/tasks/main.yml
- name: Установить nginx на прокси
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true
  notify: reload nginx

- name: Разложить upstream с бэкендами
  ansible.builtin.template:
    src: upstream.conf.j2
    dest: /etc/nginx/conf.d/upstream.conf
    mode: "0644"
    backup: true
  notify: reload nginx

- name: Проверить синтаксис конфигурации
  ansible.builtin.command:
    cmd: nginx -t
  changed_when: false
  register: nginx_test

- name: Показать результат проверки
  ansible.builtin.debug:
    var: nginx_test.stderr_lines
# roles/proxy/templates/upstream.conf.j2
upstream backend_pool {
{% for host in backend_hosts %}
    server {{ host }} max_fails=3 fail_timeout=10s;
{% endfor %}
    keepalive 32;
}

Список бэкендов берётся из переменных, поэтому добавление третьего веб-сервера сводится к правке group_vars/web.yml и повторному запуску. Параметры max_fails и fail_timeout выводят упавший бэкенд из ротации после трёх ошибок, keepalive переиспользует соединения и снижает накладные расходы на TCP.

# site.yml
- name: Настроить веб-серверы
  hosts: web
  become: true
  roles:
    - nginx

- name: Настроить прокси
  hosts: proxy
  become: true
  roles:
    - proxy
ansible-playbook -i inventory/hosts.ini site.yml

PLAY [Настроить веб-серверы] ***********************
TASK [nginx : Установить nginx] ********************
changed: [web01]
changed: [web02]

PLAY RECAP *****************************************
proxy01 : ok=5  changed=3  unreachable=0  failed=0
web01   : ok=4  changed=2  unreachable=0  failed=0
web02   : ok=4  changed=2  unreachable=0  failed=0

Проверка результата: команды и ожидаемый вывод

КомандаЧто проверяетОжидаемый результат
curl -I http://10.10.0.21/ответ прокси-сервераHTTP/1.1 200 OK, заголовок Server: nginx
curl -s http://10.10.0.11:8080/healthживость приложения на бэкенде200 и тело вида {"status":"ok"}
systemctl status nginx --no-pagerсостояние сервисаactive (running), в логе строки reload
ss -tlnp | grep ':80'слушает ли Nginx портLISTEN на 0.0.0.0:80, процесс nginx
nginx -tсинтаксис конфиговsyntax is ok, test is successful
ansible web -m shell -a 'curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health'статус приложения на всех веб-хостах200 на каждом хосте

Код 502 через прокси означает, что бэкенд недоступен: проверьте ss -tlnp на веб-хостах, совпадает ли имя upstream в proxy_pass с шаблоном upstream.conf, не блокирует ли трафик локальный firewall. Код 504 указывает на таймауты: увеличьте proxy_read_timeout и замерьте время ответа приложения через curl -w "%{time_total}".

Как перевести существующий bash-скрипт в Ansible плейбук

Миграция идёт по одному алгоритму: разбить скрипт на блоки, заменить команды модулями, вынести данные в переменные, проверить в check mode.

Разбор bash-скрипта на задачи

Возьмём типовой скрипт установки сайта.

#!/usr/bin/env bash
set -euo pipefail

apt-get update
apt-get install -y nginx
cp ./site.conf /etc/nginx/conf.d/site.conf
echo "proxy_read_timeout 60s;" >> /etc/nginx/nginx.conf
systemctl restart nginx

Пять строк распадаются на четыре независимые задачи: обновление кэша пакетов, установка Nginx, размещение конфига сайта, правка основного конфига. Перезапуск сервиса становится handler, а не отдельным шагом: он выполнится один раз в конце, даже если изменятся оба конфига.

- name: Настроить веб-сервер
  hosts: web
  become: true
  tasks:
    - name: Обновить кэш apt
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Установить nginx
      ansible.builtin.apt:
        name: nginx
        state: present

    - name: Разложить конфиг сайта
      ansible.builtin.copy:
        src: files/site.conf
        dest: /etc/nginx/conf.d/site.conf
        mode: "0644"
        backup: true
      notify: reload nginx

    - name: Задать proxy_read_timeout
      ansible.builtin.lineinfile:
        path: /etc/nginx/nginx.conf
        regexp: '^proxy_read_timeout'
        line: 'proxy_read_timeout 60s;'
      notify: reload nginx

  handlers:
    - name: reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Разница проявляется на втором запуске: bash-версия добавит вторую строку proxy_read_timeout и перезапустит сервис, плейбук покажет changed=0. Ansible к тому же делает reload вместо restart, и активные соединения не рвутся.

Замена команд на модули: таблица соответствия

КомандаМодульПримечание
apt-get installansible.builtin.aptдля RHEL-семейства ansible.builtin.dnf
cpansible.builtin.copyрежим файла задавайте через mode
cat > fileansible.builtin.templateпеременные Jinja2, проверка через validate
sed -iansible.builtin.lineinfileдля массовых замен ansible.builtin.replace
systemctlansible.builtin.servicestate: reloaded или restarted по ситуации
useraddansible.builtin.userгруппы, shell и домашний каталог в параметрах
mkdir -pansible.builtin.filestate: directory плюс owner и mode
curl или wgetansible.builtin.get_urlпроверка checksum защищает от подмены

Модули shell и command остаются для случаев без готового модуля, например для запуска certbot. Тогда добавляйте creates, чтобы задача не выполнялась при наличии результата:

- name: Выпустить сертификат, если его ещё нет
  ansible.builtin.command:
    cmd: certbot --nginx -d {{ domain }} --non-interactive --agree-tos
    creates: /etc/letsencrypt/live/{{ domain }}/fullchain.pem

Тестирование миграции: check mode и лимиты

# Проверка синтаксиса без подключения к хостам
ansible-playbook -i inventory/hosts.ini site.yml --syntax-check

# Прогноз с построчными диффами
ansible-playbook -i inventory/hosts.ini site.yml --check --diff

# Только один хост из группы
ansible-playbook -i inventory/hosts.ini site.yml --limit web01

# Подтверждение каждой задачи вручную
ansible-playbook -i inventory/hosts.ini site.yml --step

В выводе --diff смотрите строки со знаками плюс и минус: они показывают, какие именно строки конфига поменяются. Прогон с --limit на одном хосте из группы даёт понять, переживёт ли изменение вся группа, до того как плейбук пойдёт по остальным машинам.

Ansible, Terraform или bash: сравнение для разного масштаба

КритерийbashAnsibleTerraform
Создание облачных ресурсовнетнет, работает на готовых хостахда: VM, сети, балансировщики, DNS
Конфигурация ОС и ПОвручную, императивномодули по желаемому состояниютолько через cloud-init или provisioner
Идемпотентностьпроверки пишут вручнуювстроена в модулиобеспечивает state-файл
Агент на хостахне нуженне нужен, работа по SSHне нужен, работа через API провайдера
Язык описанияbashYAML и Jinja2HCL
Порог входанизкийсреднийсредний
Масштаб1-3 сервера5-500 серверовлюбой облачный ландшафт
Типовой сценарийодноразовая настройка, прототипповторяемые развёртывания, аудит, единые окруженияинфраструктура как код, версионирование окружений

Когда достаточно bash

Одна машина, одна задача, один запуск: поднять стенд, поставить сертификат, почистить логи. Bash остаётся удобным инструментом для локальных операций и обвязки, потому что не требует ни Python на хосте, ни инвентаря. Ограничение в том, что повторный запуск и параллельную работу на нескольких машинах придётся обеспечивать самому. Место shell в автоматизации Linux подробно разобрано в материале Автоматизация администрирования Linux в 2026: от bash до AI-агентов в DevOps.

Когда выбирать Ansible

Ansible закрывает управление конфигурацией: одинаковые пакеты, конфиги, пользователи, задания cron и сервисы на десятках машин, без агентов и с аудитом запусков. Он же подходит для регулярных работ: обновление конфигов из git, ротация сертификатов, раскатка версии приложения. Для первого проекта с ролями, инвентарём и Vault пригодится пошаговое руководство по Ansible со шаблоном репозитория и разбором типовых ошибок.

Когда нужен Terraform

Terraform создаёт ресурсы: виртуальные машины, подсети, security groups, балансировщики, DNS-записи. Настроить внутри созданных машин ПО он не умеет, для этого есть user_data, cloud-init или отдельный плейбук Ansible. Рабочая связка выглядит так: Terraform создаёт 3 VM и балансировщик, отдаёт IP в output, Ansible по этому инвентарю ставит Nginx, приложение и прокси. Сравнение Ansible и Terraform помогает развести зоны ответственности, а сравнение Ansible, Terraform и Chef по 8 критериям вместе с обзором Terraform, Ansible и Pulumi дают готовые сценарии для GCP и Yandex Cloud.

Практическая матрица выбора: 1-3 сервера и разовые задачи - bash; 5-50 серверов, несколько окружений и требования к повторяемости - Ansible; облачная инфраструктура с динамическим числом машин - Terraform вместе с Ansible.

Проверки и откат: как не сломать прод

Check mode и dry run

Флаг --check запускает плейбук в режиме прогноза: модули сообщают, что изменили бы, но систему не трогают. Комбинируйте его с --diff, чтобы увидеть построчную разницу файлов, и с --limit, чтобы прогнать прогноз на одном хосте.

ansible-playbook -i inventory/hosts.ini site.yml --check --diff --limit web01

Помните про модули, которые не умеют предсказывать результат: они помечаются skipped или показывают changed. Прогноз на stage и на проде даёт разные результаты, если переменные окружений расходятся, поэтому сверяйте group_vars перед запуском.

Стратегии отката

  • Бэкап перед изменением. У модулей copy, template и lineinfile есть параметр backup: true: предыдущая версия файла сохраняется рядом с оригиналом.
  • Версионирование конфигов. Шаблоны и переменные держите в git, тогда откат сводится к git revert и повторному запуску плейбука.
  • Теги. Запускайте части плейбука изолированно: --tags nginx, --skip-tags deploy, чтобы не задевать несвязанные изменения.
  • Обратные задачи. Для необратимых операций держите отдельный rollback-плейбук: вывести хост из балансировки, вернуть предыдущую версию пакета, восстановить конфиг из копии.
- name: Разложить конфиг с сохранением предыдущей версии
  ansible.builtin.copy:
    src: files/site.conf
    dest: /etc/nginx/conf.d/site.conf
    backup: true

Поэтапное применение и лимиты

Ключевое слово serial раскатывает плейбук волнами: serial: 1 обновляет машины по одной, serial: "30%" оставляет рабочими остальные 70% парка.

- name: Обновить веб-серверы по одному
  hosts: web
  become: true
  serial: 1
  max_fail_percentage: 0
  roles:
    - nginx

max_fail_percentage: 0 останавливает раскатку при первой же ошибке на любом хосте. Для проверки на одном представителе группы хватает --limit web01. Прод прогоняйте после stage с тем же инвентарём и теми же переменными: различия между средами держите только в group_vars, иначе проверка на stage ничего не гарантирует.

Для пошагового подтверждения задач служит флаг --step: перед каждой задачей Ansible спрашивает, выполнять её или пропустить. Приём полезен при первом запуске нового плейбука на живом стенде.

Чек-лист перед внедрением Ansible

  • Версии. На управляющей машине Ansible 2.16 или новее и Python 3.9+; на хостах достаточно Python 3, который уже стоит в Ubuntu 22.04/24.04.
  • SSH-доступ. Ключ разложен по хостам, вход по паролю закрыт, у пользователя есть sudo. Проверьте: ansible all -m ping и ansible all -m ping -b.
  • Инвентарь. Создан inventory/hosts.ini или динамический источник, группы совпадают с ролями: web, proxy, db.
  • Переменные. Общие значения в group_vars, исключения в host_vars, секреты зашифрованы через ansible-vault.
  • Роли. Каждая задача собрана в роль с defaults, шаблонами и handlers, внешние роли зафиксированы в requirements.yml.
  • Проверка. Прогон --syntax-check, затем --check --diff, затем --limit на одном хосте, затем stage.
  • Бэкапы. Для изменяемых конфигов включён backup: true, плейбуки и шаблоны лежат в git.
  • Раскатка. Для прод-групп заданы serial и max_fail_percentage, обновление идёт волнами.
  • Команда. Порядок запуска, роли и доступы описаны в README репозитория, новый инженер повторяет деплой по инструкции.

Типичные ошибки новичков: забыли become и получили отказ в доступе при установке пакетов; заменили все задачи на shell и потеряли идемпотентность; хранят пароли в group_vars открытым текстом; не используют handlers и перезапускают сервис на каждом прогоне; игнорируют changed_when для команд, которые всегда показывают changed.

Начните с малого: возьмите свой текущий скрипт развёртывания, разберите его на четыре-пять задач, замените команды на модули apt, copy, lineinfile и service и прогоните дважды подряд. Если второй запуск даёт changed=0, у вас есть идемпотентный плейбук, который остаётся расширить ролями, вынести переменные в group_vars и раскатывать по всем окружениям с флагом --limit на первом хосте.

Поделиться:
Сохранить гайд? В закладки браузера