Представьте себе, что вам нужно управлять парком серверов, расположенных к тому же в разных географических точках. Каждый из этих серверов требует настройки, регулярного обновления и мониторинга. Конечно, для решения этих задач можно воспользоваться самым простым способом: подключиться к каждому серверу по 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).
Установка
Требования для установки 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 может получать двумя основными способами:
- из специального текстового файла (далее этот вариант будет рассмотрен более подробно);
- с помощью внешнего скрипта, возвращающего нужный нам список серверов, например из MongoDB. В официальном github-репозитории есть готовые скрипты для получения списка из Cobbler, Digital Ocean, EC2, Linode, OpenStack Nova, Openshift, Spacewalk, Vagrant, Zabbix.
Файл 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 (если, конечно, этому пользователю это разрешено использовать sudo):
---
- 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 — пример сценария с подробными комментариями, правда для старой версии.
www.ansibleworks.com/docs/contrib.html — ещё больше ссылок на примеры использования, включая в том числе и очень сложные конфигурации.
Для тех кто не может комментировать посты на Хабре, приглашаем к нам в блог.