Система управления конфигурацией Ansible - Академия Selectel

Система управления конфигурацией Ansible

Тирекс
Тирекс Самый зубастый автор
4 октября 2013

Представьте себе, что вам нужно управлять парком серверов, расположенных к тому же в разных географических точках. Каждый из этих серверов требует настройки, регулярного обновления и мониторинга. Конечно, для решения этих задач можно воспользоваться самым простым способом: подключиться к каждому серверу по ssh и внести необходимые изменения. При всей своей простоте этот способ сопряжен с некоторыми трудностями: он чрезвычайно трудоемок, а на выполнение однообразных операций уходит очень много времени. Чтобы упростить процессы настройки и конфигурирования серверов, можно также писать shell-скрипты. Но и этот способ вряд ли можно назвать совершенным. Скрипты нужно постоянно изменять, подстраивая их под каждую новую задачу. При их написании необходимо учитывать различие операционных систем и версий. Не будем забывать и о том, что отладка скриптов отнимает много усилий и забирает немало времени.

Изображение записи

Представьте себе, что вам нужно управлять парком серверов, расположенных к тому же в разных географических точках. Каждый из этих серверов требует настройки, регулярного обновления и мониторинга. Конечно, для решения этих задач можно воспользоваться самым простым способом: подключиться к каждому серверу по ssh и внести необходимые изменения. При всей своей простоте этот способ сопряжен с некоторыми трудностями: он чрезвычайно трудоемок, а на выполнение однообразных операций уходит очень много времени.

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

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

Со всеми сложностями, о которых идет речь выше, мы хорошо знакомы на собственном опыте: у нас имеется 10 точек присутствия с NS-серверами, расположенные в разных точках планеты. На них необходимо регулярно вносить различные изменения: обновлять операционную систему, устанавливать и обновлять различное ПО, изменять конфигурцию и т.п. Мы решили все эти операции автоматизировать и внедрить систему удаленного управления конфигурациями. Изучив имеющиеся решения, мы остановили свой выбор на Ansible.

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

Что такое Ansible?

Ansible — опенсорсное программное решение для удаленного управления конфигурациями, разработанное Майклом Де Хаанном в 2012 году. Название продукта взято из научно-фантастической литературы: в романах американской писательницы Урсулы Ле Гуин ансиблом называется устройство для оперативной космической связи.

Ansible берет на себя всю работу по приведению удаленных серверов в необходимое состояние. Администратору необходимо лишь описать, как достичь этого состояния с помощью так называемых сценариев (playbooks; это аналог рецептов в Chef). Такая технология позволяет очень быстро осуществлять переконфигурирование системы: достаточно всего лишь добавить несколько новых строк в сценарий.

Почему Ansible?

Преимущества Ansible по сравнению с другими аналогичными решениями (здесь в первую очередь следует назвать такие продукты, как Puppet, Chef и Salt) заключаются в следующем:

  • на управляемые узлы не нужно устанавливать никакого дополнительного ПО, всё работает через SSH (в случае необходимости дополнительные модули можно взять из официального репозитория);
  • код программы, написанный на Python, очень прост; при необходимости написание дополнительных модулей не составляет особого труда;
  • язык, на котором пишутся сценарии, также предельно прост;
  • низкий порог вхождения: обучиться работе с Ansible можно за очень короткое время;
  • документация к продукту написана очень подробно и вместе с тем — просто и понятно; она регулярно обновляется;
  • Ansible работает не только в режиме push, но и pull, как это делают большинство систем управления (Puppet, Chef);
  • имеется возможность последовательного обновления состояния узлов (rolling update).

Push или pull?

“Из коробки” все сценарии и команды выполняются методом push: когда возникает необходимость, мы запускаем сценарий, и он последовательно выполняется на удалённых серверах. Однако разработчики также предусмотрели метод pull и даже написали специальное приложение для установки необходимой для этого части ansible на удалённые хосты.

Установка

Требования для установки Ansible минимальны. На машине с которой производится управление должен быть установлен Python 2.6 или выше. На управляемых узлах должен быть установлен только Python версии не ниже 2.4, но он, как правило, по умолчанию включен в состав большинства дистрибутивов linux-систем. MS Windows не поддерживается.

Вам также могут потребоваться следующие модули Python, устанавливаемые через pip или пакетный менеджер вашей операционной системы:

  • paramiko;
  • PyYAML;
  • jinja2.

В Ubuntu установка самого Ansible и зависимостей осуществляется добавлением репозитория и установкой пакета:

sudo add-apt-repository -y ppa:rquillo/ansible
sudo apt-get update
sudo apt-get install ansible -y

О процедуре установки в других ОС можно прочитать в официальной документации.

Группы серверов

Список групп серверов, которыми нужно управлять, Ansible может получать двумя основными способами:

Файл hosts

Дефолтное расположение файла — /etc/ansible/hosts, но оно может также быть задано параметром окружения $ANSIBLE_HOSTS или параметром -i при запуске ansible и ansible-playbook. Содержимое этого файла может выглядеть, например, так (в квадратных скобках указаны имена групп управляемых узлов, ниже перечисляются входящие в эти группы серверы):

