суббота, 9 февраля 2019 г.

Ansible - Подводные камни


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


Подборка некоторых наблюдений и будет обрисована в этой заметке.


1) Отладка ansible

Самое очевидное и простое использовать параметр -vvv
Это действительно бывает полезно, но далеко не всегда укажет на причину неисправности. А вот модуль debug позволит вывести значение переменной в процессе выполнения плейбука.
Ниже приведен сценарий (play), в котором выполняется команда на хосте, вывод регистрируется в переменную upt, а затем модулем debug выводится значение переменной.

---
- hosts: srv1
  tasks:
        - name: check uptime
          command: uptime
          register: upt

        - debug: var=upt


2) Тестировать всегда, тестировать всё

Использовать готовые решения из ansible galaxy - это нормально, позволит не изобретать велосипеды и экономить массу времени.
Но проблема в том, что очень многие роли не работают совсем, либо работают не так как указано в описании к ним.
Поэтому даже если используется готовая роль, или готовый таск, всегда нужно внимательно читать код, и проверять что он делает.
Очень часто возможны сюрпризы.
Так же, необходимо несколько раз запускать плейбуки (минимум 2 раза) для того чтобы была уверенность в идемпотентности. Иначе легко попасть в ситуацию, когда при первом выполении плейбука всё отработало, а при повторном запуске опять вносятся изменения в состояние системы.
И что тогда получаем в результате?
- Нестабильную работу, а иногда и полностью нерабочий хост.


3) Тэги

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

ansible-playbook example.yml --tags "configuration,packages"

ansible-playbook example.yml --skip-tags "packages"

В первом примере выполнятся задачи с тэгами configuration и packages, а во втором - выполнятся все таски, кроме тех что с тэгом packages. И да, один тэг можно навесить на множество тасков, объединив их таким нехитрым образом.


4) Дописать в существующую строку

Достаточно частая задача произвести изменения в конфигурационных файлах. Что-то дописать, что-то удалить или заменить.
Модуль lineinfile вполне подходит для подобных задач, но бывает более сложная ситуация, когда нужно найти существующую строку и что-то дописать в нее.
Расмотрим на нескольких примерах.
Вначале более простая ситуация, когда нужно просто дописывать в конец строки. Например мы хотим добавлять группу в список AllowGroups в конфиг ssh.

Было:  
AllowGroups Group1

Нужно чтобы стало:  
AllowGroups Group1 Group2


Решение:

 - name: Add Group to AllowGroups
   lineinfile: 
     dest: /etc/ssh/sshd_config
     backup: True
     backrefs: True
     state: present
     regexp: '^(AllowGroups(?!.*\b{{ ftp_group_name }}\b).*)$'
     line: '\1 {{ ftp_group_name }}'

Здесь используются возможности backrefs (backreferences) или обратные ссылки. Включение этой опции меняет поведение модуля lineinfile.
Таким образом, если регулярное выражение (regexp) не совпадет нигде в файле, файл будет оставлен без изменений. Если же совпадет, то последняя совпадающая строка будет заменена параметром line.
В этом примере regexp создает группу, и по дефолту она получает наименование \1. Ну а конструкция вида (?!...)  - это отрицание группы, или отрицательный lookahead, который помогает исключить из регулярного выражения значение нашей переменной.
К слову, лучше всего для отладки регулярных выражений подходит ресурс https://regex101.com/

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

Было:
GRUB_CMDLINE_LINUX="no_timer_check console=tty0 console=ttyS0,115200n8 net.ifnames=0 biosdevname=0 elevator=noop crashkernel=auto"


Нужно чтобы стало:
GRUB_CMDLINE_LINUX="no_timer_check console=tty0 console=ttyS0,115200n8 net.ifnames=0 biosdevname=0 elevator=noop crashkernel=auto user_namespace.enable=1 namespace.unpriv_enable=1"


Решение:

- name: update grub default config
  lineinfile:
    dest: /etc/default/grub
    backup: True
    backrefs: True
    state: present
    line: '\1 {{ grub_add_args | join(" ") }}"'
    regexp: '^(GRUB_CMDLINE_LINUX="(?!.*\b{{ grub_add_args | join(" ") }}\b).*)"$'

Здесь мы видим уже знакомый backrefs, но кроме этого закрывающая кавычка вынесена за пределы группы.
Кроме этого, переменная grub_add_args представляет из себя массив словарь, потому если просто ее попробовать подставить в строке образуется каша.

Переменная grub_add_args

grub_add_args:
  - user_namespace.enable=1
  - namespace.unpriv_enable=1

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

GRUB_CMDLINE_LINUX="no_timer_check console=tty0 console=ttyS0,115200n8 net.ifnames=0 biosdevname=0 elevator=noop crashkernel=auto [u'user_namespace.enable=1', u'namespace.unpriv_enable=1']"

