Ansible и Molecule

Ansible — инструмент автоматизации, который описывает желаемое состояние серверов и приложений в декларативных YAML-сценариях. С его помощью устанавливают пакеты, создают пользователей и файлы, настраивают службы, разворачивают приложения и выполняют повторяемые операции на группах узлов.

Molecule — инструмент разработки и тестирования ролей Ansible. Он создаёт временные тестовые экземпляры, применяет к ним роль, проверяет результат и удаляет окружение.

Основной принцип Ansible:

инвентарь → playbook → задачи → модули → управляемые узлы

Минимальная задача:

- name: Создать каталог приложения
  ansible.builtin.file:
    path: /opt/my_app
    state: directory
    owner: root
    group: root
    mode: "0755"

Комментарий в YAML:

# Это комментарий

YAML чувствителен к отступам. Обычно используют два пробела и не применяют табуляцию.

Содержание


Установка и базовые команды

Установка Ansible

Ansible запускается на управляющем узле: рабочей станции, CI-сервере или выделенном сервере автоматизации. На управляемых Linux-узлах обычно нужны SSH и Python.

Установка в виртуальное окружение Python:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install ansible-core

Проверка версии и путей конфигурации:

ansible --version

Пакет ansible-core содержит ядро и встроенные коллекции. Дополнительные модули и роли устанавливаются отдельно через Ansible Galaxy.

Конфигурация ansible.cfg

Пример локального файла конфигурации:

[defaults]
inventory = ./inventory.yml
roles_path = ./roles
host_key_checking = True
retry_files_enabled = False
interpreter_python = auto_silent

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

Ansible ищет конфигурацию в нескольких местах. Локальный ansible.cfg в каталоге проекта удобен тем, что хранит настройки рядом с кодом автоматизации.

Текущие параметры можно посмотреть командой:

ansible-config dump --only-changed

Основные команды

Проверить доступность узлов:

ansible all -m ansible.builtin.ping

Выполнить модуль на группе:

ansible web -m ansible.builtin.command -a "uname -a"

Запустить playbook:

ansible-playbook site.yml

Проверить синтаксис:

ansible-playbook site.yml --syntax-check

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

ansible-playbook site.yml --check --diff

--check поддерживается не всеми модулями одинаково хорошо. Результат проверочного запуска следует воспринимать как прогноз, а не как абсолютную гарантию.

Ограничить выполнение частью инвентаря:

ansible-playbook site.yml --limit web

Запустить задачи с определённым тегом:

ansible-playbook site.yml --tags nginx

Запросить пароль для sudo:

ansible-playbook site.yml --ask-become-pass

Инвентарь

Инвентарь описывает управляемые узлы, группы узлов и связанные с ними переменные.

Инвентарь может быть:

INI-инвентарь

Файл inventory.ini:

[web]
web-01 ansible_host=192.0.2.11
web-02 ansible_host=192.0.2.12

[db]
db-01 ansible_host=192.0.2.21

[production:children]
web
db

[all:vars]
ansible_user=deployer
ansible_port=22

YAML-инвентарь

Файл inventory.yml:

all:
  vars:
    ansible_user: deployer
    ansible_port: 22

  children:
    production:
      children:
        web:
          hosts:
            web-01:
              ansible_host: 192.0.2.11
            web-02:
              ansible_host: 192.0.2.12

        db:
          hosts:
            db-01:
              ansible_host: 192.0.2.21

YAML-формат удобен для вложенных структур и сложных значений.

Специальные группы

Ansible автоматически предоставляет группы:

Переменные подключения

Часто используемые переменные:

Переменная Назначение
ansible_host IP-адрес или DNS-имя для подключения
ansible_port SSH-порт
ansible_user пользователь SSH
ansible_connection тип подключения: ssh, local, docker и другие
ansible_python_interpreter путь к Python на управляемом узле
ansible_ssh_private_key_file путь к закрытому SSH-ключу
ansible_become включение повышения привилегий

Секреты не следует хранить открытым текстом в инвентаре. Для них используют Ansible Vault или внешнее хранилище секретов.

group_vars и host_vars

Переменные лучше отделять от списка узлов:

project/
├── ansible.cfg
├── inventory.yml
├── group_vars/
│   ├── all.yml
│   ├── web.yml
│   └── production.yml
└── host_vars/
    └── web-01.yml

group_vars/all.yml:

app_user: my_app
app_root: /opt/my_app

group_vars/web.yml:

http_port: 8080
worker_count: 4

host_vars/web-01.yml:

worker_count: 8

Узловая переменная позволяет переопределить групповое значение для конкретного хоста.

Просмотр инвентаря

Показать итоговую структуру:

ansible-inventory --graph

Показать все вычисленные данные:

ansible-inventory --list

Показать сведения об одном узле:

ansible-inventory --host web-01

Шаблоны узлов

Команды и plays принимают шаблоны выбора узлов:

ansible web -m ansible.builtin.ping
ansible 'web:&production' -m ansible.builtin.ping
ansible 'all:!db' -m ansible.builtin.ping

В командной оболочке шаблон лучше заключать в кавычки, чтобы специальные символы не интерпретировались оболочкой.


Playbooks

Playbook — YAML-файл со списком plays. Каждый play связывает выбранные узлы с задачами, ролями, переменными и обработчиками.

Минимальный playbook

Файл site.yml:

---
- name: Настроить веб-серверы
  hosts: web
  become: true
  gather_facts: true

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

    - name: Запустить и включить Nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