[dbservers]
one.example.com
two.example.com
three.example.com

[dnsservers]
rs1.example.com ansible_ssh_port=5555 ansible_ssh_host=192.168.1.50 rs2.example.com ns[01:50].example.com 

Помимо списка управляемых узлов, в файле hosts могут быть указаны и другие сведения, необходимые для работы: номера портов для подключения по SSH, способ подключения, пароль для подключения по SSH, имя пользователя, объединения групп и т.п. В некоторых случаях — в частности, при работе с большими и сложными конфигурациями, — различные параметры можно выносить в отдельные файлы и каталоги (о структуре каталогов см. ниже).

Более подробно о файле hosts и правилах его написания можно почитать в официальной документации.

Информация об узлах (Facts)

Перед внесением изменений Ansible подключается к управляемым узлам и собирает информацию о них: о сетевых интерфейсах и их состоянии, об установленной операционной системе и т.п. Он может делать это как с помощью собственного модуля, так и с помощью инструментов ohai и facter, если они установлены (такая возможность специально предусмотрена для пользователей, уже имеющих опыт работы с системами удаленного управления конфигурациями: ohai и facter являются библиотеками фактов для Chef и Puppet).

Переменные

Во время деплоя, как правило, требуется не только установить какое-либо приложение, но и настроить его в соответствии с определенными параметрами на основании принадлежности к группе серверов или индивидуально (например, ip-адрес BGP-соседа и номер его AS или параметры для базы данных). Как уже было сказано, загромождать файл hosts будет не очень красиво, поэтому разработчики Ansible пошли следующим путём:

  • файлы с переменными групп хранятся в директории “group_vars/имя_группы”;
  • файлы с переменными хостов в директории “hosts_vars/имя_хоста”;
  • файлы с переменными роли (о них речь пойдет ниже) в директории “имя_роли/vars/имя_задачи.yml”;

Помимо пользовательских переменных можно (и даже нужно) использовать факты, собранные ansible перед выполнением сценариев и отдельных задач.

Модули Ansible

В состав Ansible входит огромное количество модулей для развёртывания, контроля и управления различными компонентами, которые можно условно разделить на следующие группы (в скобках приведены названия некоторых продуктов и сервисов):

  • облачные ресурсы и виртуализация (Openstack, libvirt);
  • базы данных (MySQL, Postgresql, Redis, Riak);
  • файлы (шаблонизация, регулярные выражения, права доступа);
  • мониторинг (Nagios, monit);
  • оповещения о ходе выполнения сценария (Jabber, Irc, почта, MQTT, Hipchat);
  • сеть и сетевая инфраструктура (Openstack, Arista);
  • управление пакетами (apt, yum, rhn-channel, npm, pacman, pip, gem);
  • система (LVM, Selinux, ZFS, cron, файловые системы, сервисы, модули ядра);
  • работа с различными утилитами (git, hg).

О том, с чем умеет работать Ansible “из коробки”, можно прочитать в официальной документации. Список действительно впечатляет.

Примеры простых задач

С помощью Ansible можно одновременно выполнить одну задачу на целой группе серверов. Попробуем, например, отправить запрос ping на серверы выбранной группы:

$ ansible dnsservers -m ping
dns1.example.com | success >> {
    "changed": false,
    "ping": "pong"
}

dns2.example.com | success >> {
    "changed": false,
    "ping": "pong"
}

Следующий пример соберёт информацию о хостах и выведёт её на консоль в формате JSON:

$ ansible dnsservers -m setup

