# Подробное руководство по файлам Molecule **Автор:** Сергей Антропов **Сайт:** https://devops.org.ru ## 📋 Обзор Molecule - это инструмент для тестирования Ansible ролей в изолированных окружениях. В проекте AnsibleTemplate используется универсальная конфигурация Molecule с поддержкой множества операционных систем и различных preset'ов для тестирования. ### 🔧 Fallback значения **Важная особенность:** Все файлы Molecule содержат fallback значения, что обеспечивает работоспособность системы даже без preset файлов. Это означает, что: - **Система всегда работает** - даже если preset файл не найден - **Быстрый старт** - можно запустить тестирование без настройки - **Надежность** - меньше точек отказа в системе - **Отладка** - легче диагностировать проблемы с preset'ами ## 🏗️ Структура файлов Molecule ``` molecule/ ├── default/ # Основная конфигурация Molecule │ ├── molecule.yml # Главный конфигурационный файл │ ├── create.yml # Создание тестовых контейнеров │ ├── converge.yml # Выполнение ролей в контейнерах │ ├── verify.yml # Проверка результатов тестирования │ ├── destroy.yml # Удаление тестовых контейнеров │ └── site.yml # Основной playbook для тестирования └── presets/ # Preset конфигурации для разных сценариев ├── minimal.yml # Минимальный preset (1 хост) ├── performance.yml # Performance preset (12 хостов) ├── security.yml # Security preset (10 хостов) ├── etcd-patroni.yml # etcd-patroni preset └── ... # Другие preset'ы ``` ## 📄 Детальное описание файлов ### 1. `molecule/default/molecule.yml` - Главный конфигурационный файл **Назначение:** Основная конфигурация Molecule с настройками драйвера, платформ, provisioner'а и verifier'а. #### Основные секции: **Driver (Драйвер):** ```yaml driver: name: docker ``` - **Назначение:** Определяет использование Docker в качестве драйвера - **Функция:** Создание и управление тестовыми контейнерами **Platforms (Платформы):** ```yaml platforms: - name: placeholder image: ghcr.io/ansible-community/molecule-ubuntu-systemd:jammy pre_build_image: true - name: ansible-controller image: inecs/ansible-lab:ansible-controller-latest pre_build_image: true # ... другие образы ``` - **Назначение:** Определяет доступные Docker образы для тестирования - **Поддерживаемые ОС:** Ubuntu, Debian, RHEL, CentOS, AlmaLinux, Rocky Linux, Alt Linux, Astra Linux, RedOS - **Собственные образы:** AnsibleTemplate создает собственные образы для тестирования **Provisioner (Провижнер):** ```yaml provisioner: name: ansible config_options: defaults: stdout_callback: yaml env: ANSIBLE_STDOUT_CALLBACK: yaml inventory: links: hosts: "${MOLECULE_EPHEMERAL_DIRECTORY}/inventory/hosts.ini" playbooks: create: create.yml converge: converge.yml destroy: destroy.yml ``` - **Назначение:** Настройка Ansible как provisioner'а - **Функции:** - Настройка вывода в YAML формате - Связывание инвентори файла - Определение playbook'ов для разных этапов **Dependency (Зависимости):** ```yaml dependency: name: galaxy ``` - **Назначение:** Установка зависимостей через Ansible Galaxy - **Функция:** Автоматическая установка коллекций из `requirements.yml` **Verifier (Верификатор):** ```yaml verifier: name: ansible ``` - **Назначение:** Использование Ansible для проверки результатов - **Функция:** Выполнение `verify.yml` для проверки состояния системы **Lint (Линтер):** ```yaml lint: |- set -e ansible-lint /workspace/roles/ ``` - **Назначение:** Проверка синтаксиса ролей - **Функция:** Запуск `ansible-lint` для всех ролей в директории `roles/` ### 2. `molecule/default/create.yml` - Создание тестовых контейнеров **Назначение:** Создание и настройка тестовых контейнеров согласно выбранному preset'у с fallback значениями. #### Переменные и конфигурация: **Fallback значения (по умолчанию):** ```yaml vars: # Получаем preset из переменной окружения или используем default preset_name: "{{ lookup('env', 'MOLECULE_PRESET') | default('default') }}" preset_file: "/workspace/molecule/presets/{{ preset_name }}.yml" # Fallback значения если preset файл не найден docker_network: labnet generated_inventory: "{{ molecule_ephemeral_directory }}/inventory/hosts.ini" images: alt: "inecs/ansible-lab:alt-linux-latest" astra: "inecs/ansible-lab:astra-linux-latest" rhel: "inecs/ansible-lab:rhel-latest" centos: "inecs/ansible-lab:centos-latest" alma: "inecs/ansible-lab:alma-latest" rocky: "inecs/ansible-lab:rocky-latest" redos: "inecs/ansible-lab:redos-latest" ubuntu: "inecs/ansible-lab:ubuntu-latest" debian: "inecs/ansible-lab:debian-latest" systemd_defaults: privileged: true command: "/sbin/init" volumes: - "/sys/fs/cgroup:/sys/fs/cgroup:ro" tmpfs: ["/run", "/run/lock"] capabilities: ["SYS_ADMIN"] hosts: - name: u1 family: debian groups: [test] ``` - **Назначение:** Определение fallback значений для случаев когда preset файл не найден - **Функция:** Обеспечение работоспособности даже без preset файлов - **Образы:** Собственные образы AnsibleTemplate для всех поддерживаемых ОС - **Systemd настройки:** Стандартные настройки для systemd контейнеров #### Основные задачи: **Load preset configuration:** ```yaml - name: Load preset configuration include_vars: "{{ preset_file }}" when: preset_file is file ignore_errors: true ``` - **Назначение:** Загрузка конфигурации preset'а (перезаписывает fallback значения) - **Функция:** Динамическое определение тестового окружения - **Переменные:** `MOLECULE_PRESET` определяет какой preset использовать - **Fallback:** Если preset файл не найден, используются значения по умолчанию #### Преимущества fallback значений: 1. **Надежность:** Система работает даже без preset файлов 2. **Быстрый старт:** Можно запустить тестирование без настройки preset'ов 3. **Стандартизация:** Единые настройки для всех ОС 4. **Отладка:** Легче диагностировать проблемы с preset файлами 5. **Разработка:** Удобно для разработки новых preset'ов **Ensure network exists:** ```yaml - name: Ensure network exists community.docker.docker_network: name: "{{ docker_network }}" state: present ``` - **Назначение:** Создание Docker сети для тестирования - **Функция:** Обеспечение сетевого взаимодействия между контейнерами - **По умолчанию:** Сеть `labnet` **Pull systemd images:** ```yaml - name: Pull systemd images community.docker.docker_image: name: "{{ images[item.family] }}" source: pull loop: "{{ hosts | selectattr('type','undefined') | list }}" ``` - **Назначение:** Загрузка Docker образов для systemd контейнеров - **Функция:** Подготовка образов для создания контейнеров - **Поддержка:** Различные семейства ОС (debian, rhel, alt, astra) **Start systemd nodes:** ```yaml - name: Start systemd nodes community.docker.docker_container: name: "{{ item.name }}" image: "{{ images[item.family] }}" networks: - name: "{{ docker_network }}" privileged: "{{ systemd_defaults.privileged }}" command: "{{ systemd_defaults.command }}" volumes: "{{ systemd_defaults.volumes | default([]) + (item.volumes | default([])) }}" tmpfs: "{{ systemd_defaults.tmpfs | default([]) }}" capabilities: "{{ systemd_defaults.capabilities | default([]) }}" published_ports: "{{ item.publish | default([]) }}" env: "{{ item.env | default({}) }}" state: started restart_policy: unless-stopped ``` - **Назначение:** Создание и запуск systemd контейнеров - **Функции:** - Создание контейнеров с systemd поддержкой - Настройка привилегированного режима - Монтирование cgroup для systemd - Настройка tmpfs для /run - Публикация портов - Настройка переменных окружения **Start DinD nodes (Docker-in-Docker):** ```yaml - name: Start DinD nodes (docker:27-dind) community.docker.docker_container: name: "{{ item.name }}" image: "docker:27-dind" networks: - name: "{{ docker_network }}" privileged: true env: DOCKER_TLS_CERTDIR: "" published_ports: "{{ item.publish | default([]) }}" volumes: "{{ (item.volumes | default([])) + [item.name + '-docker:/var/lib/docker'] }}" state: started restart_policy: unless-stopped ``` - **Назначение:** Создание Docker-in-Docker контейнеров - **Функции:** - Тестирование Docker Compose - Тестирование Dockerfile'ов - Изолированная Docker среда - Отдельные volumes для Docker данных **Start DOoD nodes (Docker-out-of-Docker):** ```yaml - name: Start DOoD nodes (systemd + docker.sock mount) community.docker.docker_container: name: "{{ item.name }}" image: "{{ images[item.family] }}" networks: - name: "{{ docker_network }}" privileged: "{{ systemd_defaults.privileged }}" command: "{{ systemd_defaults.command }}" volumes: "{{ (systemd_defaults.volumes | default([])) + ['/var/run/docker.sock:/var/run/docker.sock'] + (item.volumes | default([])) }}" tmpfs: "{{ systemd_defaults.tmpfs | default([]) }}" capabilities: "{{ systemd_defaults.capabilities | default([]) }}" published_ports: "{{ item.publish | default([]) }}" env: "{{ item.env | default({}) }}" state: started restart_policy: unless-stopped ``` - **Назначение:** Создание Docker-out-of-Docker контейнеров - **Функции:** - Доступ к Docker daemon хоста - Тестирование Docker операций - Комбинация systemd + Docker **Build groups map:** ```yaml - name: Initialize groups map set_fact: groups_map: {} - name: Append hosts to groups set_fact: groups_map: "{{ groups_map | combine({ item_group: (groups_map[item_group] | default([])) + [item_name] }) }}" loop: "{{ hosts | subelements('groups', skip_missing=True) }}" ``` - **Назначение:** Создание карты групп для инвентори - **Функция:** Группировка хостов по назначению (servers, database, cache, etc.) **Render inventory ini:** ```yaml - name: Render inventory ini set_fact: inv_content: | [all:vars] ansible_connection=community.docker.docker ansible_python_interpreter=/usr/bin/python3 {% for group, members in (groups_map | dictsort) %} [{{ group }}] {% for h in members %}{{ h }} {% endfor %} {% endfor %} [all] {% for h in hosts %}{{ h.name }} {% endfor %} ``` - **Назначение:** Генерация инвентори файла - **Функция:** Создание динамического инвентори для Ansible - **Формат:** INI формат с группами и переменными **Write inventory file:** ```yaml - name: Write inventory file copy: dest: "{{ generated_inventory }}" content: "{{ inv_content }}" mode: "0644" ``` - **Назначение:** Сохранение инвентори файла - **Функция:** Запись сгенерированного инвентори в файл **Display inventory summary:** ```yaml - name: Display inventory summary debug: msg: | 📋 Inventory Summary: - Total hosts: {{ hosts | length }} - Groups: {{ groups_map.keys() | list | join(', ') }} - Systemd nodes: {{ hosts | selectattr('type','undefined') | list | length }} - DinD nodes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list | length }} - DOoD nodes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dood') | list | length }} ``` - **Назначение:** Отображение сводки по созданным контейнерам - **Функция:** Информация о количестве и типах контейнеров ### 3. `molecule/default/converge.yml` - Выполнение ролей **Назначение:** Выполнение Ansible ролей в созданных контейнерах. #### Основные задачи: **Load preset configuration:** ```yaml - name: Load preset configuration include_vars: "{{ preset_file }}" when: preset_file is file ignore_errors: true ``` - **Назначение:** Загрузка конфигурации preset'а - **Функция:** Применение настроек preset'а для выполнения **Preflight vault — normalize state:** ```yaml - name: Preflight vault — normalize state (encrypt if plaintext, then decrypt) community.docker.docker_container_exec: container: ansible-controller command: > bash -lc ' set -euo pipefail; shopt -s nullglob globstar; for p in {{ vault_targets | map('quote') | join(' ') }}; do for f in $p; do [ -f "$f" ] || continue; if head -n1 "$f" | grep -q "^\$ANSIBLE_VAULT;"; then echo "[vault] already encrypted: $f"; else echo "[vault] plaintext -> encrypt: $f"; ansible-vault encrypt --encrypt-vault-id default --vault-password-file /workspace/vault/.vault "$f"; fi echo "[vault] decrypt for run: $f"; ansible-vault decrypt --vault-password-file /workspace/vault/.vault "$f"; done done ' ``` - **Назначение:** Подготовка vault файлов для выполнения - **Функции:** - Шифрование незашифрованных файлов - Расшифровка файлов для выполнения - Нормализация состояния vault файлов **Run lab playbook:** ```yaml - name: Run lab playbook community.docker.docker_container_exec: container: ansible-controller command: > bash -lc " ANSIBLE_ROLES_PATH=/workspace/roles ansible-playbook -i {{ lookup('env','MOLECULE_EPHEMERAL_DIRECTORY') }}/inventory/hosts.ini /workspace/molecule/default/site.yml " ``` - **Назначение:** Выполнение основного playbook'а - **Функции:** - Запуск `site.yml` в ansible-controller контейнере - Использование сгенерированного инвентори - Установка пути к ролям **Post-run — re-encrypt secrets:** ```yaml - name: Post-run — re-encrypt secrets community.docker.docker_container_exec: container: ansible-controller command: > bash -lc ' set -euo pipefail; shopt -s nullglob globstar; for p in {{ vault_targets | map('quote') | join(' ') }}; do for f in $p; do [ -f "$f" ] || continue; if head -n1 "$f" | grep -q "^\$ANSIBLE_VAULT;"; then echo "[vault] ok (encrypted): $f"; else echo "[vault] encrypt back: $f"; ansible-vault encrypt --encrypt-vault-id default --vault-password-file /workspace/vault/.vault "$f" || true; fi done done ' ignore_errors: true ``` - **Назначение:** Повторное шифрование секретов после выполнения - **Функция:** Обеспечение безопасности vault файлов ### 4. `molecule/default/verify.yml` - Проверка результатов **Назначение:** Проверка состояния системы после выполнения ролей. #### Основные задачи: **Load preset configuration:** ```yaml - name: Load preset configuration include_vars: "{{ preset_file }}" when: preset_file is file ignore_errors: true ``` - **Назначение:** Загрузка конфигурации preset'а - **Функция:** Применение настроек для проверки **Check systemd nodes status:** ```yaml - name: Check systemd nodes status community.docker.docker_container_exec: container: "{{ item.name }}" command: systemctl is-system-running loop: "{{ hosts | selectattr('type','undefined') | list }}" register: systemd_status ignore_errors: true ``` - **Назначение:** Проверка состояния systemd в контейнерах - **Функция:** Убедиться что systemd работает корректно **Check DinD nodes docker daemon:** ```yaml - name: Check DinD nodes docker daemon community.docker.docker_container_exec: container: "{{ item.name }}" command: docker version --format '{{.Server.Version}}' loop: "{{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list }}" register: dind_status ignore_errors: true ``` - **Назначение:** Проверка Docker daemon в DinD контейнерах - **Функция:** Убедиться что Docker работает в контейнерах **Check DOoD nodes docker access:** ```yaml - name: Check DOoD nodes docker access community.docker.docker_container_exec: container: "{{ item.name }}" command: docker ps --format '{{.Names}}' loop: "{{ hosts | selectattr('type','defined') | selectattr('type','equalto','dood') | list }}" register: dood_status ignore_errors: true ``` - **Назначение:** Проверка доступа к Docker в DOoD контейнерах - **Функция:** Убедиться что контейнеры могут обращаться к Docker daemon **Test network connectivity between nodes:** ```yaml - name: Test network connectivity between nodes community.docker.docker_container_exec: container: "{{ item.0.name }}" command: ping -c 1 {{ item.1.name }} loop: "{{ hosts | subelements(hosts, 'name') }}" when: item.0.name != item.1.name register: ping_results ignore_errors: true ``` - **Назначение:** Проверка сетевого взаимодействия между контейнерами - **Функция:** Убедиться что контейнеры могут общаться друг с другом **Check published ports:** ```yaml - name: Check published ports community.docker.docker_container_exec: container: "{{ item.name }}" command: netstat -tlnp loop: "{{ hosts | selectattr('publish','defined') | list }}" register: port_status ignore_errors: true ``` - **Назначение:** Проверка опубликованных портов - **Функция:** Убедиться что порты доступны **Display verification summary:** ```yaml - name: Display verification summary debug: msg: | ✅ Verification Summary: - Total hosts: {{ hosts | length }} - Systemd nodes: {{ hosts | selectattr('type','undefined') | list | length }} - DinD nodes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list | length }} - DOoD nodes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dood') | list | length }} - Groups: {{ groups_map.keys() | list | join(', ') }} - Network: {{ docker_network }} ``` - **Назначение:** Отображение итоговой сводки проверки - **Функция:** Информация о состоянии всех компонентов ### 5. `molecule/default/destroy.yml` - Удаление контейнеров **Назначение:** Очистка тестовых контейнеров и ресурсов. #### Основные задачи: **Load preset configuration:** ```yaml - name: Load preset configuration include_vars: "{{ preset_file }}" when: preset_file is file ignore_errors: true ``` - **Назначение:** Загрузка конфигурации preset'а - **Функция:** Определение какие ресурсы нужно удалить **Stop and remove containers:** ```yaml - name: Stop and remove containers community.docker.docker_container: name: "{{ item.name }}" state: absent force_kill: true loop: "{{ hosts }}" ignore_errors: true ``` - **Назначение:** Остановка и удаление всех контейнеров - **Функция:** Полная очистка тестовых контейнеров **Remove DinD volumes:** ```yaml - name: Remove DinD volumes community.docker.docker_volume: name: "{{ item.name }}-docker" state: absent loop: "{{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list }}" ignore_errors: true ``` - **Назначение:** Удаление volumes для DinD контейнеров - **Функция:** Очистка Docker данных **Remove custom volumes:** ```yaml - name: Remove custom volumes community.docker.docker_volume: name: "{{ item.volumes | default([]) | select('match', '^[^:]+$') | list }}" state: absent loop: "{{ hosts }}" ignore_errors: true when: item.volumes is defined ``` - **Назначение:** Удаление пользовательских volumes - **Функция:** Очистка дополнительных volumes **Remove network:** ```yaml - name: Remove network community.docker.docker_network: name: "{{ docker_network }}" state: absent ignore_errors: true ``` - **Назначение:** Удаление Docker сети - **Функция:** Очистка сетевых ресурсов **Display cleanup summary:** ```yaml - name: Display cleanup summary debug: msg: | 🧹 Cleanup Summary: - Removed containers: {{ hosts | length }} - Removed DinD volumes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list | length }} - Network: {{ docker_network }} ``` - **Назначение:** Отображение сводки по очистке - **Функция:** Информация о удаленных ресурсах ### 6. `molecule/default/site.yml` - Основной playbook **Назначение:** Основной playbook для тестирования ролей. #### Структура: **Подготовка окружения:** - Обновление пакетов для всех ОС - Установка common tools - Настройка пользователей - Создание рабочих директорий **Импорт deploy.yml:** ```yaml - import_playbook: ../../roles/deploy.yml ``` - **Назначение:** Импорт playbook'а с ролями - **Функция:** Разделение логики подготовки и выполнения ролей ## 🎯 Preset файлы ### `molecule/presets/minimal.yml` - Минимальный preset **Назначение:** Быстрое тестирование с одним хостом. **Характеристики:** - **Количество хостов:** 1 - **ОС:** Debian - **Группы:** test - **Использование:** Быстрая проверка ролей ### `molecule/presets/performance.yml` - Performance preset **Назначение:** Нагрузочное тестирование с множеством хостов. **Характеристики:** - **Количество хостов:** 12 - **Серверы:** 5 узлов (web, app) - **База данных:** 3 узла - **Кэш:** 3 узла Redis - **Load balancer:** 1 узел HAProxy - **DinD:** 1 узел для Docker Compose **Группы:** - `servers` - веб-серверы - `database` - базы данных - `cache` - кэш серверы - `loadbalancer` - балансировщик нагрузки - `apps` - приложения ### `molecule/presets/security.yml` - Security preset **Назначение:** Тестирование безопасности с изолированными сетями. **Характеристики:** - **Количество хостов:** 10 - **Bastion хосты:** 2 (точки входа) - **Внутренние серверы:** 3 - **База данных:** 2 (изолированная сеть) - **Мониторинг:** 2 - **Firewall:** 2 - **DOoD:** 1 для Docker безопасности **Группы:** - `bastion` - точки входа - `internal` - внутренние серверы - `database` - базы данных - `monitoring` - мониторинг - `firewall` - сетевые компоненты - `security` - компоненты безопасности ## 🚀 Использование ### Базовые команды: ```bash # Тестирование с минимальным preset'ом make role test minimal # Тестирование с performance preset'ом make role test performance # Тестирование с security preset'ом make role test security # Тестирование конкретной роли make role test minimal ping # Тестирование без preset'а (используются fallback значения) make role test # Тестирование с несуществующим preset'ом (используются fallback значения) MOLECULE_PRESET=nonexistent make role test ``` ### Продвинутое использование: ```bash # Тестирование с кастомным preset'ом MOLECULE_PRESET=custom make role test # Отладка с подробным выводом make role test minimal --verbose # Проверка только создания контейнеров molecule create -s default # Проверка только выполнения ролей molecule converge -s default # Проверка только верификации molecule verify -s default # Очистка контейнеров molecule destroy -s default ``` ## 🔧 Настройка ### Создание собственного preset'а: ```yaml # molecule/presets/custom.yml --- docker_network: labnet generated_inventory: "{{ molecule_ephemeral_directory }}/inventory/hosts.ini" images: ubuntu: "inecs/ansible-lab:ubuntu-latest" rhel: "inecs/ansible-lab:rhel-latest" systemd_defaults: privileged: true command: "/sbin/init" volumes: - "/sys/fs/cgroup:/sys/fs/cgroup:ro" tmpfs: ["/run", "/run/lock"] capabilities: ["SYS_ADMIN"] hosts: - name: web1 family: ubuntu groups: [web, servers] publish: ["80:80"] - name: db1 family: rhel groups: [database, internal] ``` ### Использование fallback значений: ```bash # Тестирование без preset'а (используются fallback значения из create.yml) make role test # Тестирование с несуществующим preset'ом (используются fallback значения) MOLECULE_PRESET=nonexistent make role test # Проверка fallback значений molecule create -s default --debug ``` ### Использование кастомного preset'а: ```bash MOLECULE_PRESET=custom make role test ``` ## 🐛 Troubleshooting ### Проблемы с контейнерами: **Ошибка:** Контейнер не запускается **Решение:** Проверить доступность Docker образа и ресурсы системы **Ошибка:** Systemd не работает **Решение:** Убедиться что контейнер запущен с `privileged: true` и правильными volumes **Ошибка:** Сетевое взаимодействие не работает **Решение:** Проверить создание Docker сети и настройки firewall ### Проблемы с preset файлами: **Ошибка:** Preset файл не найден **Решение:** Система автоматически использует fallback значения из `create.yml` **Ошибка:** Неправильная конфигурация preset'а **Решение:** Проверить синтаксис YAML и доступность образов в preset файле **Ошибка:** Preset файл не загружается **Решение:** Убедиться что файл находится в `molecule/presets/` и имеет правильное имя ### Проблемы с ролями: **Ошибка:** Роль не найдена **Решение:** Проверить путь к роли в `roles/deploy.yml` **Ошибка:** Ошибка выполнения роли **Решение:** Проверить совместимость роли с ОС и зависимости ### Проблемы с vault: **Ошибка:** Не удается расшифровать vault файлы **Решение:** Проверить наличие файла `.vault` и правильность пароля **Ошибка:** Vault файлы остались расшифрованными **Решение:** Проверить права доступа и настройки vault ## 📊 Мониторинг ### Логи выполнения: ```bash # Просмотр логов Molecule molecule test -s default --debug # Логи конкретного этапа molecule create -s default --debug molecule converge -s default --debug molecule verify -s default --debug ``` ### Проверка состояния: ```bash # Статус контейнеров docker ps # Статус сети docker network ls # Статус volumes docker volume ls ``` ## 🎯 Лучшие практики 1. **Используйте подходящие preset'ы** для разных типов тестирования 2. **Проверяйте совместимость ролей** с различными ОС 3. **Используйте теги** для разделения задач в ролях 4. **Документируйте зависимости** ролей 5. **Тестируйте на разных preset'ах** перед коммитом 6. **Используйте vault** для секретных данных 7. **Очищайте ресурсы** после тестирования 8. **Используйте fallback значения** для быстрого старта без preset'ов 9. **Проверяйте доступность образов** перед созданием preset'ов 10. **Документируйте кастомные preset'ы** для команды ## 🔗 Связанные файлы - `roles/deploy.yml` - playbook с ролями - `inventory/hosts.ini` - статический инвентори - `vault/.vault` - пароль для vault - `requirements.yml` - зависимости Ansible - `Makefile` - команды для запуска тестов