В первой части мы начали изучение Ansible, популярного инструмента для автоматизации настройки и развертывания ИТ-инфраструктуры. Ansible был успешно установлен в InfoboxCloud, описаны принципы работы, базовая настройка. В завершении статьи мы показали как быстро установить nginx на несколько серверов.

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

В третьей части мы узнали как написать единый Ansible playbook для разных ОС (например с rpm и deb), как обслуживать сотни хостов и не писать их все в inventory и как сгруппировать сервера по регионам InfoboxCloud. Было изучено использование переменных Ansible и файла inventory.

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



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

Запускаем задачи локально с помощью local_action


Иногда задачи надо запускать на локальной машине в рамках исполнения playbook для удаленных серверов. Например, можно на Ansible–сервере прописать ключи доступа по API к облаку и отдавать команды утилите командной строки для создания новых серверов облака. Часто может требоваться отправлять запросы в REST API через модуль uri Ansible. Возможность что-то делать прямо на Ansible–сервере для отдельной задачи в playbook, где в качестве hosts прописаны удаленные сервера, есть.

Допустим, вы хотите запустить shell–модуль на сервере, откуда вы запускаете Ansible. Для этого пригодится опция local_action, которая запустит модуль локально.
---
- hosts: experiments
  remote_user: root
  tasks:

  - name: check running processes on remote system
    shell: ps
    register: remote_processes

  - name: remote running processes
    debug: msg="{{ remote_processes.stdout }}"

  - name: check running processes on local system
    local_action: shell ps
    register: local_processes

  - name: local running processes
    debug: msg="{{ local_processes.stdout }}"

Процессы на удаленных машинах.


Процессы на локальной машине.


Mы видим, что исполнение команды перенаправляется для локальной машины.


Таким образом, вы можете запустить любой модуль Ansible с local_action.

Работаем с условиями


Ansible исполняет все задачи последовательно. Тем не менее, для сложного playbook с десятками задач, вам может потребоваться в зависимости от ситуации запускать только часть задач. Ранее мы уже рассматривали ситуацию, когда с помощью переменных мы корректно устанавливали Apache на rpm и deb дистрибутивы. Подобным образом можно указывать условия для выполнения задач с помощью when:
---
- hosts: experiments
  remote_user: root
  tasks:
  - name: Install httpd package
    yum: name=httpd state=latest
    sudo: yes
    when: ansible_os_family == "RedHat"

  - name: Install apache2 package
    apt: name=apache2 state=latest
    sudo: yes
    when: ansible_os_family == "Debian"

Если ОС семейства RedHat – будет установлен пакет httpd через yum, а если семейства Debian – apache2 через apt. ansible_os_family – переменная Ansible, получаемая на стадии gather_facts.

В playbook выше мы использовали sudo: yes, подразумевая, что у пользователя есть права sudo. Давайте проверим, так ли это:
---
- hosts: experiments
  remote_user: root
  tasks:

  - name: Testing user sudo privilege
    command: /usr/bin/sudo -v
    register: sudo_response
    ignore_errors: yes

  - name: Stop if Users doesn`t have sudo privilege
    fail: msg="User doesn`t have sudo privilege"
    when: sudo_response.rc == 1



В примере выше мы запустили команду на сервере /usr/bin/sudo -v и сохранили ее вывод в переменную через register. В переменной был захвачен вывод stdout и stderr (rc, return code). Во второй задаче мы проверили содержание return code переменной и если оши��ка произошла — должны завершить исполнение playbook с выводом сообщения.

Для сравнения в условиях в Ansible можно использовать == (равно), != (не равно), > (больше), < (меньше), >= (больше равно), <= (меньше равно).

Если вам нужно проверить, есть ли в переменной символ или строка, используйте операторы in и not.
- name: Querying rpm list for httpd package
  shell: rpm -qa | grep httpd
  register: httpd_rpm

- name: Check if httpd rpm is installed on the remote host
  debug: msg="httpd is installed on the remote host"
  when: "'httpd-2.2.27–1.2.x86_64' in httpd_rpm.stdout"

– name: Check if httpd rpm is not installed on the remote host
  debug: msg="httpd is not installed on the remote host"
  when: not 'httpd-2.2.27.1.2.x86_64' in httpd_rpm.stdout