Такой результат нам не подходит, поэтому можно использовать фильтры jinja2. В данном случае фильтр join, который обработает словарь по заданному разделителю и вернет результат в виде строки.


5) Перезагрузка

Еще совсем недавно для того чтобы с помощью ansible перезагрузить удаленный хост нужно было ваять несколько тасков, настраивать delay и таймауты, но к счастью ситуация изменилась.
Начиная с ansible версии 2.7 появился встроенный модуль:

-name: reboot the machine
 reboot:


6) Составные условия

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


- name: reboot the server and wait to come back
  reboot:
  when: (reboot_hint.stdout.find("reboot") != -1) or (kernelconfig is changed)


В этом сценарии (play) условием перезагрузки является составное условие. В случае если в выводе переменной reboot_hint будет найдено слово reboot или переменная kernelconfig изменится, только тогда будет выполнена перезагрузка.


7) Handlers или обработчики событий

Казалось бы, что это просто специальные таски, которые вызываются из обычных задач, если нужно что-то выполнить по событию. Например перезапустить сервис, чтобы он перечитал изменения в конфигурации.
Но дело в том, что эти задачи срабатывают только после выполнения всех задач в сценарии (play). При этом, если несколько задач вызвали один и тот же handler, он выполниться только один раз.
В большинстве случаев, это приемлимо и даже неплохо, но возможна ситуация когда необходимо сразу же применить изменения.
Например меняются параметры ядра, затем нужно перезагрузить хост, а так же с помощью handler сгенерировать новый конфиг grub.
Не сложно представить, что произойдет по дефолту - будут внесены изменения, затем хост перезагрузится и лишь в самом конце произойдет генерация нового конфига, который не применится потому что перезагрузка была ранее.
Как выйти из такой ситуации?
Использовать специальный meta-таск:


- meta: flush_handlers

В этом случае все handlers отработают не дожидаясь завершения всего сценария.


8) Если нет готового модуля

В состав ansible входит уже несколько тысяч модулей, под различные задачи и оборудование, но в процессе работы обязательно будет ситуация, когда ничего подходящего нет и нужно вручную реализовать автоматизацию. В таком случае можно воспользоваться модулем command или shell. Про различие между ними поговорим немного позже.
А пока предположим, что необходимо установить пакет из менеджера snap, модуль для которого пока еще не реализован.


- name: install lxd with snap
  command: snap install lxd
  args:
    creates: /var/snap/lxd/
  tags: snaplxd

В этом сценарии пакет устанавливается с помощью универсального модуля command, и при этом соблюдается идемпотентность.
Параметр creates запустит проверку на наличие указанного каталога, и если он обнаружится то команда выполнятся не будет.
Если не использовать проверки в таких ситуациях, то команды будут выполнятся каждый раз, что приведет к неизвестному состоянию системы.


9) Если нужно подождать

Бывают ситуации, когда нужно выдержать паузу между выполнениями двух тасков. Например подождать, пока заполнится кеш, или что-то еще.
Для этих целей существует одноименный модуль ansible:


- pause:
    minutes: 5

В качестве параметров можно указывать как минуты (minutes: 10), так и секунды (seconds: 5).
Кроме этого, модуль интерактивный, можно прервать его работу, либо наоборот ускорить и отменить паузу.



10) Модули command, shell и raw

Эти три модуля очень похожи и все они позволяют выполнять команды на удаленных хостах.
Для начала разберем command и shell.
Основное отличие в том, что в модуле command любая команда выполняется без прохождения через оболочку /bin/sh. Поэтому переменные определенные в оболочке и перенаправления/конвееры работать попросту не будут. Модуль shell в свою очередь выполняет команды через оболочку по умолчанию /bin/sh. Поэтому там будут доступны переменные оболочки и перенаправления. Это очень важно всегда помнить.
Модуль raw значительно отличается и от command и от shell тем, что не выполняет дополнительную обработку выполнения команды. Эти дополнительные обработки присутствуют в почти любом модуле Ansible. Модуль raw передает команду, как есть в "сыром" виде без проверок, при этом наличие python на удаленном хосте не требуется.
Как раз модуль raw и позволяет управлять конфигурациями различного сетевого оборудования.

По дефолту при выполнении одиночных (ad-hoc) команд ansible использует всегда модуль command:


$ansible -a 'df -h | grep sda1' srv1
srv1 | FAILED | rc=1 >>
df: ‘|’: No such file or directory
df: ‘grep’: No such file or directory
df: ‘sda1’: No such file or directorynon-zero return code

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

$ansible -m shell -a 'df -h | grep sda1' srv1
srv1 | CHANGED | rc=0 >>
/dev/sda1        40G  3.6G   37G  10% /

...

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

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

Отправить комментарий