dns1.example.com | success >> {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "192.168.1.35"
        ], 
        "ansible_all_ipv6_addresses": [
            "fe80::ac2a:eaff:fe96:ea53"
        ], 
        "ansible_architecture": "x86_64", 
        "ansible_bios_date": "", 
        "ansible_bios_version": "", 
        "ansible_cmdline": {
            "barrier": "off", 
            "console": "ttyS0", 
            "panic": "15", 
            "ro": true, 
            "root": "UUID=c5412437-f80e-4db4-81bc-75f751a60792", 
            "xencons": "ttyS"
        }, 
        "ansible_date_time": {
            "date": "2013-10-04", 
            "day": "04", 
            "epoch": "1380891466", 
            "hour": "16", 
            "iso8601": "2013-10-04T12:57:46Z", 
            "iso8601_micro": "2013-10-04T12:57:46.130144Z", 
            "minute": "57", 
            "month": "10", 
            "second": "46", 
            "time": "16:57:46", 
            "tz": "MSK", 
            "year": "2013"
        }, 
        "ansible_default_ipv4": {
            "address": "192.168.1.35", 
            "alias": "eth0", 
            "gateway": "192.168.1.1", 
            "interface": "eth0", 
            "macaddress": "ae:aa:ea:96:ea:53", 
            "mtu": 1500, 
            "netmask": "255.255.255.0", 
            "network": "192.168.1.0", 
            "type": "ether"
        }, 
        "ansible_default_ipv6": {}, 
        "ansible_devices": {
            "xvda": {
                "holders": [], 
                "host": "", 
                "model": null, 
                "partitions": {
                    "xvda1": {
                        "sectors": "290816", 
                        "sectorsize": 512, 
                        "size": "142.00 MB", 
                        "start": "2048"
                    }, 
                    "xvda2": {
                        "sectors": "16482304", 
                        "sectorsize": 512, 
                        "size": "7.86 GB", 
                        "start": "292864"
                    }
                }, 
                "removable": "0", 
                "rotational": "0", 
                "scheduler_mode": "cfq", 
                "sectors": "16777216", 
                "sectorsize": "512", 
                "size": "8.00 GB", 
                "support_discard": "0", 
                "vendor": null
            }
        }, 
        "ansible_distribution": "Ubuntu", 
        "ansible_distribution_release": "precise", 
        "ansible_distribution_version": "12.04", 
        "ansible_domain": "", 
        "ansible_eth0": {
            "active": true, 
            "device": "eth0", 
            "ipv4": {
                "address": "192.168.1.35", 
                "netmask": "255.255.255.0", 
                "network": "192.168.1.0"
            }, 
            "ipv6": [
                {
                    "address": "fe80::ac2a:eaff:fe96:ea53", 
                    "prefix": "64", 
                    "scope": "link"
                }
            ], 
            "macaddress": "ae:aa:ea:96:ea:53", 
            "module": "xennet", 
            "mtu": 1500, 
            "type": "ether"
        }, 
        "ansible_form_factor": "", 
        "ansible_fqdn": "dns1.example.com", 
        "ansible_hostname": "dns1", 
        "ansible_interfaces": [
            "lo", 
            "eth0"
        ], 
        "ansible_kernel": "3.1.0-1.2-xen", 
        "ansible_lo": {
            "active": true, 
            "device": "lo", 
            "ipv4": {
                "address": "127.0.0.1", 
                "netmask": "255.0.0.0", 
                "network": "127.0.0.0"
            }, 
            "ipv6": [
                {
                    "address": "::1", 
                    "prefix": "128", 
                    "scope": "host"
                }
            ], 
            "mtu": 16436, 
            "type": "loopback"
        }, 
        "ansible_lsb": {
            "codename": "precise", 
            "description": "Ubuntu 12.04.3 LTS", 
            "id": "Ubuntu", 
            "major_release": "12", 
            "release": "12.04"
        }, 
        "ansible_machine": "x86_64", 
        "ansible_memfree_mb": 181, 
        "ansible_memtotal_mb": 1061, 
        "ansible_mounts": [
            {
                "device": "/dev/mapper/system-root", 
                "fstype": "ext4", 
                "mount": "/", 
                "options": "rw,errors=panic,barrier=0", 
                "size_available": 6332063744, 
                "size_total": 7798611968
            }, 
            {
                "device": "/dev/xvda1", 
                "fstype": "ext2", 
                "mount": "/boot", 
                "options": "rw", 
                "size_available": 110679040, 
                "size_total": 139539456
            }
        ], 
        "ansible_os_family": "Debian", 
        "ansible_pkg_mgr": "apt", 
        "ansible_processor": [
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz"
        ], 
        "ansible_processor_cores": 1, 
        "ansible_processor_count": 8, 
        "ansible_processor_threads_per_core": 1, 
        "ansible_processor_vcpus": 8, 
        "ansible_product_name": "", 
        "ansible_product_serial": "", 
        "ansible_product_uuid": "", 
        "ansible_product_version": "", 
        "ansible_python_version": "2.7.3", 
        "ansible_selinux": false, 
        "ansible_ssh_host_key_dsa_public": "AAAAB3NzdC1kc3MAAACBAI09PTx0Jv2dAhmwGoPV45G6ZEiZ84TwjVm6HYbGOHUZe+CKnYwWThD8ZqXYzRyvVxCcVefiS6m0PKY6a5id2GySyQlTM952bDaifd09ot9pCWjwNp5q4/EQdIG3R9Kt96DfsraVrvmJWG1qQMaUlnsiZzxHWv4Fn+7BvP0Kn6AtAAAAFQDIeO7uTIVR/kzNTV9xHN/uW6KJ8wAAAIALATT5RMZUQhtwz42ek8254hrlEqSyMnWyq+vCDOp+2rE/dIkcBcd+xnfV2lTkeizAMTzYETOE8IES4rXWKFf2AlBTk9IQDnZI0ABlpUmXQVZvHxl8pKwLwzRPA7XeW4f4bXQXimUPHzCdnrwxLj7Qht4JaspL2znMCKOtpwWBrAAAAIB45bgP1JIlVpWaj1FJ/NKhDDv5D9yM7GXaljsUXL1T7KGtZ9yMA+sJa7Sw/HF88ag/gjxe6kUwmkrsvtrsza3WpfaMYupKFZtJwmQabxYPM1QWAtVONxeSo30IimFLQuaj6tgzfD1faJVyDdFydWNDUfZ3cn5iNsCz6khsc241zQ==", 
        "ansible_ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHA43NTYAAABBBH3b5e6ZUbR+gMLMiOwcQzwuEPE+KIXHmzywNcOIltWY4ZiGRXlQZMyEFMENiOSivFHByMBV0wJj8VMxJocHd7s=", 
        "ansible_ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDH5WKsJ0UJ8LDQMDBCcbkbdDVXcG2lhdBOmxCVm128ztp3PJrHQoNwy1njit/Sty34HYvwjVXvuaT8ksCSAGhi8VPvRo+oqGaSdt3T39Ew5DsKeJTOZDqL1Vz1jNbPvvVjsdB7v34zTgEdnjuTzlwPvtNtXyTJonXC0KDlLl5WAiYSb9XpLB0rjjKAGNautp0Mgx6olWadpMT/NWT0Ub5yHBJCWK+mYAwq0M2tK+QSrsukmG93flGLboVlWTfMIM+UUR2MH3OxI7ew6Oc5P2ligH3rcHhcAWwXLIAsMJ5vcmH0+pEvTGr9ucNMbXoZzAhX3hPN+KG8hbZ+AX3z0TXn", 
        "ansible_swapfree_mb": 482, 
        "ansible_swaptotal_mb": 487, 
        "ansible_system": "Linux", 
        "ansible_system_vendor": "", 
        "ansible_user_id": "root", 
        "ansible_userspace_architecture": "x86_64", 
        "ansible_userspace_bits": "64", 
        "ansible_virtualization_role": "guest", 
        "ansible_virtualization_type": "xen"
    }, 
    "changed": false
}