Можно задавать несколько условий, используя операторы and (и) и or (или).
– name: Check if httpd rpm is installed on the remote host
  debug: msg="httpd is installed on the remote host"
  when: "'httpd-2.2.27–1.2.x86_64' in httpd_rpm.stdout and 'httpd-tools-2.2.27–1.2.x86–64' in httpd_rpm.stdout"

Также можно проверить логическое значение переменной. Давайте сделаем бекап, если в переменной backup установлено true:
– name: Rsync 
  shell: /usr/bin/rsync -ra /home /backup/{{ inventory_hostname}}
  sudo: yes
  when: backup

Ansible позволяет в условии использовать информацию о том, была ли уже определена переменная. Для этого используйте when: var is not define (где var — имя переменной, is not define – еще не была определена, is defined – уже была определена).

Работаем с циклами


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

Стандартные циклы

Используя стандартные циклы вы можете передать список пакетов для установки и Ansible запустит задачу для всех указанных пакетов.
---
- hosts: experiments
  remote_user: root
  tasks: 
  – name: Install nginx package
    yum: name={{ item }} state=latest
    with_items:
    – nginx
    – htop
    sudo: yes

В примере выше мы использовали конструкцию «with_items:» для задания переменных и использовали переменную по умолчанию item. На каждой итерации item принимает следующее значение, указанное в with_items.



Задача запускается один раз, но apt вызывается для всех указанных пакетов. Можно так же использовать with_items как словарь вместо строк:
with_items:
– {name: 'httpd', state: 'latest'}
– {name: 'htop', state: 'absent'}

Вложенные циклы

Вложенные циклы полезны, когда вы хотите выполнить несколько операций над одним и тем же ресурсом. Например, если вы хотите предоставить доступ ко множеству баз данных для пользователей MySQL.
–––
– hosts: experiments
  remote_user: root
  tasks:
  – name: give users access to multiple databases
  mysql_user: name={{ item[0] }} priv={{ item[1]}}.*:ALL append_privs=yes password=pass login_user=root login_password=root
  with_nested:
  – ['alexey', 'alexander']
  – ['clientdb', 'providerdb']

В приведенном примере мы используем модуль mysql_user для установки прав на базы данных и используем вложенные циклы с двумя списками: список пользователей и список баз данных. Ansible запустит модуль mysql_user для пользователя alexey, даст права на все указанные во втором списке базы данных, затем запустит для пользователя alexander и так же даст права.

Циклы по подэлементам


В предыдущем примере мы назначили все указанн��е базы данных всем указанным пользователям. Но что делать, если для каждого пользователя нужно назначить свой специфический набор баз данных? Для этого нам пригодятся циклы по подэлементам.
---
- hosts: experiments
  remote_user: root
  vars:
    users:
    – name: alexey
      database:
      – clientdb
      – providerdb
    – name: alexander
      database:
      – providerdb
  tasks:
  – name: give users access to multiple databases
    mysql_user: name={{ item.0.name }} priv={{ item.1 }}.*:ALL append_privs=yes password=pass login_user=root login_password=root
    with_subelements:
    – users
    - database

Мы создали словари, которые состоят из имен пользователей и имен баз данных. Вместо добавления данных пользователей в playbook можно вынести их в отдельный файл переменных и включить в playbook. Ansible пройдется по словарю используя переменную item. Ansible назначает численные значения ключам, представленным конструкцией with_subelements, начиная с 0. В словаре 0 имя — пара «ключ-значения», поэтому для обращения по имени пользователя мы используем item.0.name. Dictionary — простой список, поэтому для обращения используем item.1.

Работаем с ролями


При проектировании архитектуры обычно оперируют ролями серверов: веб-сервер, сервер баз данных, балансировщик нагрузки и так далее. Каждая роль включает в себя определенный набор софта для установки и настройки. С ростом вашей системы постепенно будут выделяться компоненты, которые можно повторно использовать. Роли в Ansible предоставляют удобный способ организации ваших playbook. На основе предопределенной файловой структуры будут загружаться компоненты роли. Фактически роли — просто магия вокруг include (импортов), облегчающая подготовку playbook.

Типичная стру��тура playbook с ролями:
---
- hosts: webservers
  roles:
     - common
     - web
     – db

Файловая структура ролей будет выглядеть так:
site.yml
webservers.yml
roles/
   common/
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/
   web/
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/
   db/
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/

