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
- Больше 5 серверов. На 10 машинах разовый обход по SSH занимает минуты, а сверка версий пакетов и конфигов превращается в ручной аудит. Инвентарь Ansible даёт адресную выборку: ansible web -m ping.
- Два и больше окружения. dev, stage и prod расходятся в переменных: домен, порты, пути к секретам. В bash это отдельные копии скрипта, в Ansible это group_vars/stage.yml и group_vars/prod.yml.
- Требование идемпотентности. Если развёртывание запускается из CI после каждого коммита или по расписанию, повторный прогон обязан завершаться без изменений.
- Нужен откат. Перед правкой конфига требуется бэкап и понятный план возврата. Модуль copy с backup: true сохраняет предыдущую версию файла рядом с оригиналом.
- Командная работа и аудит. 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 nginx | ansible.builtin.apt (dnf для RHEL) | ставит пакет только при отсутствии, кэш обновляется по cache_valid_time |
| cp site.conf /etc/nginx/conf.d/ | ansible.builtin.copy | копирует при расхождении, backup: true сохраняет старую версию |
| cat > vhost.conf со heredoc | ansible.builtin.template | подставляет переменные Jinja2 и сравнивает результат с текущим файлом |
| sed -i 's/old/new/' nginx.conf | ansible.builtin.lineinfile, ansible.builtin.replace | меняет строку по regexp и не дублирует её при повторном запуске |
| systemctl restart nginx | ansible.builtin.service | перезапускает только при changed через handler |
| useradd -m deploy | ansible.builtin.user | создаёт пользователя, повторный запуск не вносит изменений |
| curl -fsSL url -o /tmp/file | ansible.builtin.get_url | скачивает при отсутствии файла или смене checksum |
| tar -xzf archive.tar.gz -C /opt | ansible.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 install | ansible.builtin.apt | для RHEL-семейства ansible.builtin.dnf |
| cp | ansible.builtin.copy | режим файла задавайте через mode |
| cat > file | ansible.builtin.template | переменные Jinja2, проверка через validate |
| sed -i | ansible.builtin.lineinfile | для массовых замен ansible.builtin.replace |
| systemctl | ansible.builtin.service | state: reloaded или restarted по ситуации |
| useradd | ansible.builtin.user | группы, shell и домашний каталог в параметрах |
| mkdir -p | ansible.builtin.file | state: directory плюс owner и mode |
| curl или wget | ansible.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: сравнение для разного масштаба
| Критерий | bash | Ansible | Terraform |
|---|---|---|---|
| Создание облачных ресурсов | нет | нет, работает на готовых хостах | да: VM, сети, балансировщики, DNS |
| Конфигурация ОС и ПО | вручную, императивно | модули по желаемому состоянию | только через cloud-init или provisioner |
| Идемпотентность | проверки пишут вручную | встроена в модули | обеспечивает state-файл |
| Агент на хостах | не нужен | не нужен, работа по SSH | не нужен, работа через API провайдера |
| Язык описания | bash | YAML и Jinja2 | HCL |
| Порог входа | низкий | средний | средний |
| Масштаб | 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 на первом хосте.