====== Ansible ======
Система управления автоматической конфигурации серверов. Контроль и отслеживание изменений в системе: версий установленных пакетов, файлов конфигурации, настроек ОС.
[[https://habr.com/ru/post/305400/|Пособие по Ansible]]\\
[[https://docs.ansible.com/ansible/latest/index.html|Документация]]\\
[[https://docs.ansible.com/ansible/2.9/modules/list_of_all_modules.html|Список модулей]]\\
^Task |Базовый блок. Обычно модуль с параметрами, выполняющий определённое действие. |
^Role |Типа функции - объединяет задачи, файлы, шаблоны, переменные и обработчики в единое целое. |
^Play |Сущность, связывающая задачи и роли с целевыми серверами. |
^Playbook |Файл yaml для запуска, где перечислены все нужные //плеи//. |
//Модули// используются в задачах конфигурации и запускаются на целевых серверах.\\
//Плагины// упрощают работу с Ansible на контрольной ноде — сервере, где запускается сам Ansible.
# копирование файла, используя модуль copy
ansible localhost -m copy -a "src=file.txt dest=/tmp/file.txt"
Важное отличие от простого копирования через консоль или скрипт - //идемпотентность//, т. е., свойство функции или операции при повторном применении к объекту давать тот же результат, что и при первом. В данном случае, при втором запуске файл копироваться не будет, потому что ansible вычисляет контрольные суммы файлов и понимает, что ничего не изменилось. Можно, конечно, сделать проверки и в скрипте, но это трудоёмкий процесс, требующий изобретения разных подходов для разных задач.
//Роли// в Ansible - это список //задач//.\\
//Плейбук// - это список //плеев//, где перечисляются роли и к каким серверам они применяются.
===== Задача =====
Базовый элемент.
# 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 <имя модуля>''.
==== Управление ошибками / кодами возврата, условия ====
https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_error_handling.html
==== 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: <имя хэндлера>'', если только эта задача реально выполняется, т. е. что-то изменяется.\\
Хэндлеры выполняются в конце плейбука после всех задач, если нет спец. настроек.
- 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 ''.
==== 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
===== Плейбук =====
Связь задач/ролей с целевыми серверами.
---
- 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-файл по умолчанию.
[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 ====
Это движок для подстановки переменных или для динамического формирования конфигурационных файлов. Ссылка на переменную идёт в любом файле 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
==== Динамические переменные ====
Используются для нескольких ОС, где разные пути к конфигам и один и тот же пакет называется по-разному, например apache2 в Debian и httpd в CentOS. Создаётся несколько файлов с переменными:
apache_package: httpd
apache_service: httpd
apache_config_dir: /etc/httpd/conf.d
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/.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
[[https://docs.ansible.com/ansible/latest/user_guide/collections_using.html|Коллекция]] - совокупность необходимых ролей, плейбуков, плагинов для inventory, собственных модулей. Коллекции можно хранить в git и устанавливать их оттуда или публиковать на специальном сервисе [[https://galaxy.ansible.com/|Ansible Galaxy]].
Примерный вид рабочего процесса для автоматизации управления конфигурацией:
- Описание зависимостей (список используемых коллекций) в особом файле, например, ''requirements.yaml''
- Установка зависимостей утилитой ''ansible-galaxy'' и формирование inventory
- Запуск команды ''ansible-playbook'' с указанием плейбука
Этот рабочий процесс может быть запущен на CI системе или в специальной системе, построенной поверх Ansible — AWX. У AWX есть GUI, упрощающий некоторые задачи по работе с Ansible, например, сделать кнопку для техподдержки для выполнения рутинных операций, которые требуются как реакция на инцидент или запрос от пользователя.
===== Ansible Galaxy =====
[[https://galaxy.ansible.com/|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.
====== Литература ======
[[https://www.redhat.com/sysadmin/ansible-automate-updates-home|How I used Ansible to automate updates at home]] (RedHat)\\
[[https://www.youtube.com/playlist?list=PL2_OBreMn7FqZkvMYt6ATmgC0KAGGJNAN|Ansible 101 with Jeff Geerling]]\\
[[https://www.youtube.com/playlist?list=PLg5SS_4L6LYufspdPupdynbMQTBnZd31N|ADV-IT - Уроки Ansible на Русском]]