Ansible и Molecule
Ansible — инструмент автоматизации, который описывает желаемое состояние серверов и приложений в декларативных YAML-сценариях. С его помощью устанавливают пакеты, создают пользователей и файлы, настраивают службы, разворачивают приложения и выполняют повторяемые операции на группах узлов.
Molecule — инструмент разработки и тестирования ролей Ansible. Он создаёт временные тестовые экземпляры, применяет к ним роль, проверяет результат и удаляет окружение.
Основной принцип Ansible:
инвентарь → playbook → задачи → модули → управляемые узлыМинимальная задача:
- name: Создать каталог приложения
ansible.builtin.file:
path: /opt/my_app
state: directory
owner: root
group: root
mode: "0755"name— понятное описание задачи.ansible.builtin.file— модуль Ansible в формате FQCN.path,state,owner,group,mode— аргументы модуля.- задача сообщает Ansible, какое состояние должно быть достигнуто, а не только какую команду нужно выполнить.
Комментарий в YAML:
# Это комментарийYAML чувствителен к отступам. Обычно используют два пробела и не применяют табуляцию.
Содержание
- Установка и базовые команды
- Инвентарь
- Playbooks
- Роли
- Модули Ansible
- Переменные и шаблоны Jinja2
- Идемпотентность
- Ansible Vault — шифрование секретов
- Molecule — тестирование ролей
- Общий пример роли с Molecule
- Практические рекомендации
- Краткая шпаргалка
Установка и базовые команды
Установка 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 = FalseAnsible ищет конфигурацию в нескольких местах. Локальный 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- или YAML-файл;
- динамическим — плагин, получающий сведения из облака, API или другой системы учёта;
- составным — несколько файлов и каталогов с разными источниками.
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=22web-01— имя узла внутри Ansible.ansible_host— фактический адрес подключения.[web]и[db]— группы узлов.[production:children]— группа, включающая другие группы.[all:vars]— переменные для всех узлов.
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.21YAML-формат удобен для вложенных структур и сложных значений.
Специальные группы
Ansible автоматически предоставляет группы:
all— все узлы инвентаря;ungrouped— узлы, не входящие ни в одну пользовательскую группу.
Переменные подключения
Часто используемые переменные:
| Переменная | Назначение |
|---|---|
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.ymlgroup_vars/all.yml:
app_user: my_app
app_root: /opt/my_appgroup_vars/web.yml:
http_port: 8080
worker_count: 4host_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.pingweb— узлы группыweb;web:&production— пересечение групп;all:!db— все узлы, кроме группыdb.
В командной оболочке шаблон лучше заключать в кавычки, чтобы специальные символы не интерпретировались оболочкой.
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: truehosts— целевая группа или шаблон узлов.become— выполнение задач с повышением привилегий.gather_facts— сбор фактов об ОС, сети, памяти и других параметрах.tasks— последовательность задач.
Начальная строка --- обозначает начало 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: absentblock— основной набор задач;rescue— задачи после ошибки внутри блока;always— задачи, выполняемые независимо от результата блока.
Теги
- 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: falseargv помогает безопасно передавать аргументы:
- name: Выполнить команду с аргументами
ansible.builtin.command:
argv:
- /usr/local/bin/my_app
- --config
- /etc/my_app/app.conf
changed_when: falseshell нужен, когда используются конвейеры, перенаправления, подстановка оболочки или её встроенные команды:
- 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.
Предпочтительный порядок выбора:
- специализированный идемпотентный модуль;
command;shell, только если необходимы функции оболочки;raw, только для особых случаев.
Ожидание и проверки
Ожидание порта:
- name: Дождаться запуска приложения
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ my_app_port }}"
timeout: 30HTTP-проверка:
- 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 много уровней приоритета. Практически важно помнить:
- значения из
defaults/main.ymlроли предназначены для переопределения; group_varsиhost_varsуточняют значения для окружений, групп и узлов;- переменные play, task и параметры подключения роли могут переопределять более общие значения;
--extra-varsимеет один из самых высоких приоритетов.
Вместо запоминания всей таблицы приоритетов полезно придерживаться стабильной схемы хранения и не определять одну переменную сразу во многих местах.
Имена переменных роли
Переменные публичной роли желательно снабжать уникальным префиксом:
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 подходит для:
- паролей;
- API-токенов;
- закрытых ключей и сертификатов;
- строк подключения;
- других конфиденциальных переменных.
Шифрование не отменяет контроль доступа. Пароль 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: trueno_log: true скрывает параметры и результат задачи в обычном выводе Ansible. Это полезно для секретов, но усложняет отладку. Кроме того, секрет может попасть в журналы внешней программы или сервиса, поэтому защита должна быть сквозной.
Практические правила Vault
- не хранить пароль Vault в том же репозитории;
- использовать разные секреты для разных окружений;
- ограничивать доступ к файлам паролей;
- включать
no_log: trueдля задач, способных вывести секрет; - не передавать секреты открытым текстом через аргументы процессов;
- регулярно менять секреты и отзывать неиспользуемые значения;
- в крупных системах рассмотреть специализированное внешнее хранилище секретов.
Molecule — тестирование ролей
Molecule автоматизирует жизненный цикл тестового окружения роли:
создание экземпляра → подготовка → применение роли → проверка → повторное применение → удалениеMolecule помогает проверить:
- синтаксис роли;
- успешность применения;
- итоговое состояние системы;
- идемпотентность;
- работу на нескольких дистрибутивах или версиях ОС;
- корректность зависимостей и handlers.
Установка
Пример установки с 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.ymldefault — имя сценария. У роли может быть несколько сценариев, например 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: ansibledependency— установка зависимостей роли;driver— способ создания экземпляров;platforms— тестовые экземпляры и их образы;provisioner— средство применения конфигурации;verifier— механизм проверки результата.
Минимальный образ должен содержать компоненты, необходимые для подключения и выполнения модулей. Если роль управляет 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: 8080prepare.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
Полный тест обычно включает этапы, подобные следующим:
- удаление старого окружения;
- проверка зависимостей и синтаксиса;
- создание экземпляров;
- подготовка экземпляров;
- применение роли;
- проверка идемпотентности;
- проверка итогового состояния;
- очистка и удаление экземпляров.
Точный порядок зависит от версии 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 testmolecule test обычно уничтожает окружение в конце, поэтому для отладки удобнее раздельные команды.
Проверка идемпотентности в Molecule
Этап idempotence повторно применяет роль и ожидает отсутствие изменений. Если задача каждый раз возвращает changed: true, тест завершится ошибкой.
Типичные причины:
- использование
shellилиcommandбез условий; - шаблон содержит текущее время или случайные значения;
- служба всегда получает
state: restartedв обычной задаче; - файл каждый раз генерируется в другом порядке;
- задача неверно определяет
changed_when; - пакет или артефакт запрашивается по постоянно меняющейся версии.
Проверки негативных сценариев
Можно создать отдельный сценарий, который проверяет ошибку при неверных входных данных:
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 testrequirements-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.ymldefaults/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:
- metricstasks/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: ansiblemolecule/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
- auditmolecule/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Ожидаемый результат:
- контейнер создан;
- роль применилась без ошибок;
- повторное применение не внесло изменений;
- файл
/etc/my_app.confсуществует с режимом0644; - конфигурация содержит переданные значения;
- тестовый контейнер удалён.
Если имя роли или способ её обнаружения отличается от примера, необходимо скорректировать 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Если проверка завершится ошибкой, целевой файл не будет заменён некорректной версией.
Разделяйте данные и логику
- задачи роли описывают действия;
defaultsопределяет публичные настройки;group_varsиhost_varsсодержат данные окружений;templatesформируют конфигурации;- Vault защищает секреты;
- Molecule проверяет ожидаемый результат.
Не подавляйте изменения без причины
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-lintVault
# Создать зашифрованный файл
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-passMolecule
# Создать окружение
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