dns1.example.com | success >> {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "192.168.1.43"
        ], 
        "ansible_all_ipv6_addresses": [
            "fe80::cc2b:97ff:fe7b:d221"
        ], 
        "ansible_architecture": "x86_64", 
        "ansible_bios_date": "", 
        "ansible_bios_version": "", 
        "ansible_cmdline": {
            "autorun": "fsck", 
            "barrier": "off", 
            "console": "xvc0", 
            "ro": true, 
            "root": "/dev/mapper/system-root"
        }, 
        "ansible_date_time": {
            "date": "2013-10-04", 
            "day": "04", 
            "epoch": "1380891479", 
            "hour": "16", 
            "iso8601": "2013-10-04T12:57:59Z", 
            "iso8601_micro": "2013-10-04T12:57:59.276859Z", 
            "minute": "57", 
            "month": "10", 
            "second": "59", 
            "time": "16:57:59", 
            "tz": "MSK", 
            "year": "2013"
        }, 
        "ansible_default_ipv4": {
            "address": "192.168.1.43", 
            "alias": "eth0", 
            "gateway": "192.168.1.1", 
            "interface": "eth0", 
            "macaddress": "ce:cb:97:7b:d2:21", 
            "mtu": 1500, 
            "netmask": "255.255.255.0", 
            "network": "192.168.1.0", 
            "type": "ether"
        }, 
        "ansible_default_ipv6": {}, 
        "ansible_devices": {
            "xvda": {
                "holders": [], 
                "host": "", 
                "model": null, 
                "partitions": {
                    "xvda1": {
                        "sectors": "290816", 
                        "sectorsize": 512, 
                        "size": "142.00 MB", 
                        "start": "2048"
                    }, 
                    "xvda2": {
                        "sectors": "12288000", 
                        "sectorsize": 512, 
                        "size": "5.86 GB", 
                        "start": "292864"
                    }
                }, 
                "removable": "0", 
                "rotational": "0", 
                "scheduler_mode": "cfq", 
                "sectors": "12582912", 
                "sectorsize": "512", 
                "size": "6.00 GB", 
                "support_discard": "0", 
                "vendor": null
            }
        }, 
        "ansible_distribution": "Debian", 
        "ansible_distribution_release": "NA", 
        "ansible_distribution_version": "7.0", 
        "ansible_domain": "", 
        "ansible_eth0": {
            "active": true, 
            "device": "eth0", 
            "ipv4": {
                "address": "192.168.1.43", 
                "netmask": "255.255.255.0", 
                "network": "192.168.1.0"
            }, 
            "ipv6": [
                {
                    "address": "fe80::cc2b:97ff:fe7b:d221", 
                    "prefix": "64", 
                    "scope": "link"
                }
            ], 
            "macaddress": "ce:cb:97:7b:d2:21", 
            "module": "xennet", 
            "mtu": 1500, 
            "type": "ether"
        }, 
        "ansible_form_factor": "", 
        "ansible_fqdn": "dns2.example.com", 
        "ansible_hostname": "dns2", 
        "ansible_interfaces": [
            "lo", 
            "eth0"
        ], 
        "ansible_kernel": "3.1.0-1.2-xen", 
        "ansible_lo": {
            "active": true, 
            "device": "lo", 
            "ipv4": {
                "address": "127.0.0.1", 
                "netmask": "255.0.0.0", 
                "network": "127.0.0.0"
            }, 
            "ipv6": [
                {
                    "address": "::1", 
                    "prefix": "128", 
                    "scope": "host"
                }
            ], 
            "mtu": 16436, 
            "type": "loopback"
        }, 
        "ansible_lsb": {
            "codename": "wheezy", 
            "description": "Debian GNU/Linux 7.0 (wheezy)", 
            "id": "Debian", 
            "major_release": "7", 
            "release": "7.0"
        }, 
        "ansible_machine": "x86_64", 
        "ansible_memfree_mb": 9, 
        "ansible_memtotal_mb": 547, 
        "ansible_mounts": [
            {
                "device": "/dev/mapper/system-root", 
                "fstype": "ext3", 
                "mount": "/", 
                "options": "rw,relatime,errors=panic,barrier=0,data=ordered", 
                "size_available": 3733434368, 
                "size_total": 5684838400
            }, 
            {
                "device": "/dev/xvda1", 
                "fstype": "ext2", 
                "mount": "/boot", 
                "options": "rw,relatime,user_xattr,acl,barrier=1", 
                "size_available": 112991232, 
                "size_total": 139539456
            }
        ], 
        "ansible_os_family": "Debian", 
        "ansible_pkg_mgr": "apt", 
        "ansible_processor": [
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz", 
            "Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz"
        ], 
        "ansible_processor_cores": 1, 
        "ansible_processor_count": 8, 
        "ansible_processor_threads_per_core": 1, 
        "ansible_processor_vcpus": 8, 
        "ansible_product_name": "", 
        "ansible_product_serial": "", 
        "ansible_product_uuid": "", 
        "ansible_product_version": "", 
        "ansible_python_version": "2.7.3", 
        "ansible_selinux": false, 
        "ansible_ssh_host_key_dsa_public": "AAAAB3szaC1kc3MAAACBAJFX2aR1G5QM57/3vLSlLmPR46nXNPAx0jtf6fPWkit/64W5FFBH7BW9YtPHGrucAagz1drKd9SiE+U5GlVqg/4xXOLMHmWUHitivVV9obtkyF2BM/+1OKTwxGIBP6Vu3YP/Wbpbv5TDCxjClWpZs3kCWrqRsScTdZTkk66YDTmbAAAAFQCEEjs6jtnyfF45scSgIxy60we9bQAAAIAzlb3pno+ljpE7yEjh6oBvl1RgUeYzwJZxHkBRMfOt30DyaCuXhNVhykhGYFqybv66BSu3C2br+Zk3peQRf6rie7QWV/lAXyDfInbGxgklFX6yAcd+JYj4u2vJ9j2k3GinnN9TLL3kafn0oqduy8sujozTCFZcG7dJx+4NZY29ZgAAAIBB94cFFAxC56HApvuRAcU/Wr+YeyKtJ3IHDz0hLRO+ziyuMgr2ajG80LNBGzG3rV2AEXSlH6egXaLfzcn9iPlB7VFpB/Fg/GZGOSpIUCFSSpEke6AoO8Z19Y5uR2EfcegyHhWVXGkIsaIon5KnH1bC//XAn9ir7AmANUCeXSz1Fg==", 
        "ansible_ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBM70PfnLbbXU+cJ27tWcKoom+P+TC08EncjB71bF4zp7Kw46YrWVjtPoFqAy3b1E2KkzUNcSrbJyEoCIgfzCC3Y=", 
        "ansible_ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABAQCqltJwL3ThbfWbwBSuaZ2zZNRtcrjld0Z/ulAM6sygWTjHIeIuxT1lbJJFfKZneyo29nPho1q/HAlYGDRdcDZhKufNqDN/c9iFDbjnuPvCetUxxf+t9jKnUHnqDpO+fLYbosIEio9cmS/pOEwAU4+VBB8mdNAj9fjqrE08xcdEgt8QnAjIRlKDCdtTuYbisyt96GR10RrLPkr0epqGmHE6vzC1PyidqmQkGuCrcJHPJiS40J7S8QVP11TRS4Un+V6B7fTcfoZcPDrMsPj/NpOVh3egCJGg5VRJ2D3Fmoapg/R3ZPrMD/AW+PNQLa+1GSIVTc3cNu4ctXgnwQJwSjWL", 
        "ansible_swapfree_mb": 412, 
        "ansible_swaptotal_mb": 487, 
        "ansible_system": "Linux", 
        "ansible_system_vendor": "", 
        "ansible_user_id": "root", 
        "ansible_userspace_architecture": "x86_64", 
        "ansible_userspace_bits": "64", 
        "ansible_virtualization_role": "guest", 
        "ansible_virtualization_type": "xen"
    }, 
    "changed": false
}

