Автоматизация серверов с Ansible: уроки из продакшена

ansibleautomationdevops

Terraform vs Ansible: два подхода к Infrastructure as Code

Два года назад я перешёл с ручного администрирования серверов на Ansible. За это время автоматизировал деплой нескольких приложений, CI/CD-пайплайнов и мониторинга. Расскажу про структуру проектов, подходы к работе с секретами и грабли, на которые успел наступить.

Почему Ansible, а не Salt/Puppet/Chef

У меня был опыт с Puppet и краткое знакомство с SaltStack. Вот почему я остановился на Ansible:

Отсутствие агентов. Ansible работает по SSH. Не нужно устанавливать и обновлять агенты на каждом сервере, не нужно следить за их здоровьем, не нужен отдельный master-сервер. Для моего масштаба (5-10 серверов) это идеальный подход.

Декларативный YAML. Плейбуки читаются как документация. Новый человек в команде может понять, что происходит, даже без знания Ansible. В Puppet с его DSL порог входа значительно выше.

Модульность. Система ролей в Ansible позволяет переиспользовать код между проектами. Роль для настройки Nginx написал один раз — используешь везде.

Огромная экосистема. Ansible Galaxy, коллекции от вендоров (AWS, GCP, Docker), тысячи готовых модулей. Для большинства задач не нужно писать свои модули.

Минусы тоже есть: скорость выполнения (SSH-сессия на каждую задачу), отсутствие постоянного состояния (нет drift detection из коробки), иногда неочевидное поведение переменных. Но для мне плюсы однозначно перевешивают.

Структура проекта

После нескольких итераций я пришёл к такой структуре:

ansible/
├── ansible.cfg
├── inventory.yml
├── group_vars/
│   ├── all/
│   │   ├── vars.yml
│   │   └── vault.yml
│   ├── web_servers/
│   │   └── vars.yml
│   └── db_servers/
│       └── vars.yml
├── playbooks/
│   ├── deploy.yml
│   └── monitoring.yml
└── roles/
    ├── common/
    │   ├── tasks/
    │   │   └── main.yml
    │   ├── handlers/
    │   │   └── main.yml
    │   ├── templates/
    │   └── defaults/
    │       └── main.yml
    ├── web_app/
    ├── monitoring/
    └── nginx/

Ключевой файл — ansible.cfg, который задаёт разумные значения по умолчанию:

[defaults]
inventory = inventory.yml
roles_path = roles
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
pipelining = True

[privilege_escalation]
become = False
become_method = sudo
become_ask_pass = False

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True

Обратите внимание на pipelining = True — это значительно ускоряет выполнение, потому что Ansible не создаёт временные файлы на удалённом хосте, а передаёт код через stdin. Экономия — десятки секунд на плейбуках с большим количеством задач.

Инвентарь

Я использую YAML-формат инвентаря вместо INI — он нагляднее:

all:
  children:
    web_servers:
      hosts:
        production:
          ansible_host: 194.87.104.208
          ansible_port: 6622
          ansible_user: alex
    db_servers:
      hosts:
        db-master:
          ansible_host: 185.210.45.67
          ansible_port: 22
          ansible_user: root

Для динамических окружений (AWS, Hetzner Cloud) лучше использовать динамические инвентари — плагины, которые автоматически подтягивают список хостов из API провайдера.

Работа с секретами: ansible-vault

Одна из лучших фич Ansible — встроенное шифрование секретов. Никаких сторонних инструментов, никаких утечек паролей в git.

# Создаём зашифрованный файл
ansible-vault create group_vars/all/vault.yml

# Редактируем существующий
ansible-vault edit group_vars/all/vault.yml

# Шифруем отдельную строку (для вставки в обычные файлы)
ansible-vault encrypt_string 'my_secret_password' --name 'vault_db_password'

Структура vault-файла:

# group_vars/all/vault.yml (зашифрован)
vault_db_password: "s3cur3_p@ssw0rd"
vault_api_secret: "another_secret"
vault_telegram_bot_token: "123456:ABC-DEF..."
vault_acme_email: "admin@example.com"

Важное правило: все переменные в vault начинаются с vault_. В обычном vars.yml делаем ссылки:

# group_vars/all/vars.yml (не зашифрован)
db_password: "{{ vault_db_password }}"
api_secret: "{{ vault_api_secret }}"

Это позволяет видеть, какие переменные существуют, не расшифровывая vault. Паттерн подсмотрен в официальных best practices Ansible.

Для удобства можно сохранить пароль vault в файл:

echo "my_vault_password" > ~/.vault_pass
chmod 600 ~/.vault_pass

И указать его в ansible.cfg:

[defaults]
vault_password_file = ~/.vault_pass

Тогда не придётся каждый раз вводить --ask-vault-pass.

Идемпотентность: главный принцип

Идемпотентность — это гарантия, что повторный запуск плейбука не сломает систему и не изменит то, что уже настроено корректно. Это фундаментальный принцип Ansible, и его легко нарушить.

Плохой пример:

# НЕ ДЕЛАЙТЕ ТАК
- name: Add line to config
  ansible.builtin.shell: echo "MaxSessions 10" >> /etc/ssh/sshd_config

Каждый запуск будет добавлять строку заново. Правильно:

# Правильный подход
- name: Set MaxSessions in sshd_config
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?MaxSessions'
    line: 'MaxSessions 10'
    state: present
  notify: restart sshd

