Содержание
Ansible
Система управления автоматической конфигурации серверов. Контроль и отслеживание изменений в системе: версий установленных пакетов, файлов конфигурации, настроек ОС.
Пособие по Ansible
Документация
Список модулей
Task | Базовый блок. Обычно модуль с параметрами, выполняющий определённое действие. |
---|---|
Role | Типа функции - объединяет задачи, файлы, шаблоны, переменные и обработчики в единое целое. |
Play | Сущность, связывающая задачи и роли с целевыми серверами. |
Playbook | Файл yaml для запуска, где перечислены все нужные плеи. |
Модули используются в задачах конфигурации и запускаются на целевых серверах.
Плагины упрощают работу с Ansible на контрольной ноде — сервере, где запускается сам Ansible.
# копирование файла, используя модуль copy ansible localhost -m copy -a "src=file.txt dest=/tmp/file.txt"
Важное отличие от простого копирования через консоль или скрипт - идемпотентность, т. е., свойство функции или операции при повторном применении к объекту давать тот же результат, что и при первом. В данном случае, при втором запуске файл копироваться не будет, потому что ansible вычисляет контрольные суммы файлов и понимает, что ничего не изменилось. Можно, конечно, сделать проверки и в скрипте, но это трудоёмкий процесс, требующий изобретения разных подходов для разных задач.
Роли в Ansible - это список задач.
Плейбук - это список плеев, где перечисляются роли и к каким серверам они применяются.
ad-hoc команды
# k3 - имя целевого хоста, -m - модуль, -a - аргументы # Создать каталог (рекурсивно, аналог mkdir -p) ansible k3 -m file -a "path=/home/vasya/.config/systemd/user state=directory" # проверка доступа к хосту ansible k3 -m ping
Запуск команды без инвентори-файла
ansible all -i "k3.example.com," -m ping
https://docs.ansible.com/ansible/latest/command_guide/intro_adhoc.html
Задача
Базовый элемент.
# Cписок из 5 задач - name: Модуль maven_artifact скачивает приложение из Nexus maven_artifact: dest: "/opt/project/backend/lib/file.jar" repository_url: "https://nexus-srv/repository/project" group_id: "com.group.id" artifact_id: "backend" version: "0.1.0" - name: Добавление сервисного пользователя user: name: "serviceuser" create_home: no shell: /sbin/nologin - name: Шаблонизация конфигурации - управление настройками приложения с помощью переменных template: src: project-backend.service.j2 dest: /etc/systemd/system/project-backend.service - name: Перечитать конфигурацию systemd systemd: daemon_reload: yes - name: Запуск сервиса service: name: project-backend state: running
name
отображается в отчёте. После него идёт название модуля. Справка по модулю - ansible-doc <имя модуля>
.
Управление ошибками / кодами возврата, условия
fail / assert
Проверка, всё ли выполнилось так, как нужно.
fail
- инициировать сбой
assert
- убедиться, что условие выполняется (в некотором смысле, fail наоборот)
- hosts: 127.0.0.1 gather_facts: no connection: local vars: fail_via_fail: false fail_via_assert: false fail_via_complex_assert: true tasks: - name: Fail if conditions warrant a failure fail: msg: "Task failed" when: fail_via_fail - name: Fail if an assertion isn't validated assert: that: "fail_via_assert != true" - name: Assertions can have certain conditions assert: that: - "fail_via_fail != true" - "fail_via_assert != true" - "fail_via_complex_assert != true"
Debug
Вывод информации.
tasks: # Вывод переменной foo, свойства rc - debug: var=foo.rc # Лучше использовать такой синтаксис, т. к. если в свойстве будет дефис, то может быть ошибка. - debug: var=foo['rc'] # Вывод сообщения - debug: msg="Hello {{ foo['rc'] }}"
Handlers
Это дополнительные задачи, обработчики, вызываемые по запросу. Вызываются из задачи с помощью notify: <имя хэндлера>
, если только эта задача реально выполняется, т. е. что-то изменяется.
Хэндлеры выполняются в конце плейбука после всех задач, если нет спец. настроек.
- playbook_apache.yml
- name: Install Apache hosts: staging_servers become: yes vars: source: ~/for_site/index.html destination: /var/www/html tasks: - name: Install package: name: apache2 state: latest - name: Start & enable service: name: apache2 state: started enabled: yes - name: Copy index.html copy: src: "{{ source }}" dest: "{{ destination }}" mode: 0774 owner: www-data group: www-data notify: restart apache handlers: - name: restart apache service: name: apache2 state: restarted
Если нужно выполнить хэндлер сразу после какой-то задачи, не дожидаясь выполнения всех остальных задач, то сразу после неё ставится спецзадача
- name: Kick handlers immediately meta: flush_handlers
Т. к. по умолчанию обработчики выполняются в конце, есть шанс, что какая-то из задач, вызывающая хэндлер, выполнится, а затем какая-то из последующих задач завершится сбоем и обработчики уже не сработают, т. к. сбоем завершится весь плейбук. Чтобы форсировать выполнение хэндлеров в таком случае, нужно запускать команду с ключом –force-handlers
.
ansible-playbook -i hosts playbook.yml --force-handlers
Вызывать можно несколько обработчиков из одной задачи:
notify: - restart apache - restart memcached
А можно вызывать обработчик из другого обработчика, т. е., вызвать restart memcached из restart apache.
Роль
Набор задач, вызываемый как единый модуль или функция одной строкой с параметрами. Это каталог с упорядоченными по назначению YAML-файлами.
Файловая структура роли:
defaults/main.yml # значения переменных по умолчанию для роли. files/ # каталог с файлами, не требующими шаблонизации. handlers/main.yml # обработчики, запускаются только при получении соответствующих уведомлений (notify) от задач. meta/main.yml # метаданные с описанием авторов роли, зависимостей и версии. tasks/main.yml # задачи, запускаемые ролью. templates/ # каталог с jinja2-шаблонами. tests/ vars/main.yml # переменные для роли.
Генерация файловой структуры роли: ansible-galaxy init <role name>
.
Include / Import
Другая организация плейбука, где можно указать, откуда брать задачи/хэндлеры и т. д. Можно не создавать роль, а обойтись этим, если плейбук не очень большой. Если он большой, лучше использовать роли.
Здесь идёт ссылка на файл, куда вынесены соответствующие куски плейбука.
handlers: - import_tasks: handlers/apache.yml - import_tasks: handlers/app.yml tasks: - import_tasks: tasks/apache.yml - import_tasks: tasks/app.yml
Import отличается от include тем, что import сразу формирует файл плейбука при парсинге во время запуска с подстановкой переменных, а include выполняет эти файлы, когда доходит до них. Т. е., если в задаче используются какие-то переменные, значение которых формируется по ходу выполнения плейбука, то нужно использовать Include. Если это просто для выноса кусков кода для расхламления плейбука, то тогда Import.
После слов
import
и include
можно писать что угодно: import_aaa
или include_greatThings
Импортировать/включать можно и целый плейбук. Для этого include нужно разместить на верхнем уровне.
name: playbook1 hosts: all become:true tasks: - import: tasks/apache.yml - include: playbook2.yml
Плейбук
Связь задач/ролей с целевыми серверами.
- playbook.yaml
--- - name: Запуск backend сервиса project # Шаблон целевых хостов это группа хостов с именем backend hosts: backend # Список ansible-ролей для backend-серверов roles: - project-backend - name: Запуск frontend сервиса project # Шаблон целевых хостов это группа хостов с именем frontend hosts: frontend # Список ansible-ролей для frontend-серверов roles: - project-frontend
Запуск плейбука: ansible-playbook playbook.yaml
. Будет выполняться один плей за другим, запуская задачи ролей на указанных серверах.
Тэги/метки
Задачи можно помечать определённым словом, и тогда можно будет запускать плейбук только с выбранными задачами.
tasks: - name: ... shell: ... tags: - api - echo
Запустит только задачи с тэгом api.
ansible-playbook playbook.yml --tags=api
Inventory
Группы хостов берутся из inventory. Это в простом случае может быть текстовый файл со списком IP-адресов или имён серверов, а может быть и в формате yaml.
# Названия чувствительны к регистру - [staging_servers] и [staging_Servers] будут разными группами. # Все сервера из всех групп входят в группу all # можно писать сервера вообще без группы, они будут входить в группу ungrouped 192.168.1.1 192.168.1.2 server.example.com server2.example.com ansible_host=192.168.1.3 [staging_db] 192.168.1.51 192.168.1.52 [staging_web] 192.168.1.61 192.168.1.62 [staging_app] 192.168.1.71 192.168.1.72 # Группа staging_all, объединяющая группы staging_db, staging_db и staging_app [staging_all:children] staging_db staging_db staging_app [staging_servers] # чтобы не прописывать одинаковые значения ansible_user, ansible_ssh_private_key и т. д., можно прописать переменные # См. [staging_servers:vars] ниже #k2 ansible_host=192.168.1.22 ansible_user=user ansible_ssh_private_key=/home/user/.ssh/id_ed25519 #k3 ansible_host=192.168.1.23 ansible_user=user ansible_ssh_private_key=/home/user/.ssh/id_ed25519 k2 ansible_host=192.168.1.22 k3 ansible_host=192.168.1.23 # Vars лучше не прописывать в файле inventory, а держать отдельно, но так тоже можно [staging_servers:vars] ansible_user=user ansible_ssh_private_key=/home/user/.ssh/id_ed25519 [windows_servers] windows2012 ansible_host=192.168.1.10 windows2016 ansible_host=192.168.1.11 [windows_servers:vars] ansible_user = myadmin ansible_port = 5986 ansible_connection = winrm ansible_winrm_server_cert_validation = ignore
# Показать список хостов с принадлежащими им переменными ansible-inventory --list # Типа tree для файлов ansible-inventory --graph # проверка связи (inventory - hosts.txt, все хосты, модуль ping) ansible -i hosts.txt all -m ping
Проблема в том, что если на сервера раньше не заходили, запросит кучу подтверждений ssh fingerprint. Чтобы этого избежать, рисуем ansible.cfg, где заодно указываем inventory-файл по умолчанию.
- ansible.cfg
[defaults] host_key_checking = false inventory = ./hosts.txt
# проверка связи c учётом ansible.cfg ansible all -m ping
Формат yaml:
all: # Обязательный параметр (все сервера) children: backend: # (группа серверов backend) hosts: 192.168.3.4: dev-backend.example.com: frontend: hosts: 192.168.3.5: dev-frontend.example.com: dev: hosts: dev-backend.example.com: dev-frontend.example.com: prod: hosts: 192.168.3.4: 192.168.3.5:
После этого можно запускать плейбук с ограничением по группе, например, ansible-playbook playbook.yaml --limit dev
В файле inventory можно прописывать переменные, но это не рекомендуется:
all: children: backend: hosts: dev-backend.example.com: # Переменная только для этого хоста ansible_user: ansible vars: # Переменные для группы backend backend_version: 3.1.1 vars: # Переменные для всех хостов в inventory ansible_connection: ssh
Лучше создавать файлы с переменными в каталоге group_vars
рядом с playbook.yaml
, например, файл group_vars/all.yaml
будет выглядеть так:
ansible_connection: ssh
Для часто меняющейся инфраструктуры используется динамический inventory. В Ansible есть плагин динамической инвентаризации, который подключается к облачной платформе или API системы виртуализации и получает список серверов.
Переменные
Используются для управления поведением задач подобно параметрам функции в программировании.
Переменные могут быть определены в плейбуках, в inventory, внутри ролей или передаваться в командной строке запуска плейбука (т. н. extra vars).
В именах переменных могут использоваться только буквы, цифры (не первый символ) и подчёркивания.
# Extra vars имеют наивысший приоритет ansible-playbook playbook.yaml --extra-vars "my_var=value"
Переменные, указанные в файлах group_vars/*.yaml
, в свою очередь, имеют приоритет над переменными, заданными в Ansible-ролях.
https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#ansible-variable-precedence
https://docs.ansible.com/ansible/latest/reference_appendices/general_precedence.html#general-precedence-rules
Добавить переменную в ~/.bash_profile
.
Если строки с regexp:
нет, то она создастся. Если есть, то она изменится.
Если под regexp:
попадает несколько строк, то изменится последняя.
- name: Var playbook hosts: k3 tasks: - name: Add environment variable to the remote user's shell lineinfile: dest: "~/.bash_profile" regexp: '^ENV_VAR=' line: 'ENV_VAR=value' # Вывод переменной - name: Get the ENV_VAR value shell: 'source ~/.bash_profile && echo $ENV_VAR' # Если не указать bash, то может запуститься sh с ошибкой "stderr": "/bin/sh: 1: source: not found" args: executable: /bin/bash register: envvar # Либо в составе сообщения, либо просто вывести переменную - debug: msg="The value is {{ envvar.stdout }}" - debug: var=envvar.stdout
Общесистемные переменные нужно загонять в файл
/etc/environment
. Понадобится become: true
.
Местоположение переменных
Положим, есть плейбук со скачиванием файла и настройкой прокси-сервера.
- name: Var playbook hosts: k3 tasks: - name: Download a file get_url: url: http://ipv4.download.thinkbroadband.com/5MB.zip dest: /tmp environment: http_proxy: http://proxy.example.com https_proxy: https://proxy.example.com
Можно вынести переменные прокси на верхний уровень, тогда в задаче можно на них сослаться.
vars: proxy_vars: http_proxy: http://proxy.example.com https_proxy: https://proxy.example.com tasks: - name: ... environment: proxy_vars
А можно сделать так, что переменные будут действовать на все задачи:
environment: http_proxy: http://proxy.example.com https_proxy: https://proxy.example.com tasks: - name: ... - name: ...
Переменные из файла:
vars: key: value vars_files: - vars/main.yml
Задать переменную ansible_super_secret
из значения переменной окружения SUPER_SECRET
:
ansible_super_secret: "{{ lookup('env', 'SUPER_SECRET') }}"
Факты
Это переменные, которые формируются автоматически на основе сбора данных из системы. Это нужно, потому что в разных системах могут быть разные пакетные менеджеры, разная иерархия каталогов файловой системы и т. д.
При запуске плея, при первом подключении к хостам и до запуска указанных задач, Ansible запускает модуль setup, который выполняет анализ окружения целевого хоста и формирует набор переменных в словаре ansible_facts.
# принудительный запуск на локальной машине ansible localhost -m setup
Так как сбор фактов - операция ресурсоёмкая, применяются следующие техники для ускорения работы:
- Отключение - применяется в облаках, когда VM автоматически создана, но ОС внутри ещё не запустилась. Чтобы Ansible не выдал ошибку, даётся указание ждать подключения, а сбор данных выключить.
- hosts: development gather_facts: no tasks: - name: waiting for connection wait_for_connect
- Фильтрация - собирать только необходимую информацию.
- hosts: development gather_facts: no tasks: - name: wait for connection wait_for_connect - name: selected facts ansible.builtin.setup: filter: - 'ansible_distribution' - 'ansible_machine_id' - 'ansible_*_mb'
- Кэширование - настраивается в файле
ansible.cfg
.[inventory] cache = True cache_plugin = jsonfile cache_connection = ~/.cache/ansible cache_timeout = 3600
Шаблоны Jinja2
Чтобы избавить себя от возни с лишними строками и отступами в формируемом файле, нужно добавить в шаблон .j2
#jinja2: lstrip_blocks:True
First, I’ll give you an easy, by far more preferable, way of taming whitespace and then we’ll dig into the more involving methods.
So here it comes:
Always render with trim_blocks and lstrip_blocks options enabled.
That’s it, the big secret is out. Save yourself trouble and tell Jinja2 to apply trimming and stripping to all of the blocks.
Это движок для подстановки переменных или для динамического формирования конфигурационных файлов. Ссылка на переменную идёт в любом файле yaml в двойных фигурных скобках.
- name: Install package {{ package_name }} package: name: "{{ package_name }}"
Чтобы создавать файлы на основе шаблонов, используется модуль template.
- name: Create app backend service unit template: src: app-backend.service.j2 dest: /etc/systemd/system/app-backend.service
Если переменные бэкенда описаны в group_vars/backend.yml
:
backend_maven_version: 3.4.1 backend_service_user: app_user backend_report_directory: /opt/app/reports backend_lib_directory: /opt/app/lib/
То можно генерировать systemd-unit на основе шаблона app-backend.service.j2
:
[Unit] Description=app [Service] User={{ backend_service_user }} Restart=always Environment=REPORT_PATH={{ backend_report_directory }} ExecStart=/usr/bin/java -Xrs -jar {{ backend_lib_directory }}/sausage-store-{{ backend_maven_version }}.jar [Install] WantedBy=multi-user.target
Значение переменной в кавычках
Чтобы после обработки шаблона значение переменной было окружено кавычками, надо делать так:
datasource: url: "{{ datasource_url | quote }}"
Т. е., ставить кавычки сами по себе и дополнительно применять команду. Одни кавычки отбросятся, вторые останутся.
Копирование нескольких шаблонов одной задачей
- name: Copy templates template: src: "{{ item }}" dest: /tmp/{{ item | basename | regex_replace('\\.j2$', '') }} with_fileglob: - ../templates/*.j2
Note
with_fileglob
always operates from files/
, you can get to templates with ../templates/mytemplate/*
https://serverfault.com/questions/578544/deploying-a-folder-of-template-files-using-ansible
Копирование дерева шаблонов, с папками
# Скопируются все подпапки из templates/postgres - name: Create directories tree file: state: directory dest: "/tmp/{{ item.path }}" with_filetree: "../templates/postgres" when: item.state == "directory" # Скопируются все шаблоны из templates/postgres в соответствующие подпапки - name: Copy templates template: src: "{{ item.src }}" dest: "/tmp/{{ item.path | regex_replace('\\.j2$', '') }}" with_filetree: "../templates/postgres" when: item.state == "file"
https://stackoverflow.com/questions/41667864/can-the-templates-module-handle-multiple-templates-directories
https://docs.ansible.com/ansible/2.9/plugins/lookup/filetree.html
Условия
В зависимости от значения feature.enabled
вставляется та или иная строка.
Задача
- name: demo the template hosts: localhost gather_facts: false vars: setting: a_val feature: enabled: true another_setting: b_val tasks: - name: pause with render pause: prompt: "{{ lookup('template', 'demo.j2') }}"
Шаблон
setting = {{ setting }} {% if feature.enabled %} feature = True {% else %} feature = False {% endif %} another_setting = {{ another_setting }}
Встроенное условие
- name: demo the template hosts: localhost gather_facts: false vars: api: v2: true tasks: - name: pause with render debug: msg: "API = cinder{{ 'v2' if api.v2 else '' }}"
Выведет API = cinderv2
, если api.v2
имеет значение true
.
Цикл с фильтрацией элементов
Если в data_dirs есть список значений, он вставится в результате обработки шаблона.
- name: demo the template hosts: localhost gather_facts: false vars: data_dirs: - Ыыы - Зьзьзь - Жжжжж - ЪУЪ tasks: - name: pause with render pause: prompt: "{{ lookup('template', 'demo-for.j2') }}
Шаблон с отсевом значения /
, условием по счётчику цикла и контролем запятых в значении. Дефис перед процентом в конце строки препятствует печати лишней пустой строки.
# data dirs {% for dir in data_dirs if dir != "/" -%} {% if loop.first -%} data_dir = {{ dir }}{{ ',' if not loop.last else '' }} {% else -%} {{ dir }}{{ ',' if not loop.last else '' }} {% endif -%} {% else -%} # no data dirs found {% endfor -%}
Способы доступа к индексу цикла
Переменная | Описание |
---|---|
loop.index | Текущая итерация данного цикла (начиная с 1) |
loop.index0 | Текущая итерация данного цикла (начиная с 0) |
loop.revindex | Общее число итераций до окончания данного цикла (начиная с 1) |
loop.revindex0 | Общее число итераций до окончания данного цикла (начиная с 0) |
loop.first | Булево True если это первая итерация |
loop.last | Булево True если это последняя итерация |
loop.first | Общее число элементов в данной последовательности |
https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_loops.html#extended-loop-variables
Динамические переменные
Используются для нескольких ОС, где разные пути к конфигам и один и тот же пакет называется по-разному, например apache2 в Debian и httpd в CentOS. Создаётся несколько файлов с переменными:
- apache_RedHat.yml
apache_package: httpd apache_service: httpd apache_config_dir: /etc/httpd/conf.d
- apache_default.yml
apache_package: apache2 apache_service: apache2 apache_config_dir: /etc/apache2/sites-enabled
В плейбуке рисуется раздел с предварительным заданием (на уровне tasks), который читает первый совпадающий файл с переменными:
pre_tasks: # - debug: var=ansible_os_family - name: Load variable file include_vars: "{{ item }}" with_first_found: - "vars/apache_{{ ansible_os_family }}.yml" - "vars/apache_default.yml"
Вместо ansible_os_family
можно использовать ansible_distribution
.
ansible.cfg
Базовый способ настройки Ansible. В ansible.cfg определены директивы конфигурации для всего набора утилит (ansible, ansible-playbook, ansible-galaxy, ansible-vault и т. д.). В этом файле указывается расположение ролей, конфигурация inventory и имя пользователя для подключения к хостам. Пример:
[defaults] roles_path = roles inventory = inventory remote_user = ansible vault_password_file = .vault host_key_checking = False [privilege_escalation] become = true ; повышать привилегии (sudo)
Ansible поддерживает следующие способы передачи параметров конфигурации (по убыванию приоритета):
- Переменные (если значение переменной присвоено несколько раз — используется последнее)
- Значения из плейбуков
- Значения, установленные через аргументы командной строки
- Значения из файла конфигурации
ansible-config
покажет список всех параметров конфигурации, их стандартные значения и пояснения, откуда взялось то или иное значение.
Ansible ищет конфигурационные файлы в следующем порядке:
- Путь из переменной окружения ANSIBLE_CONFIG
./ansible.cfg
(файл в текущем каталоге)~/.ansible.cfg
(файл в домашнем каталоге)/etc/ansible/ansible.cfg
Используется только первый найденный файл, остальные игнорируются.
https://docs.ansible.com/ansible/devel/reference_appendices/config.html
Vault
Шифрованное хранилище для секретных данных - паролей, токенов и т. п. Командой ansible-vault
их можно зашифровать, а при запуске Ansible передать спец. пароль, который расшифрует данные в процессе работы:
ansible-vault encrypt_string "VerySecretPassword!@#$%" --name secret_pass
Вывод команды указывается в group_vars/<my_group>.yml
:
secret_pass: !vault | $ANSIBLE_VAULT;1.1;AES256 39836982658142836487543449583409549856387698734580384098152098347874095687623654 ... 8457
Передача пароля для расшифровки происходит в интерактивной сессии при запуске ansible-playbook с параметром --ask-vault-pass
:
ansible-playbook playbook.yaml --ask-vault-pass
При запуске Ansible в CI-системе этот способ не подойдёт и потребуется механизм передачи пароля без участия пользователя.
Для этого нужно сформировать файл с паролем на предыдущем шаге CI и указать его расположение в ansible.cfg
:
vault_password_file = .vault
Файл .vault
необходимо добавить в .gitignore
!
Структура проекта Ansible, коллекции
Пример структуры.
. ├── README.md ├── ansible.cfg ├── roles │ ├── backend │ └── frontend ├── group_vars │ ├── all.yaml │ ├── backend.yaml │ └── frontend.yaml ├── inventory │ ├── static.yaml │ └── cloud.yaml └── playbook.yaml
Коллекция - совокупность необходимых ролей, плейбуков, плагинов для inventory, собственных модулей. Коллекции можно хранить в git и устанавливать их оттуда или публиковать на специальном сервисе Ansible Galaxy.
Примерный вид рабочего процесса для автоматизации управления конфигурацией:
- Описание зависимостей (список используемых коллекций) в особом файле, например,
requirements.yaml
- Установка зависимостей утилитой
ansible-galaxy
и формирование inventory - Запуск команды
ansible-playbook
с указанием плейбука
Этот рабочий процесс может быть запущен на CI системе или в специальной системе, построенной поверх Ansible — AWX. У AWX есть GUI, упрощающий некоторые задачи по работе с Ansible, например, сделать кнопку для техподдержки для выполнения рутинных операций, которые требуются как реакция на инцидент или запрос от пользователя.
Ansible Galaxy
Ansible Galaxy - общий ресурс для хранения коллекций и ролей.
По умолчанию роли оттуда ставятся в глобальный каталог типа /etc/ansible/roles
. Если нужно установить локально в текущий плейбук, нужно в ansible.cfg
прописать
roles_path = ./roles
Рядом положить файл requirements.yml
с перечислением ролей-зависимостей и, по желанию, их версий.
--- roles: - name: elliotweiser.osx-command-line-tools version: 2.3.0 - name: geerlingguy.mac version: 4.0.0
После этого запустить установку ролей из Galaxy, указав файл зависимостей. Скачанные роли окажутся в подкаталоге roles, как указано в файле конфигурации.
ansible-galaxy install -r requirements.yml
Плейбук:
--- - hosts: localhost connection: local vars: homebrew_installed_packages: - pv roles: - elliotweiser.osx-command-line-tools - role: geerlingguy.mac become: true
Проверка/тестирование
По степени увеличения сложности:
yamllint
ansible-playbook –syntax-check
- встроенная проверка синтаксиса. Ловит не всё, например,include:
не может, т. к. для этого нужно запустить плейбук.ansible-lint
- molecule test (integration)
ansible-playbook –check
(against prod)- parallel infrastructure
yamllint
Проверка YAML на корректность. Даже если плейбук запускается и работает, то шероховатости в его написании могут впоследствии привести к проблемам.
# Установка sudo apt install yamllint # или pip3 install yamllint yamllint playbook_delegate.yml playbook_delegate.yml 1:1 error too many blank lines (1 > 0) (empty-lines) 2:1 warning missing document start "---" (document-start) 4:11 warning truthy value should be one of [false, true] (truthy) 11:1 warning comment not indented like content (comments-indentation) 13:81 error line too long (156 > 80 characters) (line-length) 18:13 warning truthy value should be one of [false, true] (truthy) 19:81 error line too long (155 > 80 characters) (line-length) 23:1 error too many blank lines (3 > 0) (empty-lines)
ansible-lint
Проверка на соответствие best practices.
# Установка sudo apt install ansible-lint # или pip3 install ansible-lint ansible-lint playbook2.yml WARNING Listing 4 violation(s) that are fatal yaml: truthy value should be one of [false, true] (truthy) playbook2.yml:3 package-latest: Package installs should not use latest playbook2.yml:11 Task/Handler: Install yaml: wrong indentation: expected 4 but found 2 (indentation) playbook2.yml:11 yaml: truthy value should be one of [false, true] (truthy) playbook2.yml:19 You can skip specific rules or tags by adding them to your configuration file: # .ansible-lint warn_list: # or 'skip_list' to silence them completely - package-latest # Package installs should not use latest - yaml # Violations reported by yamllint Finished with 4 failure(s), 0 warning(s) on 1 files.
Windows
Особый подход, свои модули. Подключение в основном через WinRM.
Inventory.
ansible_winrm_transport=ntlm
, если доменная учётка, без этой настройки ошибка "msg": "plaintext: the specified credentials were rejected by the server"
.
Если используется WinRM HTTPS (порт 5986), то бывает полезным добавить опцию ansible_winrm_server_cert_validation=ignore
[server1] server1.example.com ansible_connection=winrm ansible_port=5985 ansible_winrm_transport=ntlm
Пример задачи. Для Windows лучше оборачивать переменные с путями в одинарные кавычки, т. к. там используется обратный слэш.
- name: Remove {{ deploy_dir }} win_file: path: '{{ deploy_dir }}' state: absent - name: Create {{ deploy_dir }} win_file: path: '{{ deploy_dir }}' state: directory - name: Copy autotests to {{ deploy_dir }} win_copy: src: autotest/ dest: '{{ deploy_dir }}' #- name: Dir files # win_shell: | # dir ~ # dir '{{ deploy_dir }}' # test-path '{{ run_bat }}' # register: dir_result #- debug: # var: dir_result.stdout_lines - name: Run {{ run_bat }} win_shell: '{{ run_bat }}' args: executable: cmd chdir: '{{ run_bat_dir }}'
Конкретные задачи
Копирование с одного удалённого сервера на другой
Существует модуль synchronize, где можно настроить делегирование и тем самым запустить копирование непосредственно с одного хоста на другой.
- name: Synchronization of src on delegate host to dest on the current inventory host. ansible.posix.synchronize: src: /first/absolute/path # Путь на делегируемом хосте-источнике delegate.host dest: /second/absolute/path # Путь на текущем обрабатываемом хосте delegate_to: delegate.host
Но есть проблема аутентификации: каким образом обеспечить доступ с делегируемого хоста на конечные? Есть вариант SSH Agent Forwarding, но это вроде как небезопасно. Поэтому в общем случае копируем сначала на менеджера (достаточно одного раза - run_once: true
), а потом с него на конечные сервера.
--- - name: Sync hosts: k3 gather_facts: no tasks: - name: Copy WAR to worker from source command: rsync k2:/home/user/file.war /home/user/file.war run_once: true delegate_to: localhost - name: Copy WAR to destination from worker synchronize: src: /home/user/file.war dest: /home/user/file.war
Останов, копирование .war-файла с эталонного сервера и конфигов, запуск, удаление паролей
- name: "Get service status" shell: systemctl --user is-active {{ service_name }}.service register: service_status ignore_errors: true - name: "Get process ID" shell: pgrep -f "{{ catalina_base }}" register: process_id ignore_errors: true - name: "Stop process by script" script: "{{ deploy_dir }}/stop.sh" args: creates: "~/.config/systemd/user/{{ service_name }}.service" when: service_status.stdout != 'active' and process_id.stdout != "" - name: "Kill process" shell: kill -9 "{{ process_id.stdout }}" when: service_status.stdout != 'active' and process_id.stdout != "" - name: "Stop service" systemd: scope: user name: "{{ service_name }}" state: stopped when: service_status.stdout == 'active' - name: "Create folders" file: path: "{{ item }}" state: directory with_items: - "~/.config/systemd/user" - "{{ catalina_base }}/conf" - "{{ catalina_base }}/webapps" - "{{ pool_settings_dir }}" - name: "Enable systemd lingering" shell: loginctl enable-linger $USER - name: "Copy WAR file to worker from source ({{ war_source_server }})" command: rsync "{{ deploy_user }}@{{ war_source_server }}:{{ catalina_base }}/webapps/app.war" /tmp/app.war delegate_to: localhost run_once: true when: copy_war|bool == true - name: "Copy WAR to destination" syncronize: src: /tmp/app.war dest: "{{ catalina_base }}/webapps/app.war" when: copy_war|bool == true - name: "Copy conf files" copy: src: "{{ item }}" dest: "{{ catalina_base }}/conf" owner: "{{ deploy_user }}" mode: 0660 with_fileglob: - "conf/*" - "conf/{{ my_env }}/*" when: copy_conf|bool == true - name: "Copy scripts" copy: src: "{{ item }}" dest: "{{ deploy_dir }}" owner: "{{ deploy_user }}" mode: 0770 with_fileglob: - "scripts/*" - name: "Copy pool-settings.xml" template: src: "pool-settings.xml.{{ my_env }}.j2" dest: "{{ pool_settings_dir }}/pool-settings.xml" mode: 0660 - name: "Copy unit file" template: src: "{{ service_name }}.service.j2" dest: "~/.config/systemd/user/{{ service_name }}.service" - name: "Start service" systemd: scope: user daemon_reload: true name: "{{ service_name }}" state: started enabled: true - name: "Remove passwords from pool-settings.xml" replace: path: "{{ pool_settings_dir }}/pool-settings.xml" regexp: '(password\">).*(</entry>)' replace: '\1\2'
Добавить секцию в шаблон конфига по условию
Например, если сервер входит в ту или иную группу. В самом шаблоне:
{% if inventory_hostname in groups['t01'] or inventory_hostname in groups['t03'] %} Секция, которую надо вставить для хостов в группах t01 и t03 {% else %} Секция, которую надо вставить для прочих хостов {% endif %}
Если используются переменные из фактов (в примере inventory_hostname
из спец. переменных), то в плейбуке надо не запрещать сбор этих фактов, т. е., gather_facts: false
не применять.
Дополнительно про инвентори: Основы Ansible, без которых ваши плейбуки — комок слипшихся макарон, часть 2
Template Designer Documentation: https://jinja.palletsprojects.com/en/stable/templates/
Выборка из словаря по значениям из списка
В зависимости от набора работающих служб получать набор строк для формирования API-запроса. Если есть работающие службы, которые отсутствуют в словаре, они будут пропущены по условию.
- services_status.yml
- hosts: k3 roles: - services_status vars: services: project-service1: main project-service2: thingy project-service3: bonanza
- roles/services_status/tasks/main.yml
- name: Gather running services shell: cmd: systemctl --user --type service --state running --plain --quiet |grep ^project- |sed -E 's#\.service.*##' register: running_services # В curl body - это --data '{"dataSources": ["main","thingy","bonanza"]}' - name: Update API uri: url: https://localhost/admin/api/update_metadata user: admin password: {{ pass }} force_basic_auth: true method: POST validate_certs: false body_format: json body: dataSources: - "{{ services[item] }}" with_items: - "{{ running_services['stdout_lines'] }}" when: item in services | list
Работают 1-я и 3-я службы, 2-я не работает. Значения получаются main и bonanza соответственно.
ansible-playbook services_status.yml TASK [services_status : debug] ********************************************************************************************************** ok: [k3] => (item=project-service1) => { "msg": "Values are: main" } ok: [k3] => (item=project-service3) => { "msg": "Values are: bonanza" }
Выбор списка значений из словаря
- name: "Update metadata" uri: url: https://localhost/service/json/admin/api/refresh_metadata user: admin password: admin force_basic_auth: true method: POST validate_certs: false body_format: json timeout: 600 body: dataSources: {{ services | dict2items | selectattr('key', 'in', running_services['stdout_lines']) | map(attribute='value') | list }}
Учёт кода возврата в в ответе Nexus в формате json
- staging.yml
- hosts: 127.0.0.1 connection: local roles: - staging vars: name: "" version: "" repo_test: "project_maven_test" repo_uat: "project_maven_uat" repo_prod: "project_maven_prod" nexus_user_1: "user1" nexus_user_2: "user2" nexus_password_1: "" nexus_password_2: "" timeout: 300
- roles/staging/tasks/main.yml
- name: "Create tag" uri: url: https://nexus-cd.example.com/service/rest/v1/script/getVersion/run user: "{{ nexus_user_1 }}" password: "{{ nexus_password_1 }}" force_basic_auth: true method: POST validate_certs: false timeout: "{{ timeout }}" return_content: true body_format: json body: version: "{{ version }}" register: response_ctag - debug: var: response_ctag.json.result - pause: seconds: 10 - name: "Set tag" uri: url: https://nexus-cd.example.com/service/rest/v1/script/setVersion/run user: "{{ nexus_user_1 }}" password: "{{ nexus_password_1 }}" force_basic_auth: true method: POST validate_certs: false timeout: "{{ timeout }}" return_content: true body_format: json body: tagName: "ver-{{ version }}" repository: "{{ repo_test }}" name: "{{ name }}" version: "{{ version }}" register: response_stag - debug: var: response_stag.json.result - pause: seconds: 10 when: (response_stag.json.result |from_json).status == 200 - name: "Staging to UAT" uri: url: https://nexus-cd.example.com/service/rest/v1/script/staging/run user: "{{ nexus_user_2 }}" password: "{{ nexus_password_2 }}" force_basic_auth: true method: POST validate_certs: false timeout: "{{ timeout }}" return_content: true body_format: json body: repoName: "{{ repo_uat }}" version: "{{ version }}" register: response_uat when: (response_stag.json.result |from_json).status == 200 - debug: var: response_uat.json.result when: response_uat.json.result is defined - pause: seconds: 10 when: response_uat.json.result |from_json).status == 200 - name: "Staging to PROD" uri: url: https://nexus-cd.example.com/service/rest/v1/script/staging/run user: "{{ nexus_user_2 }}" password: "{{ nexus_password_2 }}" force_basic_auth: true method: POST validate_certs: false timeout: "{{ timeout }}" return_content: true body_format: json body: repoName: "{{ repo_prod }}" version: "{{ version }}" register: response_prod when: (response_uat.json.result |from_json).status == 200 - debug: var: response_prod.json.result when: response_prod.json.result is defined
Вывод содержимого файла в консоль
- name: "Read a file content" shell: | cat /home/user/add-time.txt register: file_content - name: "Print the file content to a console" debug: msg: "{{ file_content.stdout_lines }}"
https://www.shellhacks.com/ansible-cat-file-print-read-file-content/
Замена текста в файлах
Удаление шеллом всех бэкапов целевого файла, затем новый бэкап и замена текста.
Применяются именованные референсы (backreferences), т. к. числовые несовместимы с цифрами, например, \1800\2
даст ошибку.
ansible all -u admin -m shell -a "rm /opt/app/PG/job-config.xml.*" ansible all -u admin -m replace \ -a "path=/opt/app/PG/job-config.xml regexp='(?P<start>max-connections\">)400(?P<end><)' replace='\g<start>800\g<end>' backup=yes"
https://docs.ansible.com/ansible/latest/collections/ansible/builtin/replace_module.html
https://linuxbuz.com/devops/replace-a-string-in-a-file-with-ansible-replace-module
https://www.linuxtechi.com/replace-strings-lines-with-ansible
Шаблон-цикл для составления конфигурационного файла из нескольких однотипных секций
Здесь секция пишется, если хост входит в группу, совпадающую с alias
в списке queues
. Можно сделать по любому другому условию.
Если не подходит alias
, можно добавить в каждый пункт списка переменных ещё один параметр, например, tag: my_group
и ориентироваться на него.
- templates/queue-settings.xml.j2
<?xml version="1.0" encoding="windows-1251"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> {% for q in queues if q.alias in group_names %} <entry key="queue-{{ loop.index0 }}.alias">{{ q.alias }}</entry> <entry key="queue-{{ loop.index0 }}.cft-cache-mode">{{ q.cache_mode }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jms-factory-name">{{ q.factory_name }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jms-factory-url">{{ q.factory_url }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jms-factory-prefix">{{ q.factory_prefix }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jms-factory-connectionFactory">{{ q.factory_connection }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jms-factory-username">{{ q.factory_username }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jms-factory-password">{{ q.factory_password }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jmx-url">{{ q.jmx_url }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jmx-broker">{{ q.jmx_broker }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jmx-username">{{ q.jmx_username }}</entry> <entry key="queue-{{ loop.index0 }}.cft-jmx-password">{{ q.jmx_password }}</entry> {% endfor %} </properties>
- vars/main.yml
queues: - alias: "rrr1" cache_mode: "true" factory_name: "org.apache.activemq.jndi.ActiveMQInitialContextFactory" factory_url: "tcp://server3.example.com:8881?jms.prefetchPolicy.queuePrefetch=0" factory_prefix: "dynamicQueues/" factory_connection: "ConnectionFactory" factory_username: "rkf1admin" factory_password: "P@ssw0rd1" jmx_url: "service:jmx:rmi:///jndi/rmi://server4.example.com:11099/jmxrmi" jmx_broker: "Broker_TEST2" jmx_username: "write" jmx_password: ";o23489yrn8" - alias: "test_servers" cache_mode: "false" factory_name: "org.apache.activemq.jndi.ActiveMQInitialContextFactory" factory_url: "tcp://server5.example.com:8881?jms.prefetchPolicy.queuePrefetch=0" factory_prefix: "dynamicQueues/" factory_connection: "ConnectionFactory" factory_username: "adminnn" factory_password: "P@ssw0rddd" jmx_url: "service:jmx:rmi:///jndi/rmi://server8.example.com:11099/jmxrmi" jmx_broker: "Broker_TEST6" jmx_username: "controleRole" jmx_password: "aasdpkpdjpeeeee"
Ansible for Network Configuration Templates (Github)
Глава 5. Высвобождение всей мощи шаблонов Jinja2 (James Freeman, Jesse Keating - Mastering Ansible, 3rd ed.)
Extended loop variables
Special Variables
Lingering
Включение возможности для непривилегированного пользователя запускать демоны systemd, которые работают после выхода пользователя из своего сеанса.
- name: Ensure lingering enabled for user {{ deploy_user }} command: cmd: loginctl enable-linger {{ deploy_user }} creates: /var/lib/systemd/linger/{{ deploy_user }}
https://github.com/ansible/ansible/issues/72674#issuecomment-801200193
Ad-hoc:
ansible k3 -m command -a "loginctl enable-linger user creates=/var/lib/systemd/linger/user"
Выбор каталога шаблона в зависимости от группы хоста
Здесь: группы rrr1copy
, test1
и test2
входят в группу queue_settings
в inventory.
Для каждой группы создан свой queue-settings.xml.j2
в соответствующем подкаталоге в templates.
- name: Delete queue-settings.xml from unauthorized servers file: path: "{{ postgres_dir }}/queue-settings.xml" state: absent when: inventory_hostname not in groups["queue_settings"] tags: - postgres - name: Copy template queue-settings.xml template: src: "postgres/{{ group_names |select('match', 'rrr1copy|test[12]') |join() }}/queue-settings.xml.j2" dest: "{{ postgres_dir }}/queue-settings.xml" owner: "{{ deploy_user }}" mode: 0660 when: inventory_hostname in groups["queue_settings"] tags: - postgres
https://www.reddit.com/r/ansible/comments/ozopzi/list_hosts_in_group_and_filter/
Слияние переменных-словарей
По умолчанию, одноимённая дочерняя переменная полностью перезаписывает родительскую.
# group_vars/all.yml settings: user: "Vasya" group: "Managers" mood: "Angry" status: "Divorced" # group_vars/k3.yml settings: user: "Kolya" group: "Admins" mood: "Frustrated" # Результат - debug: var: settings ok: [k3.workgroup] => { "settings": { "group": "Admins", "mood": "Frustrated", "user": "Kolya" } }
Чтобы дочерняя переменная дополняла родительскую, нужно переименовать переменные в файлах group_vars
и применить фильтр combine()
в плейбуке, чтобы собрать их вместе.
# vars_test.yml - hosts: k3 remote_user: user roles: - role: vars_test vars: settings: "{{ settings_all | combine(settings_host, recursive=true) }}" # Результат - debug: var: settings ok: [k3.workgroup] => { "settings": { "group": "Admins", "mood": "Frustrated", "status": "Divorced", "user": "Kolya" } }
Затем в шаблонах уже можно применять {{ settings.user }}
и т. д.
Параметр
hash_behaviour=merge
, добавляемый в ansible.cfg
, считается устаревшим и нежелательным к применению.
How to merge variables of type hash across different variable files in ansible?
ansible.builtin.combine filter – combine two dictionaries
Получение списка дочерних групп в группе
Для копирования шаблонов из подкаталогов. Группа с дочерними в инвентори:
- hosts.txt
[queue_servers:children] a_servers b_servers c_servers d_servers
Роль:
- roles/test_groups/main.yml
- name: Dump hosts to json file command: cmd: ansible-inventory -i hosts.txt --list --output hosts.json delegate_to: localhost - name: Json file to variable include_vars: file: hosts.json name: hosts - debug: msg: "{{ item }}/path/file.txt" loop: "{{ hosts.queue_servers.children }}"
Результат выполнения:
ok: [k3.workgroup] => (item=a_servers) => { "msg": "a_servers/path/file.txt" } ok: [k3.workgroup] => (item=b_servers) => { "msg": "b_servers/path/file.txt" } ok: [k3.workgroup] => (item=c_servers) => { "msg": "c_servers/path/file.txt" } ok: [k3.workgroup] => (item=d_servers) => { "msg": "d_servers/path/file.txt" }
Ошибки, проблемы
Collection ansible.posix does not support Ansible version
[WARNING]: Collection ansible.posix does not support Ansible version 2.17.0
ansible-galaxy collection install ansible.posix Starting galaxy collection install process [WARNING]: Collection junipernetworks.junos does not support Ansible version 2.17.0 [WARNING]: Collection frr.frr does not support Ansible version 2.17.0 [WARNING]: Collection ibm.qradar does not support Ansible version 2.17.0 [WARNING]: Collection cisco.asa does not support Ansible version 2.17.0 [WARNING]: Collection ansible.posix does not support Ansible version 2.17.0 Nothing to do. All requested collections are already installed. If you want to reinstall them, consider using `--force`. ansible-galaxy collection install ansible.posix --force
The processing instruction target matching "[xX][mM][lL]" is not allowed
Ошибка после запуска jar, где log4j2.xml
настроен как конфигурация логов. Проблема в том, что первой строкой стоял комментарий, а там всегда должен быть XML declaration. Вот так будет работать корректно:
<?xml version="1.0" encoding="UTF-8" ?> <!-- ###################### # Здесь какой-то текст ####################### -->
Platform linux on host X is using the discovered Python interpreter
Предупреждения в логах:
[WARNING]: Platform linux on host X is using the discovered Python interpreter at /usr/bin/python, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.11/reference_appendices/interpreter_discovery.html for more information.
Решение: добавить в ansible.cfg
[default] interpreter_python = auto_silent
https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html
Литература
How I used Ansible to automate updates at home (RedHat)
Ansible 101 with Jeff Geerling
ADV-IT - Уроки Ansible на Русском
Getting started with Ansible (Learn Linux TV)
Ansible: передаем json в теле запроса, используя модуль uri
Как в Ansible запустить задачу, если сервер не входит в группу
Mastering Ansible - Third Edition (Github repo)