Начальная строка --- обозначает начало YAML-документа. Она необязательна для Ansible, но часто используется для единообразия.

Задачи

Каждая задача обычно вызывает один модуль:

- name: Создать пользователя приложения
  ansible.builtin.user:
    name: my_app
    system: true
    shell: /usr/sbin/nologin
    create_home: false

Имена задач должны объяснять желаемый результат. Формулировка «Создать пользователя приложения» информативнее, чем «Запустить user».

Повышение привилегий

Для всего play:

- name: Системная настройка
  hosts: all
  become: true
  tasks:
    - name: Создать каталог
      ansible.builtin.file:
        path: /opt/my_app
        state: directory
        mode: "0755"

Для отдельной задачи:

- name: Прочитать локальный пользовательский файл
  ansible.builtin.command: id
  become: false
  changed_when: false

Факты

При gather_facts: true доступны переменные с информацией об узле:

- name: Показать семейство ОС
  ansible.builtin.debug:
    msg: "Семейство ОС: {{ ansible_facts['os_family'] }}"

Условие по фактам:

- name: Установить пакет на Debian-подобной системе
  ansible.builtin.apt:
    name: curl
    state: present
    update_cache: true
  when: ansible_facts['os_family'] == 'Debian'

Если факты не нужны, их сбор можно отключить:

gather_facts: false

Условия when

- name: Создать конфигурацию только для production
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/my_app/app.conf
    mode: "0644"
  when: app_environment == 'production'

В when выражение Jinja2 записывается без {{ }}.

Несколько условий:

when:
  - app_enabled | bool
  - ansible_facts['os_family'] == 'Debian'

Циклы

Создание нескольких каталогов:

- name: Создать каталоги приложения
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: my_app
    group: my_app
    mode: "0755"
  loop:
    - /opt/my_app
    - /opt/my_app/bin
    - /opt/my_app/data

Цикл по словарям:

- name: Создать пользователей
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
    state: present
  loop:
    - name: alice
      groups: developers
    - name: bob
      groups: operators
  loop_control:
    label: "{{ item.name }}"

Регистрация результата

- name: Получить версию приложения
  ansible.builtin.command: /opt/my_app/bin/my_app --version
  register: app_version_result
  changed_when: false

- name: Показать версию
  ansible.builtin.debug:
    var: app_version_result.stdout

Результат модуля обычно содержит поля changed, failed, stdout, stderr, rc и другие, но точный набор зависит от модуля.

Обработчики

Handler выполняется только после уведомления от изменившей состояние задачи.

- name: Установить конфигурацию Nginx
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"
    validate: "nginx -t -c %s"
  notify: Перезапустить Nginx

handlers:
  - name: Перезапустить Nginx
    ansible.builtin.service:
      name: nginx
      state: restarted

Если шаблон не изменился, обработчик не апускается. По умолчанию handlers выполняются в конце соответствующего play.

Принудительно выполнить накопленные handlers раньше:

- name: Выполнить обработчики сейчас
  ansible.builtin.meta: flush_handlers

Блоки и обработка ошибок

- name: Обновить приложение
  block:
    - name: Развернуть новую версию
      ansible.builtin.copy:
        src: my_app
        dest: /opt/my_app/bin/my_app
        mode: "0755"

  rescue:
    - name: Сообщить об ошибке развёртывания
      ansible.builtin.debug:
        msg: Развёртывание не выполнено

  always:
    - name: Удалить временные файлы
      ansible.builtin.file:
        path: /tmp/my_app.tmp
        state: absent

Теги

- name: Установить Nginx
  ansible.builtin.package:
    name: nginx
    state: present
  tags:
    - nginx
    - packages

Запуск:

ansible-playbook site.yml --tags nginx
ansible-playbook site.yml --skip-tags packages

Импорт и подключение файлов

Статический импорт:

- name: Подключить задачи установки
  ansible.builtin.import_tasks: install.yml

Динамическое подключение:

- name: Подключить задачи для выбранной ОС
  ansible.builtin.include_tasks: "{{ ansible_facts['os_family'] | lower }}.yml"

import_* обрабатывается при разборе playbook, а include_* — во время выполнения. Динамическое подключение удобно, когда имя файла или набор задач зависит от переменных и условий.


Роли

Роль — стандартная структура каталогов для переиспользуемой конфигурации. Роль объединяет задачи, handlers, шаблоны, файлы, переменные, зависимости и метаданные.

Структура роли

roles/
└── my_app/
    ├── defaults/
    │   └── main.yml
    ├── files/
    │   └── my_app.service
    ├── handlers/
    │   └── main.yml
    ├── meta/
    │   └── main.yml
    ├── tasks/
    │   └── main.yml
    ├── templates/
    │   └── app.conf.j2
    ├── vars/
    │   └── main.yml
    └── README.md
Каталог Назначение
tasks основные задачи роли
handlers обработчики
templates шаблоны Jinja2
files статические файлы
defaults значения по умолчанию с низким приоритетом
vars внутренние переменные роли с более высоким приоритетом
meta метаданные и зависимости роли
molecule сценарии тестирования Molecule, если они добавлены

Пустые каталоги создавать необязательно.

Создание каркаса

ansible-galaxy role init roles/my_app

Команда создаёт стандартную структуру. Ненужные каталоги можно удалить.

Значения по умолчанию

roles/my_app/defaults/main.yml:

---
my_app_user: my_app
my_app_group: my_app
my_app_root: /opt/my_app
my_app_port: 8080
my_app_log_level: info

Пользователь роли может переопределить эти значения в инвентаре или playbook.

Внутренние переменные роли

roles/my_app/vars/main.yml:

---
my_app_config_dir: /etc/my_app
my_app_service_name: my_app

В vars следует помещать значения, которые обычно не должны изменяться пользователем. Настраиваемые параметры лучше хранить в defaults.

Задачи роли

roles/my_app/tasks/main.yml:

---
- name: Создать группу приложения
  ansible.builtin.group:
    name: "{{ my_app_group }}"
    system: true
    state: present

- name: Создать пользователя приложения
  ansible.builtin.user:
    name: "{{ my_app_user }}"
    group: "{{ my_app_group }}"
    system: true
    create_home: false
    shell: /usr/sbin/nologin
    state: present

- name: Создать каталог конфигурации
  ansible.builtin.file:
    path: "{{ my_app_config_dir }}"
    state: directory
    owner: root
    group: "{{ my_app_group }}"
    mode: "0750"

- name: Создать конфигурацию приложения
  ansible.builtin.template:
    src: app.conf.j2
    dest: "{{ my_app_config_dir }}/app.conf"
    owner: root
    group: "{{ my_app_group }}"
    mode: "0640"
  notify: Перезапустить my_app

Обработчики роли

roles/my_app/handlers/main.yml:

---
- name: Перезапустить my_app
  ansible.builtin.service:
    name: "{{ my_app_service_name }}"
    state: restarted

Подключение роли

Через секцию roles:

---
- name: Настроить приложение
  hosts: app
  become: true

  roles:
    - role: my_app
      my_app_port: 9000

Динамически внутри задач:

- name: Подключить роль my_app
  ansible.builtin.include_role:
    name: my_app
  vars:
    my_app_port: 9000

Статически внутри задач:

- name: Импортировать роль my_app
  ansible.builtin.import_role:
    name: my_app

Зависимости роли

roles/my_app/meta/main.yml:

---
dependencies:
  - role: common

Зависимости выполняются до основной роли. Не следует превращать meta/main.yml в длинную неявную цепочку: явное подключение ролей в playbook часто проще читать и отлаживать.

Коллекции

Коллекция — пакет, который может содержать роли, модули, плагины и документацию.

Установка коллекции:

ansible-galaxy collection install community.general

Файл зависимостей requirements.yml:

---
collections:
  - name: community.general
    version: ">=8.0.0"

roles:
  - name: geerlingguy.git

Установка зависимостей:

ansible-galaxy install -r requirements.yml

Версии зависимостей желательно фиксировать или ограничивать совместимым диапазоном, чтобы сборки оставались воспроизводимыми.


Модули Ansible

Модуль — единица работы Ansible. Модуль получает аргументы, проверяет состояние узла, при необходимости изменяет его и возвращает структурированный результат.

FQCN

Рекомендуется указывать полное имя модуля:

ansible.builtin.copy
community.general.ufw

Такой формат называется Fully Qualified Collection Name. Он показывает, из какой коллекции взят модуль, и устраняет неоднозначность.

Документация модуля

ansible-doc ansible.builtin.copy
ansible-doc ansible.builtin.template
ansible-doc -l

Документация содержит параметры, примеры, возвращаемые значения и сведения о поддержке check mode.

Управление пакетами

Универсальный модуль:

- name: Установить Git
  ansible.builtin.package:
    name: git
    state: present

Для Debian-подобных систем:

- name: Установить пакеты
  ansible.builtin.apt:
    name:
      - nginx
      - curl
    state: present
    update_cache: true
    cache_valid_time: 3600

Для систем с DNF:

- name: Установить пакеты
  ansible.builtin.dnf:
    name:
      - nginx
      - curl
    state: present

Файлы и каталоги

Создать каталог:

- name: Создать каталог данных
  ansible.builtin.file:
    path: /var/lib/my_app
    state: directory
    owner: my_app
    group: my_app
    mode: "0750"

Удалить файл или каталог:

- name: Удалить устаревший файл
  ansible.builtin.file:
    path: /etc/my_app/old.conf
    state: absent

Скопировать статический файл:

- name: Скопировать unit-файл
  ansible.builtin.copy:
    src: my_app.service
    dest: /etc/systemd/system/my_app.service
    owner: root
    group: root
    mode: "0644"

Создать файл из шаблона:

- name: Создать конфигурацию
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/my_app/app.conf
    owner: root
    group: my_app
    mode: "0640"

Изменить одну строку:

- name: Установить параметр конфигурации
  ansible.builtin.lineinfile:
    path: /etc/my_app/app.conf
    regexp: '^log_level='
    line: 'log_level=warning'

Управлять блоком:

- name: Добавить управляемый блок
  ansible.builtin.blockinfile:
    path: /etc/hosts
    marker: "# {mark} ANSIBLE MANAGED MY_APP"
    block: |
      192.0.2.11 app-01
      192.0.2.12 app-02

Для полностью управляемого конфигурационного файла обычно лучше template, а не множество lineinfile.

Службы

- name: Запустить и включить службу
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

Для systemd-специфичных возможностей:

- name: Перечитать unit-файлы systemd
  ansible.builtin.systemd_service:
    daemon_reload: true

Загрузка и распаковка