Если какой-то директории в роли нет — она будет проигнорирована и playbook будет исполняться. Совсем не обязательно у вас должны быть все элементы и директории playbook.

Правила, используемые для каждой роли:
  • Если roles/x/tasks/main.yml существует, задачи будут добавлены в процесс исполнения playbook.
  • Если roles/x/handlers/main.yml существует, обработчики событий будут добавлены в процесс исполнения playbook.
  • Если roles/x/vars/main.yml существует, переменные будут добавлены в процесс исполнения playbook.
  • Если roles/x/meta/mail.yml существует, любые роли-зависимости будут добавлены в список ролей. (В meta можно указывать список ролей, которые должны быть применены до конкретной роли, чтобы она применилась корректно).
  • Любая задача копирования может ссылаться на файл в roles/x/files без указания абсолютного или относительного пути.
  • Любая скриптовая задача может ссылаться на скрипты в roles/x/files без указания абсолютного или относительного пути.
  • Любая задача шаблонизации может ссылаться на roles/x/templates без указания абсолютного или относительного пути.
  • Любые импортируемые задачи могут ссылаться на файлы задач в директории roles/x/tasks без указания абсолютного или относительного пути.

В конфигурационном файле Аnsible можно задать roles_path (директорию с ролями). Это может пригодиться, если у вас playbook лежат в одном репозитории, а сами роли в другом. Можно задавать сразу несколько путей к ролям через двоеточие:
roles_path = /opt/mysite/roles:/opt/othersite/roles

В роли можно передавать переменные или использовать условия:
---
- hosts: experiments
  roles:
   – common
   – {role: web, dir: '/var/www', port: 80}
   – {role: repository, when: "ansible_os_family =='RedHat'"}

Ранее в статьях мы не рассматривали тэги. С их помощью можно запускать помеченную часть playbook.
С задачами использование тэгов выглядит так:
tasks:
    - apt: name={{ item }} state=installed
      with_items:
         - httpd
         - htop
      tags:
         - packages

    - template: src=templates/src.j2 dest=/var/www/.htaccess
      tags:
         - configuration

Можно запустить часть playbook так: ansible-playbook example.yml --tags «configuration,packages» или пропустить исполнение части так: ansible-playbook example.yml --skip-tags «notification».

Так вот тэги также можно использовать и при указании ролей:
---
- hosts: experiments
  roles:
    - { role: web, tags: ["apache", "simple"] }

Можно указать, какие задачи должны выполниться до роли и после:
---
- hosts: experiments
  pre_tasks:
    - shell: echo 'hello, habr'
  roles:
    - { role: web }
  tasks:
    - shell: echo 'still busy'
  post_tasks:
    - shell: echo 'goodbye, habr'

Зависимости ролей

Зависимости ролей позволяют автоматически исполнить зависимые роли при запуске конкретных ролей, у которых зависимости есть. Зависимости хранятся в roles/x/meta/main.yml. Вместе с зависимыми ролями могут быть переданы ��араметры. Путь к ролям может быть указан как в сокращенном виде, так и в полном. Также может быть использован репозиторий системы управления версиями.
---
dependencies:
  - { role: common, some_parameter: 3 }
  - { role: '/path/to/common/roles/foo', x: 1 }
  - { role: 'git+http://git.example.com/repos/role-foo,v1.1,foo' }

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

Ansible Galaxy


Ansible Galaxy — репозиторий ролей Ansible. С этого ресурса можно использовать уже готовые роли Ansible или добавлять свои.

Заключение


В написании статьи очень помогла книга "Learning Ansible" и конечно официальная документация.

Все эксперименты с Ansible удобно проводить в InfoboxCloud, так как имеется возможность для каждого виртуального сервера установить именно то количество ресурсов, которое необходимо для задачи (CPU/Ram/диск независимо друг от друга) или использовать автомасштабирование, а не выбирать VM из готовых шаблонов. Когда эксперименты не проводятся — можно просто выключить VM и оплачивать только стоимость диска.

Если вы обнаружили ошибку в статье, автор ее с удовольствием исправит. Пожалуйста напишите в ЛС или на почту о ней. Туда же можно задавать вопросы по Ansible для освещения в последующих статьях. Если вы не можете писать комментарии на Хабре, можно оставить их в Сообществе InfoboxCloud.

Успешной работы!