А вот так можно создать логический том (или, в зависимости от текущего состояния, изменить его размер) с именем examplevolume в группе examplegroup:

$ ansible dnsservers -m lvol -a "vg=examplegroup lv=examplevolume size=1024 state=present"
dns1.example.com | success >> {
    "changed": true,
    "msg": ""
}

dns2.example.com | success >> {
    "changed": false,
    "msg": ""
}

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

Cценарии (playbooks)

Все сценарии в Ansible пишутся на YAML. Это — человекочитаемый формат сериализованных данных, гораздо более простой, чем XML или JSON.

Чтобы выполнить сценарий используется команда ansible-playbook со следующим сиснтаксисом:

ansible-playbook <имя_файла_сценария.yml> ... [другие параметры]

В начале сценария обязательно должна присутствовать последовательность символов «–––» (так в YAML обозначается начало документа). Перед каждым новым разделом списка ставится дефис ( – ):

---
- hosts: webservers

Основными параметрами/группами простого сценария являются:

  • hosts — в нем указываются управляемые узлы или группы узлов, к которым нужно применить изменения;
  • tasks — здесь описывается состояние, в которое необходимо привести управляемый узел, альтернативой этому могут служить роли;

Также в сценарии перед непосредственным описанием задач могут быть указаны следующие параметры или группы параметров:

  • gather_facts — собирать или нет информацию о хостах перед выполнением задач, по умолчанию — да;
  • vars — в нем указываются различные переменные, которые будут использованы при выполнении сценария;
  • connection — можно указать метод соединения с хостами: pure ssh, paramiko, fireball, chroot, jail, local, accelerate (применимо также для выполнения отдельного модуля);
  • sudo — после установления соединения выполнять задачу с привилегиями другого пользователя, по умолчанию другой пользователь — root;
  • sudo_user — в сочетании с предыдущим параметром можно указать с привилегиями какого именно пользователя будет выполнена задача;
  • vars_prompt — перед выполением плэйбука Ansible в интерактивном режиме может уточнить указанные в этом разделе параметры;
  • remote_user (в предыдущих версиях — просто user) — имя пользователя для авторизации на удалённом хосте.