Ещё лучше — использовать шаблоны. Модуль template рендерит Jinja2-шаблон и копирует файл только если содержимое изменилось:

- name: Deploy sshd config
  ansible.builtin.template:
    src: sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: '0644'
    validate: '/usr/sbin/sshd -t -f %s'
  notify: restart sshd

Обратите внимание на параметр validate — он проверяет конфигурацию перед заменой файла. Если шаблон сгенерировал невалидный конфиг, задача упадёт, и рабочий файл останется нетронутым.

Хендлеры: не перезапускайте сервисы зря

Хендлеры вызываются только при изменении задачи (статус changed). Это предотвращает ненужные рестарты:

# roles/common/handlers/main.yml
- name: restart sshd
  ansible.builtin.systemd:
    name: sshd
    state: restarted
    daemon_reload: true

- name: reload nginx
  ansible.builtin.systemd:
    name: nginx
    state: reloaded

Хендлеры выполняются в конце play, в порядке их определения (не в порядке вызова). Если нужно выполнить хендлер немедленно, используйте meta: flush_handlers:

- name: Deploy nginx config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: reload nginx

- name: Flush handlers now
  ansible.builtin.meta: flush_handlers

- name: Check nginx is responding
  ansible.builtin.uri:
    url: "http://localhost"
    status_code: 200

Роли: переиспользуемые блоки

Типичная роль для установки и настройки сервиса:

# roles/common/tasks/main.yml
- name: Update apt cache
  ansible.builtin.apt:
    update_cache: true
    cache_valid_time: 3600
  become: true

- name: Install essential packages
  ansible.builtin.apt:
    name:
      - curl
      - wget
      - htop
      - vim
      - ufw
      - fail2ban
      - unattended-upgrades
    state: present
  become: true

- name: Configure UFW defaults
  community.general.ufw:
    direction: incoming
    default: deny
  become: true

- name: Allow SSH
  community.general.ufw:
    rule: allow
    port: "{{ ansible_port | default(22) }}"
    proto: tcp
  become: true

- name: Enable UFW
  community.general.ufw:
    state: enabled
  become: true

- name: Set timezone
  community.general.timezone:
    name: "{{ server_timezone | default('UTC') }}"
  become: true

Переменные со значениями по умолчанию идут в defaults/main.yml:

# roles/common/defaults/main.yml
server_timezone: "UTC"
ssh_port: 22
enable_fail2ban: true

Плейбук, использующий роли:

# playbooks/deploy.yml
---
- name: Configure base server
  hosts: all
  become: false
  roles:
    - common

- name: Deploy web application
  hosts: web_servers
  become: false
  roles:
    - web_app
    - monitoring

Практические советы из продакшена

1. Всегда используйте FQCN для модулей

Вместо apt пишите ansible.builtin.apt. Это избавляет от конфликтов имён с коллекциями и является обязательным с Ansible 2.10+.

2. Тестируйте плейбуки в check mode

ansible-playbook playbooks/deploy.yml --check --diff

Флаг --check выполняет «сухой прогон» без реальных изменений, --diff показывает, что именно изменится в файлах.

3. Используйте теги для частичного выполнения

- name: Deploy application code
  ansible.builtin.git:
    repo: "{{ app_repo }}"
    dest: /opt/app
    version: "{{ app_version }}"
  tags: [deploy, app]
# Запускаем только задачи с тегом deploy
ansible-playbook playbooks/deploy.yml --tags deploy

4. Не игнорируйте ошибки без причины

# Плохо — скрывает реальные проблемы
- name: Do something
  ansible.builtin.command: some_command
  ignore_errors: true

# Лучше — явная обработка
- name: Check if service exists
  ansible.builtin.command: systemctl status myservice
  register: service_status
  failed_when: false
  changed_when: false

- name: Configure service
  ansible.builtin.template:
    src: myservice.conf.j2
    dest: /etc/myservice.conf
  when: service_status.rc == 0

5. Используйте block для группировки и обработки ошибок

- name: Deploy with rollback
  block:
    - name: Deploy new version
      ansible.builtin.git:
        repo: "{{ app_repo }}"
        dest: /opt/app
        version: "{{ new_version }}"

    - name: Run migrations
      ansible.builtin.command: /opt/app/manage.py migrate
  rescue:
    - name: Rollback to previous version
      ansible.builtin.git:
        repo: "{{ app_repo }}"
        dest: /opt/app
        version: "{{ old_version }}"

    - name: Notify about failure
      ansible.builtin.debug:
        msg: "Deploy failed, rolled back to {{ old_version }}"

Типичные ошибки

Хардкод путей и IP-адресов. Всё должно быть в переменных. Вынесите пути в defaults/main.yml, IP-адреса в инвентарь.

Монолитные плейбуки. Если плейбук на 500 строк — разбейте его на роли. Каждая роль отвечает за один сервис или компонент.

Отсутствие become: true. Забыть become на задаче, требующей root — классика. Ansible молча падает с «Permission denied», а вы десять минут ищете причину.

Запуск без --diff. Всегда запускайте с --diff, чтобы видеть, что именно меняется на серверах.

Заключение

Ansible — не серебряная пуля, но для команд из 1-5 человек, управляющих десятками серверов, это оптимальный инструмент. Порог входа низкий, документация отличная, а правильная структура проекта позволяет масштабироваться без боли.

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

© 2026 Terminal Notes. Built with SvelteKit.