- name: Скачать архив
  ansible.builtin.get_url:
    url: "https://example.com/my_app-{{ my_app_version }}.tar.gz"
    dest: "/tmp/my_app-{{ my_app_version }}.tar.gz"
    checksum: "sha256:{{ my_app_archive_sha256 }}"
    mode: "0644"
- name: Распаковать приложение
  ansible.builtin.unarchive:
    src: "/tmp/my_app-{{ my_app_version }}.tar.gz"
    dest: /opt/my_app
    remote_src: true

Контрольная сумма защищает от случайной подмены или повреждения файла и делает загрузку предсказуемее.

command, shell и raw

command выполняет программу без оболочки:

- name: Проверить конфигурацию
  ansible.builtin.command: /usr/local/bin/my_app --check-config
  changed_when: false

argv помогает безопасно передавать аргументы:

- name: Выполнить команду с аргументами
  ansible.builtin.command:
    argv:
      - /usr/local/bin/my_app
      - --config
      - /etc/my_app/app.conf
  changed_when: false

shell нужен, когда используются конвейеры, перенаправления, подстановка оболочки или её встроенные команды:

- name: Найти ошибки в журнале
  ansible.builtin.shell:
    cmd: "set -o pipefail && grep ERROR /var/log/my_app.log | tail -n 20"
    executable: /bin/bash
  register: error_lines
  changed_when: false
  failed_when: error_lines.rc not in [0, 1]

raw выполняет команду без использования Python на удалённой стороне. Он полезен главным образом для первоначальной подготовки узла, например установки Python.

Предпочтительный порядок выбора:

  1. специализированный идемпотентный модуль;
  2. command;
  3. shell, только если необходимы функции оболочки;
  4. raw, только для особых случаев.

Ожидание и проверки

Ожидание порта:

- name: Дождаться запуска приложения
  ansible.builtin.wait_for:
    host: 127.0.0.1
    port: "{{ my_app_port }}"
    timeout: 30

HTTP-проверка:

- name: Проверить health endpoint
  ansible.builtin.uri:
    url: "http://127.0.0.1:{{ my_app_port }}/health"
    method: GET
    status_code: 200
    return_content: true
  register: health_response
  changed_when: false

Проверка условия:

- name: Проверить ответ приложения
  ansible.builtin.assert:
    that:
      - health_response.status == 200
      - "'ok' in health_response.content"
    fail_msg: Приложение не прошло проверку

Переменные и шаблоны Jinja2

Переменные позволяют использовать один playbook или роль в разных окружениях. Jinja2 формирует строки и файлы на основе этих переменных.

Определение переменных

В playbook:

- name: Настроить приложение
  hosts: app
  vars:
    app_port: 8080
    app_debug: false
    app_features:
      - metrics
      - audit

Через отдельный файл:

vars_files:
  - vars/common.yml
  - "vars/{{ app_environment }}.yml"

Через командную строку:

ansible-playbook site.yml --extra-vars "app_environment=staging app_port=9000"

Или из YAML/JSON-файла:

ansible-playbook site.yml --extra-vars "@vars/release.yml"

Extra vars имеют очень высокий приоритет. Для обычной настройки проекта удобнее group_vars, host_vars и defaults роли.

Типы данных

app_name: my_app
app_port: 8080
app_enabled: true
app_optional_value: null

app_packages:
  - curl
  - ca-certificates

app_database:
  host: db.internal
  port: 5432
  ssl: true

Обращение к словарю:

- name: Показать адрес БД
  ansible.builtin.debug:
    msg: "{{ app_database.host }}:{{ app_database.port }}"

Безопаснее использовать явный доступ по ключу, если ключ может совпасть с методом словаря:

msg: "{{ app_database['host'] }}:{{ app_database['port'] }}"

Подстановка значений

- name: Создать каталог версии
  ansible.builtin.file:
    path: "/opt/{{ app_name }}/releases/{{ app_version }}"
    state: directory
    mode: "0755"

Если всё значение является выражением, его обычно заключают в кавычки:

port: "{{ app_port }}"

Это особенно важно, когда выражение стоит в начале YAML-значения.

Фильтры

Значение по умолчанию:

{{ app_port | default(8080) }}

Преобразование к логическому типу:

when: app_enabled | bool

Преобразование регистра:

{{ app_environment | upper }}

Объединение списка:

{{ app_features | join(',') }}

Сериализация в JSON:

{{ app_config | to_nice_json }}

Хеш:

{{ app_secret | hash('sha256') }}

Обязательная переменная:

my_app_api_url: "{{ undef(hint='Необходимо задать my_app_api_url') }}"

Доступность undef() и отдельных фильтров зависит от версии Ansible и установленных коллекций. Совместимость роли следует фиксировать в метаданных и CI.

Проверка существования

- name: Использовать необязательную переменную
  ansible.builtin.debug:
    var: optional_value
  when: optional_value is defined
- name: Проверить обязательные параметры
  ansible.builtin.assert:
    that:
      - my_app_port is defined
      - my_app_port | int > 0
      - my_app_port | int < 65536
    fail_msg: my_app_port должен быть допустимым TCP-портом

Шаблон Jinja2

templates/app.conf.j2:

# Этот файл управляется Ansible. Ручные изменения будут перезаписаны.

[server]
host = {{ my_app_host | default('0.0.0.0') }}
port = {{ my_app_port }}
debug = {{ my_app_debug | bool | lower }}

[logging]
level = {{ my_app_log_level | upper }}