Рассмотрим все эти разделы более подробно.

В разделе hosts указывается группа управляемых узлов, к которой будут применены описываемые в сценарии изменения.

Так, строка формата:

hosts: webservers

означает, что изменения будут применены к узлам из группы webservers.

Сценарии могут выполняться не только от имени пользователя, под именем которого установлено соедиение, но и любого другого. В следующем примере авторизация на хосте будет произведена с именем yourname, но задачи будут выполняться от имени пользователя root (если, конечно, этому пользователю это разрешено):

---
- hosts: webservers
  user: yourname
  sudo: yes

Если добавить параметр “user: postgres”, то все действия будут выполняться с привилегиями пользователя postgres.

В разделе vars указываются переменные, которые будут использованы в сценарии, и их значения:

- hosts: webservers
  vars:
    http_port: 80
    max_clients: 200

Список изменений, которые необходимо произвести на управляемом узле, приводится в разделе tasks. Каждой задаче (task) присваивается имя (name). Далее указываются модули Ansible, которые будут задействованы при ее выполнении:

- hosts: webservers
  user: yourname
  tasks:
    - service: name=nginx state=started

Для каждой задачи можно указывать пользователя, от имени которого она будет выполнена:

---
- hosts: webservers
  user: yourname
  tasks:
    - service: name=nginx state=started
      sudo: yes

Шаблонизация

В Ansbile используется шаблонизатор Jinja2. Приведём пример шаблона (часть конфига powerdns):

# пароль для подключения к базе данных
gpgsql-password={{ lookup('password', 'credentials/' + inventory_hostname + '/postgresql/powerdns', length=15) }}

# IPv4-адрес, который будет “слушать” powerdns
local-address={{ ansible_default_ipv4.address }}

# IPv6-адрес, который будет “слушать” powerdns
local-ipv6={{ ansible_default_ipv6.address }}

# nsid dns-сервера (EDNS option 3, rfc5001)
server-id={{ ansible_hostname }}

В приведённом примере мы подставляем в шаблон следующие значения:

  • из заранее собранных фактов о хосте:
    • ansible_default_ipv4.address — основной IPv4-адрес хоста;
    • ansible_default_ipv6.address — основной IPv6-адрес хоста;
    • ansible_hostname — имя хоста (результат выполнения команды hostname).
  • inventory_hostname — имя хоста в инвентарном файле;
  • пароль пользователя powerdns из внешнего источника данных (в данном случае файл) для подключения к базе Postgresql , полученный с помощью lookup-плагина password из стандартной поставке. Особенность некоторых lookup-плагинов — если данных нет, то они могут их сгенерировать и сохранить для последующего использования.

Обработку шаблонов и, в данном случае, генерацию конфигурационного файла выполняет модуль template; он же может задать необходимые права доступа и изменить владельца/группу:

- name: generate powerdns config
  template: src=pdns.conf.j2 dest=/etc/powerdns/pdns.conf owner=powerdns group=powerdns mode=600

Обратим внимание на то, что файл шаблона и файл с паролем пользователя базы данных находятся на машине управления, а результатом будет файл на удалённом узле.

Обработчики событий (Handlers)

Ansible не просто выполняет задачи в указанном порядке, но и проверяет их состояние на наличие изменений. Если при выполнении сценария требовалось, например, добавить строку в конфигурационный файл, и в результате выполнения он изменился (необходимой строки действительно не было), то Ansible может выполнить специальную задачу, описанную как обработчик события (handler). Если при выполнении строка уже была в конфигурационном файле, то обработчик выполнен не будет. Обработчики событий описываются в конце сценария; в описании задачи они указываются через параметр notify. Приведём пример:

---
- hosts: webservers
  vars:
    max_clients: 200

  tasks:
    # сгенерируем файл конфигурации на основе шаблона
    # и укажем, что требуется выполнить задачу “restart apache”
    # если файл изменился
  - name: write the apache config file
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf
    notify:
    - restart apache

  - name: ensure apache is running
    service: name=httpd state=started

  # раздел описания обработчиков
  handlers:
    - name: restart apache
      # используем модуль service для перезапуска веб-сервера
      service: name=httpd state=restarted

Контроль выполнения

Допустим, что при выполнении сценария нам нужно проверять определённые переменные или состояния и, в зависимости от них, выполнять или не выполнять какие-либо задачи. Для этого можно использовать оператор “when”:

tasks:
      # сохраняем файл шаблона и сохраняем результат задачи
      # в переменную last_result
    - template: src=/templates/foo.j2 dest=/etc/foo.conf
      register: last_result
      # проверяем переменную last_result.changed и если она имеет
      # значение true - задача будет выполнена, иначе - будет пропущена
    - command: echo 'the file has changed'
      when: last_result.changed

Делегирование задачи другому хосту

Иногда требуется выполнить задачу на определённом узле, но в контексте другого узла. Например, во время обновления узла может возникнуть необходимость отключить для него мониторинг, находящийся на отдельном сервере. Для этого используется управляющая директива delegate_to. Приведём пример:

- name: disable nagios alerts for this host webserver service
  nagios: action=disable_alerts host={{inventory_hostname}} services=dnsserver
  delegate_to: mon_host.example.com

Результатом выполнения этой задачи будет отключение сообщений для сервиса dnsserver в Nagios.

Роли

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

--- 
- name: check and apply basic configuration to all hosts
  hosts: all
  roles:
    - common

- name: check and apply configuration to group1
  hosts: group1
  roles:
    - pgsql

- name: check and apply configuration to group2
  hosts: group2
  roles:
    - fooapp

Структура проекта

├── production                # инвентарный файл для продакшн-серверов
├── stage                     # инвентарный файл для stage-окружения
│
├── group_vars/
│   ├── group1                # здесь назначаются переменные для
│   └── group2                # конкретных групп
├── host_vars/
│   ├── hostname1             # специфические переменные для хостов в
│   └── hostname2             # случае необходимости прописываются здесь
│
├── site.yml                  # основной сценарий
├── webservers.yml            # сценарий для веб-сервера
├── dbservers.yml             # сценарий для сервера базы данных
│
└── roles/
    ├── common/               # здесь описываются роли
    │   ├── tasks/            #
    │   │   └── main.yml      # - файл задач роли, может включать файлы
    │   │                     #   меньшего размера
    │   ├── handlers/         #
    │   │   └── main.yml      # - файл с обработчиками (handlers)
    │   ├── templates/        # - директория для шаблонов, в данном
    │   │   └── ntp.conf.j2   #   случае - для конфига ntp
    │   ├── files/            #
    │   │   ├── bar.txt       # - файл-ресурс для копирования на хост
    │   │   └── foo.sh        # - скрипт для выполнения на удалённом хосте
    │   └── vars/             #
    │       └── main.yml      # - ассоциированные с ролью переменные
    │
    ├── pgsql/                # такая же структура, как выше, для роли pgsql
    └── fooapp/               # такая же структура, как выше, для роли fooapp

Пример сценария

Чтобы понять, как это все работает, рассмотрим практический пример: простой сценарий развёртывания новой версии PostgreSQL 9.3 на debian-based ОС. Роли в этом примере не используются.

Спойлер
---
- name: install postgresql 9.3 # имя playbook'a
  # секция, описывающая параметры, которые нужно уточнить у пользователя в начале запуска
  vars_prompt:
    hosts: "Please enter hosts group name" # спрашиваем имя группы серверов в инвентаре (в нашем случае файл $ANSIBLE_HOSTS)
    username: "Please enter username for auth" # спрашиваем имя пользователя для подключения к серверам
  hosts: $hosts # 
  user: $username
  sudo: True
  accelerate: true
  vars:
    app_username: 'app_user'     # имя пользователя мифического приложения, которое работать с базой данных
    app_host_ip: '192.168.0.100' # ip-адрес хоста с запущенным приложением, с него будут поступать запросы в базу данных
    app_database_name: 'appdb'   # имя базы данных приложения

  tasks:
      # Проверяем установлен ли и устанавливаем пакет python-software-properties
      # для обеспечения работы модуля apt. Параметры модуля:
      # pkg - имя пакета для установки
      # state - устанавливаем последнюю версию пакета, 
      # update_cache - обновляем список доступных пакетов перед установкой
    - name: check add-apt-repository 
      apt: pkg=python-software-properties state=latest update_cache=yes

      # добавляем ключ официального apt-репозитория проекта postgresql
      # Параметры модуля:
      # url - URL до файла с ключём
      # state - добавить ключ
    - name: add apt key
      apt_key: url=http://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc state=present

      # добавляем репозиторий, адрес формируется на основе имени релиза установленной ОС
    - name: add apt repo
      apt_repository: repo='deb http://apt.postgresql.org/pub/repos/apt/ ${ansible_lsb.codename}-pgdg main'

      # устанавливаем пакет с ключём для последующего возможного автоматического обновления
    - name: install pgdg-key
      apt: pkg=pgdg-keyring state=latest update_cache=yes

      # устанавливаем пакеты postgresql-9.3 (непосредственно сам сервер баз данных)
      # и python-psycopg2 - для работы модулей postgresql_user, postgresql_db, postgresql_privs
    - name: install packages
      apt: pkg=$item state=latest
      with_items:
        - postgresql-9.3
        - python-psycopg2
        - python-keyczar

      # создаём пользователя для работы нашего мифического приложения c атрибутом LOGIN
      # сгенерированный пароль будет сохранён в credentials/имя_хоста/postgres/имя_пользователя
    - name: create postresql user for some app
      # выполнение задачи будем производить с правами пользователя postgres (создаётся при установке postgresql)
      sudo: yes
      sudo_user: postgres 
      postgresql_user:
          user=${app_username}
          password="{{ lookup('password','example/credentials/' + inventory_hostname + '/postgres/' + app_username, length=15) }}"
          role_attr_flags=LOGIN

      # создаём базу данных для мифического приложения с говорящими за себя параметрами
    - name: create db for our app
      sudo: yes
      sudo_user: postgres
      action: postgresql_db name=${app_database_name} owner=${app_username} encoding='UTF8' lc_collate='en_US.UTF-8' lc_ctype='en_US.UTF-8' template='template0' state=present

      # Следующая задача будет выполнена хосте приложения, а не на текущем настраиваемом хосте
    - name: add app_user password to .pg_pass file on server with our app
      sudo: yes
      sudo_user: ${app_username}
      delegate_to: ${app_host_ip}
      lineinfile:
          dest=/home/${app_username}/.pgpass
          regexp='^{{ inventory_hostname }}\:\*\:${app_database_name}\:${app_username}'
          line='{{ inventory_hostname }}:*:${app_database_name}:${app_username}:{{ lookup('password','example/credentials/' + inventory_hostname + '/postgres/' + app_username, length=15) }}'
          create=yes
          state=present
          backup=yes

      # добавляем в pg_hba.conf строчку, описываюшую разрешение подключение с ip-адреса приложения для ранее созданной базы и пользователя
    - name: add entry to pg_hba.conf
      lineinfile:
          dest=/etc/postgresql/9.3/main/pg_hba.conf
          regexp='host ${app_database_name} ${app_username} ${app_host_ip}/32 md5'
          line='host ${app_database_name} ${app_username} ${app_host_ip}/32 md5' state=present
      # если файл изменился, то вызовем задачу по перечитыванию конфига postgresql
      # напоминаем что модули ansible возвращают состояние "изменилось/не изменилось" после выполнения,
      # хэндлеры описываются либо в конце playbook'a или в отдельном файле
      notify:
        - reload postgres

      # по умолчанию postgresql слушает только localhost
      # изменияем соответствующий параметр в postgresql.conf на ip-адрес сервера
    - name: add entry to postgresql
      lineinfile:
          dest=/etc/postgresql/9.3/main/postgresql.conf
          regexp='^listen_addresses'
          line="listen_addresses = '${ansible_default_ipv4.address}'"
          state=present
      # если файл изменился, то вызовем задачу по перезапуску postgresql, т.к.
      # параметр listen_addresses можно изменить только перезагрузкой сервера postgresql
      notify:
        - restart postgres

  # описание хэндлеров
  handlers:
      # перечитываем конфигурацию postgresql
    - name: reload postgres
      sudo: yes
      action: service name=postgresql state=reloaded

      # перезагружаем postgresql
    - name: restart postgres
      sudo: yes
      action: service name=postgresql state=restarted

Ansible AWX

Во всех приведенных выше примерах управление Ansible осуществляется с помощью интерфейса командной строки. Но с официального сайта можно загрузить графическую панель управления Ansibleworks AWX, очень симпатичную внешне и удобную в использовании. Собственно, за счет ее распространения и осуществляется монетизация продукта: управление небольшим (до 10) количеством серверов обходится бесплатно, если же серверов больше — нужно приобретать лицензию. Похожие варианты монетизации используются и разработчиками конкурирующих решений — Puppet и Chef.

Заключение

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

Для желающих узнать больше — несколько ссылок:

https://github.com/ansible/ — официальный аккаунт на github c исходным кодом проекта и хорошим набором примеров проектов

http://www.ansibleworks.com/docs/ — официальная документация;

http://jpmens.net/2012/06/06/configuration-management-with-ansible/ — статья Яна Пита Менса об управлении конфигурациями с помощью Ansible (в его блоге есть и много других материалов по теме).

https://gist.github.com/marktheunissen/2979474 — пример сценария с подробными комментариями, правда для старой версии.

http://www.ansibleworks.com/docs/contrib.html — ещё больше ссылок на примеры использования, включая в том числе и очень сложные конфигурации.

ansible-playbook <filename.yml> … [options]