{% if my_app_tls_enabled | bool %}
[tls]
certificate = {{ my_app_tls_certificate }}
private_key = {{ my_app_tls_private_key }}
{% endif %}

[features]
{% for feature in my_app_features %}
{{ feature }} = enabled
{% endfor %}

Основные конструкции Jinja2:

Синтаксис Назначение
{{ expression }} вывести значение
{% statement %} условие, цикл или другая управляющая конструкция
{# comment #} комментарий Jinja2, не попадающий в результат

Условия в шаблоне

{% if app_environment == 'production' %}
log_level = warning
{% elif app_environment == 'staging' %}
log_level = info
{% else %}
log_level = debug
{% endif %}

Циклы в шаблоне

{% for server in backend_servers %}
server {{ server.name }} {{ server.host }}:{{ server.port }};
{% endfor %}

Переменные:

backend_servers:
  - name: app-01
    host: 192.0.2.31
    port: 8080
  - name: app-02
    host: 192.0.2.32
    port: 8080

Пробелы в шаблонах

Дефисы в управляющих конструкциях удаляют соседние пробелы и переводы строк:

{% for item in items -%}
{{ item }}
{% endfor %}

Использовать такое управление пробелами следует осторожно: чрезмерное удаление переносов может склеить строки конфигурации.

Приоритет переменных

У Ansible много уровней приоритета. Практически важно помнить:

Вместо запоминания всей таблицы приоритетов полезно придерживаться стабильной схемы хранения и не определять одну переменную сразу во многих местах.

Имена переменных роли

Переменные публичной роли желательно снабжать уникальным префиксом:

my_app_port: 8080
my_app_user: my_app
my_app_config_dir: /etc/my_app

Префикс уменьшает риск конфликтов с переменными других ролей.


Идемпотентность

Идемпотентность означает, что повторный запуск автоматизации приводит систему к тому же состоянию и не создаёт лишних изменений.

Первый запуск:

changed=5  failed=0

Повторный запуск без изменения входных данных:

changed=0  failed=0

Идемпотентные модули

- name: Установить пакет
  ansible.builtin.package:
    name: nginx
    state: present

Модуль сначала проверяет наличие пакета. Если пакет уже установлен, задача возвращает changed: false.

- name: Обеспечить запуск службы
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

Если служба уже запущена и включена, повторный запуск ничего не меняет.

Неидемпотентная команда

- name: Добавить строку
  ansible.builtin.shell: echo "enabled=true" >> /etc/my_app/app.conf

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

Идемпотентный вариант:

- name: Обеспечить наличие параметра
  ansible.builtin.lineinfile:
    path: /etc/my_app/app.conf
    regexp: '^enabled='
    line: 'enabled=true'
    create: true
    mode: "0644"

creates и removes

Если специализированного модуля нет, выполнение команды можно ограничить:

- name: Инициализировать базу данных
  ansible.builtin.command: /usr/local/bin/my_app init-db
  args:
    creates: /var/lib/my_app/.initialized

Команда не выполняется, если файл уже существует.

- name: Выполнить миграцию при наличии старого маркера
  ansible.builtin.command: /usr/local/bin/my_app migrate
  args:
    removes: /var/lib/my_app/.needs-migration

Команда выполняется только пока существует указанный путь.

changed_when

Команда чтения не должна сообщать об изменении:

- name: Получить текущий статус
  ansible.builtin.command: /usr/local/bin/my_app status
  register: app_status
  changed_when: false

Определение изменения по выводу:

- name: Применить миграции
  ansible.builtin.command: /usr/local/bin/my_app migrate
  register: migration_result
  changed_when: "'applied' in migration_result.stdout"

failed_when

- name: Проверить состояние
  ansible.builtin.command: /usr/local/bin/my_app check
  register: check_result
  changed_when: false
  failed_when: check_result.rc not in [0, 2]

Здесь коды 0 и 2 считаются допустимыми по правилам конкретной программы.

Handler вместо безусловного перезапуска

Плохо:

- name: Установить конфигурацию
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/my_app/app.conf

- name: Всегда перезапускать приложение
  ansible.builtin.service:
    name: my_app
    state: restarted

Лучше:

- name: Установить конфигурацию
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/my_app/app.conf
    mode: "0644"
  notify: Перезапустить my_app

Обработчик будет вызван только при реальном изменении файла.

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

Вручную:

ansible-playbook site.yml
ansible-playbook site.yml

Во втором запуске ожидается changed=0, если входные данные и состояние внешних систем не изменились.

Через Molecule:

molecule idempotence

Или полный сценарий:

molecule test

Некоторые операции по природе меняются при каждом запуске: генерация случайного значения, принудительный рестарт, получение постоянно обновляемого артефакта. Их следует изолировать, явно описывать и не маскировать необоснованным changed_when: false.


Ansible Vault — шифрование секретов

Ansible Vault шифрует YAML-файлы или отдельные значения, чтобы секреты можно было хранить рядом с кодом в зашифрованном виде.

Vault подходит для:

Шифрование не отменяет контроль доступа. Пароль Vault, временные расшифрованные файлы, журналы и вывод CI также необходимо защищать.

Создание зашифрованного файла

ansible-vault create group_vars/production/vault.yml

Редактор откроет новый файл. Пример расшифрованного содержимого:

vault_db_password: correct-horse-battery-staple
vault_api_token: replace-with-real-token

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

$ANSIBLE_VAULT;1.1;AES256
...

Шифрование существующего файла

ansible-vault encrypt group_vars/production/vault.yml

Расшифровать файл полностью:

ansible-vault decrypt group_vars/production/vault.yml

Редактировать без постоянной расшифровки:

ansible-vault edit group_vars/production/vault.yml

Посмотреть содержимое:

ansible-vault view group_vars/production/vault.yml

Изменить пароль:

ansible-vault rekey group_vars/production/vault.yml

Шифрование отдельной строки

ansible-vault encrypt_string --name 'vault_db_password'

После ввода секрета команда выдаст YAML-фрагмент:

vault_db_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  6638...

Его можно поместить в обычный YAML-файл рядом с незашифрованными переменными.

Передача секрета аргументом командной строки может оставить его в истории оболочки. Безопаснее вводить значение интерактивно или подавать через защищённый стандартный ввод.

Запуск playbook с Vault

Интерактивный запрос пароля:

ansible-playbook site.yml --ask-vault-pass

Пароль из файла:

ansible-playbook site.yml --vault-password-file .vault-password

Файл .vault-password нельзя добавлять в репозиторий:

.vault-password
*.vault-password

Ограничение прав:

chmod 600 .vault-password

--vault-password-file также может ссылаться на исполняемый скрипт, который получает пароль из защищённой системы.

Vault ID

Vault ID позволяет использовать разные пароли для разных окружений или команд:

ansible-vault encrypt \
  --vault-id production@prompt \
  group_vars/production/vault.yml

Запуск с несколькими идентификаторами:

ansible-playbook site.yml \
  --vault-id development@.vault-development \
  --vault-id production@prompt

Метка Vault ID помогает выбрать подходящий секрет, но безопасность определяется самим паролем и способом его хранения.

Разделение обычных и секретных переменных

group_vars/production/main.yml:

app_database_host: db.production.internal
app_database_user: my_app
app_database_password: "{{ vault_app_database_password }}"

group_vars/production/vault.yml:

vault_app_database_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  6638...

Префикс vault_ показывает, что исходное значение должно приходить из защищённого файла.

Защита вывода

- name: Выполнить запрос с токеном
  ansible.builtin.uri:
    url: https://api.example.com/deploy
    method: POST
    headers:
      Authorization: "Bearer {{ vault_api_token }}"
    status_code: 200
  no_log: true

no_log: true скрывает параметры и результат задачи в обычном выводе Ansible. Это полезно для секретов, но усложняет отладку. Кроме того, секрет может попасть в журналы внешней программы или сервиса, поэтому защита должна быть сквозной.

Практические правила Vault


Molecule — тестирование ролей

Molecule автоматизирует жизненный цикл тестового окружения роли:

создание экземпляра → подготовка → применение роли → проверка → повторное применение → удаление

Molecule помогает проверить:

Установка

Пример установки с Docker-драйвером:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install ansible-core molecule "molecule-plugins[docker]" ansible-lint

Проверка:

molecule --version
ansible --version

Для Docker-драйвера Docker Engine должен быть установлен и доступен текущему пользователю.

Molecule и его плагины развиваются отдельно. Синтаксис команды и набор автоматически создаваемых файлов могут немного различаться между версиями, поэтому версии зависимостей проекта лучше фиксировать.

Создание сценария

В каталоге роли:

molecule init scenario

Или создание новой роли с тестовым сценарием, если команда поддерживается установленной версией:

molecule init role my_app --driver-name docker

Типичная структура:

my_app/
├── defaults/
│   └── main.yml
├── handlers/
│   └── main.yml
├── tasks/
│   └── main.yml
├── templates/
│   └── app.conf.j2
└── molecule/
    └── default/
        ├── converge.yml
        ├── molecule.yml
        ├── prepare.yml
        └── verify.yml

default — имя сценария. У роли может быть несколько сценариев, например default, debian, upgrade или cluster.

molecule.yml

Пример конфигурации Docker:

---
dependency:
  name: galaxy

driver:
  name: docker

platforms:
  - name: instance
    image: docker.io/python:3.12-slim
    pre_build_image: true

provisioner:
  name: ansible

verifier:
  name: ansible

Минимальный образ должен содержать компоненты, необходимые для подключения и выполнения модулей. Если роль управляет systemd, сетью, ядром или привилегированными ресурсами, обычного контейнера может быть недостаточно. Тогда используют подготовленный systemd-образ, дополнительные параметры контейнера или виртуальные машины.

Несколько платформ

platforms:
  - name: debian-instance
    image: docker.io/debian:12-slim
    pre_build_image: true

  - name: ubuntu-instance
    image: docker.io/ubuntu:24.04
    pre_build_image: true

Для минимальных образов может понадобиться prepare.yml, устанавливающий Python и другие зависимости. Практичнее использовать специально подготовленные тестовые образы и фиксировать их версии или digest.

converge.yml

Этот playbook применяет тестируемую роль:

---
- name: Converge
  hosts: all
  become: true
  gather_facts: true

  roles:
    - role: my_app
      my_app_port: 8080
      my_app_log_level: debug

Если роль подключается через include_role:

---
- name: Converge
  hosts: all
  become: true

  tasks:
    - name: Применить роль my_app
      ansible.builtin.include_role:
        name: my_app
      vars:
        my_app_port: 8080

prepare.yml

prepare.yml выполняет предварительную подготовку экземпляров:

---
- name: Prepare
  hosts: all
  become: true

  tasks:
    - name: Установить вспомогательные пакеты
      ansible.builtin.package:
        name:
          - ca-certificates
          - curl
        state: present

Подготовка не должна незаметно реализовывать обязанности тестируемой роли. Иначе тест может пройти только потому, что нужное состояние создал prepare.yml.

verify.yml

Проверка встроенными модулями Ansible:

---
- name: Verify
  hosts: all
  become: true
  gather_facts: false

  tasks:
    - name: Получить сведения о конфигурации
      ansible.builtin.stat:
        path: /etc/my_app/app.conf
      register: app_config

    - name: Проверить файл конфигурации
      ansible.builtin.assert:
        that:
          - app_config.stat.exists
          - app_config.stat.isreg
          - app_config.stat.mode == '0640'
        fail_msg: Конфигурация приложения создана неверно

    - name: Прочитать конфигурацию
      ansible.builtin.slurp:
        src: /etc/my_app/app.conf
      register: app_config_content

    - name: Проверить порт в конфигурации
      ansible.builtin.assert:
        that:
          - "'port = 8080' in (app_config_content.content | b64decode)"

stat, slurp, uri, service_facts, package_facts и assert позволяют писать проверки без отдельного тестового фреймворка.

Основные команды Molecule

Создать экземпляры:

molecule create

Применить роль:

molecule converge

Выполнить проверки:

molecule verify

Проверить повторный запуск:

molecule idempotence

Войти в тестовый экземпляр:

molecule login

Удалить экземпляры:

molecule destroy

Запустить полный тестовый цикл:

molecule test

Проверить определённый сценарий:

molecule test --scenario-name default

Последовательность molecule test

Полный тест обычно включает этапы, подобные следующим:

  1. удаление старого окружения;
  2. проверка зависимостей и синтаксиса;
  3. создание экземпляров;
  4. подготовка экземпляров;
  5. применение роли;
  6. проверка идемпотентности;
  7. проверка итогового состояния;
  8. очистка и удаление экземпляров.

Точный порядок зависит от версии Molecule и настроек test_sequence.

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

scenario:
  test_sequence:
    - dependency
    - cleanup
    - destroy
    - syntax
    - create
    - prepare
    - converge
    - idempotence
    - verify
    - cleanup
    - destroy

Разработка без полного пересоздания

Во время работы быстрее использовать:

molecule create
molecule converge
molecule verify

После изменения роли повторять:

molecule converge
molecule verify

Перед завершением работы запускать полный цикл:

molecule test

molecule test обычно уничтожает окружение в конце, поэтому для отладки удобнее раздельные команды.

Проверка идемпотентности в Molecule

Этап idempotence повторно применяет роль и ожидает отсутствие изменений. Если задача каждый раз возвращает changed: true, тест завершится ошибкой.

Типичные причины:

Проверки негативных сценариев

Можно создать отдельный сценарий, который проверяет ошибку при неверных входных данных:

molecule/
├── default/
└── invalid_port/

В роли:

- name: Проверить значение порта
  ansible.builtin.assert:
    that:
      - my_app_port | int > 0
      - my_app_port | int < 65536
    fail_msg: my_app_port должен быть в диапазоне 1–65535

Такой сценарий должен явно учитывать ожидаемую ошибку, а не просто завершаться неуспешно без проверки причины.

Molecule в CI

Типичная последовательность CI:

python -m pip install -r requirements-test.txt
ansible-galaxy install -r requirements.yml
ansible-lint
molecule test

requirements-test.txt:

ansible-core==2.17.*
molecule==24.*
molecule-plugins[docker]
ansible-lint

Это только пример стратегии фиксации. Реальные версии следует выбирать совместимыми между собой и обновлять контролируемо.

Для нескольких платформ CI может использовать матрицу:

сценарий default × поддерживаемые версии Ansible × тестовые образы ОС

Не нужно без необходимости тестировать все комбинации. Обычно выбирают минимальную и максимальную поддерживаемые версии и основные целевые ОС.


Общий пример роли с Molecule

Ниже приведена компактная роль, создающая конфигурационный файл. Она не требует systemd и подходит для базового контейнерного теста.

Структура

my_app_config/
├── defaults/
│   └── main.yml
├── tasks/
│   └── main.yml
├── templates/
│   └── app.conf.j2
└── molecule/
    └── default/
        ├── converge.yml
        ├── molecule.yml
        └── verify.yml

defaults/main.yml

---
my_app_config_path: /etc/my_app.conf
my_app_host: 0.0.0.0
my_app_port: 8080
my_app_log_level: info
my_app_features:
  - metrics

tasks/main.yml

---
- name: Проверить параметры роли
  ansible.builtin.assert:
    that:
      - my_app_port | int > 0
      - my_app_port | int < 65536
      - my_app_log_level in ['debug', 'info', 'warning', 'error']
    fail_msg: Параметры my_app заданы неверно

- name: Создать конфигурацию приложения
  ansible.builtin.template:
    src: app.conf.j2
    dest: "{{ my_app_config_path }}"
    owner: root
    group: root
    mode: "0644"

templates/app.conf.j2

# Managed by Ansible

[server]
host = {{ my_app_host }}
port = {{ my_app_port }}

[logging]
level = {{ my_app_log_level }}

[features]
{% for feature in my_app_features %}
{{ feature }} = enabled
{% endfor %}

molecule/default/molecule.yml

---
dependency:
  name: galaxy

driver:
  name: docker

platforms:
  - name: instance
    image: docker.io/python:3.12-slim
    pre_build_image: true

provisioner:
  name: ansible

verifier:
  name: ansible

molecule/default/converge.yml

---
- name: Converge
  hosts: all
  become: true
  gather_facts: false

  roles:
    - role: my_app_config
      my_app_port: 9000
      my_app_log_level: warning
      my_app_features:
        - metrics
        - audit

molecule/default/verify.yml

---
- name: Verify
  hosts: all
  become: true
  gather_facts: false

  tasks:
    - name: Получить сведения о файле
      ansible.builtin.stat:
        path: /etc/my_app.conf
      register: config_file

    - name: Проверить атрибуты файла
      ansible.builtin.assert:
        that:
          - config_file.stat.exists
          - config_file.stat.isreg
          - config_file.stat.mode == '0644'
          - config_file.stat.pw_name == 'root'
          - config_file.stat.gr_name == 'root'

    - name: Прочитать файл
      ansible.builtin.slurp:
        src: /etc/my_app.conf
      register: config_content

    - name: Декодировать содержимое
      ansible.builtin.set_fact:
        rendered_config: "{{ config_content.content | b64decode }}"

    - name: Проверить содержимое
      ansible.builtin.assert:
        that:
          - "'port = 9000' in rendered_config"
          - "'level = warning' in rendered_config"
          - "'metrics = enabled' in rendered_config"
          - "'audit = enabled' in rendered_config"

Запуск

molecule test

Ожидаемый результат:

Если имя роли или способ её обнаружения отличается от примера, необходимо скорректировать role в converge.yml или настроить путь ролей.


Практические рекомендации

Пишите декларативные задачи

Предпочтительно:

- name: Обеспечить наличие каталога
  ansible.builtin.file:
    path: /opt/my_app
    state: directory
    mode: "0755"

Вместо:

- name: Создать каталог командой
  ansible.builtin.command: mkdir -p /opt/my_app

Специализированный модуль лучше сообщает об изменениях, поддерживает check mode и обычно уже реализует идемпотентность.

Используйте понятные имена задач

Хорошо:

- name: Установить конфигурацию API

Менее информативно:

- name: Copy file

Имя задачи должно помогать понять назначение без чтения всех аргументов.

Указывайте права явно

- name: Создать файл настроек
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/my_app/app.conf
    owner: root
    group: my_app
    mode: "0640"

Режим рекомендуется записывать строкой, чтобы YAML не интерпретировал значение неожиданным образом.

Проверяйте конфигурации до применения

Некоторые модули поддерживают validate:

- name: Установить конфигурацию sudo
  ansible.builtin.template:
    src: my_app.sudoers.j2
    dest: /etc/sudoers.d/my_app
    owner: root
    group: root
    mode: "0440"
    validate: /usr/sbin/visudo -cf %s

Если проверка завершится ошибкой, целевой файл не будет заменён некорректной версией.

Разделяйте данные и логику

Не подавляйте изменения без причины

changed_when: false

следует применять к операциям чтения или при наличии точного собственного критерия. Если поставить его на реально изменяющую задачу, handlers и проверка идемпотентности могут перестать отражать действительность.

Используйте линтер

ansible-lint

Линтер помогает выявлять проблемы со стилем, FQCN, именами задач, обработкой команд и другими типичными ошибками.

Тестируйте результат, а не реализацию

Хрупкая проверка подтверждает, что задача была вызвана. Полезная проверка подтверждает состояние:

Фиксируйте зависимости

Храните требования в файлах:

requirements.yml
requirements-test.txt

Это уменьшает различия между рабочими станциями и CI.

Не храните секреты в логах

Используйте Vault, no_log: true, защищённые переменные CI и минимальные права доступа. Перед добавлением отладочного debug проверяйте, не содержит ли объект секретных полей.


Краткая шпаргалка

Ansible

# Проверить доступность
ansible all -m ansible.builtin.ping

# Показать инвентарь
ansible-inventory --graph

# Проверить синтаксис
ansible-playbook site.yml --syntax-check

# Пробный запуск с разницей файлов
ansible-playbook site.yml --check --diff

# Запустить playbook
ansible-playbook site.yml

# Ограничить группу
ansible-playbook site.yml --limit web

# Запустить по тегу
ansible-playbook site.yml --tags nginx

# Открыть документацию модуля
ansible-doc ansible.builtin.template

# Установить зависимости
ansible-galaxy install -r requirements.yml

# Проверить стиль
ansible-lint

Vault

# Создать зашифрованный файл
ansible-vault create secrets.yml

# Зашифровать файл
ansible-vault encrypt secrets.yml

# Редактировать файл
ansible-vault edit secrets.yml

# Посмотреть файл
ansible-vault view secrets.yml

# Изменить пароль
ansible-vault rekey secrets.yml

# Запустить playbook с запросом пароля
ansible-playbook site.yml --ask-vault-pass

Molecule

# Создать окружение
molecule create

# Применить роль
molecule converge

# Проверить результат
molecule verify

# Проверить идемпотентность
molecule idempotence

# Войти в экземпляр
molecule login

# Удалить окружение
molecule destroy

# Выполнить полный цикл
molecule test

Типичный цикл разработки роли

ansible-lint
molecule create
molecule converge
molecule verify
molecule idempotence
molecule destroy

Перед отправкой изменений:

ansible-lint && molecule test