Об Ansible

2015-06-08

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

Ansible + Docker

С одной стороны — хорошо. Не использовал я большие штуки вроде Chef или Puppet. За ними стоят серьезные и даже вроде научные концепции, где важным является не выполнение какой-то команды на сервере, а именно приведение сервера в какое-то описанное состояние.

Я пользовался Fabric, который представляет собой лишь питоновую библиотечку для одновременного удаленного выполнения команд на туевой хуче серверов. Ansible не далеко ушел от этой концепции. Всё, что ему нужно, это ssh до машины, да Python 2.7 на той стороне. Ну иногда некоторые дополнительные штуки. И да, Ansible выполняет команды, точнее питоновые скрипты, на удаленной стороне. И у него уже искаропки имеется куча мегаполезных скриптов — модулей.

В другой стороны — как-то не очень. Сыроват наш Ansible. Иногда ведет себя совсем не так, как хотелось бы. Иногда капризничает на ровном месте.

В подражение Chefу да Puppetу многие модули приводят удаленную систему в некоторое состояние. Таким образом, выполнение этих модулей является идемпонентным. Скажем, запуск сервиса - service: name=httpd state=started требует, чтобы httpd был запущен. И, если он действительно запущен, ничего предприниматься не будет.

Но, в отличие от автора Bash Booster, создатели Ansible не заморачивались обязательностью идемпотентности. Вполне можно запустить любую команду на сервере - command: /usr/bin/make_database.sh arg1 arg2 и она будет выполняться всегда, ибо модуль command будет не в состоянии понять, в правильное ли состояние перешла система при выполнении команды, или же надо повторить. Можно даже попросить сделать - service: name=httpd state=restarted, restarted — это такое "состояние", когда сервис был рестартован, т.е. рестарт делается всегда.

Ansible настолько сырой, что, поставив свеженькую версию 1.9.1 из PPA, я тут же поймал баг. Дело в том, что в Ubuntu 14.04 таки уже еще используется Upstart, но и инит скрипты, что в /etc/init.d/ лежат, все еще сохранились. Вот только в новомодных пакетах, в том числе и Dockerа, эти скрипты ничего не делают, а лишь вежливо ругаются, что, мол, пользуйтесь командой service вместо прямого вызова скрипта. А вот свежий Ansible внезапно решил, что инит скрипты важнее команды service и пытается использовать именно их. И в результате не может ни запустить, ни перезапустить сервисы. Тупой косяк, который лечится правкой одной строчки. Но он есть.

Ansible настолько быстро развивается, что функционал и набор параметров у docker модуля вот прям сильно отличается от версии к версии. И, кажется, даже не всегда с сохранением обратной совместимости. Но доставляет боли больше всего зависимость от docker-py.

Вроде как Ansible требует на удаленном хосте лишь возможность подключения по ssh, да наличие Python. Но это не всегда так. Иногда встречаются зависимости — какие-то программки или питоновые модули, которые должны быть установлены на сервере. В данном случае ансибловый модуль docker использует питоновый модуль docker-py для общения c docker демоном (и управления контейнерами) на удаленном хосте. И вот эту зависимость надо поставить самому, например, выполнив отдельный ансибловый таск. Почему Ansible не заботится об установке всех нужных зависимостей самостоятельно?

Случай с docker-py усугубляется еще и тем, что конкретная версия Ansible гвоздями прибита к конкретной версии docker-py. Более новые или более старые версии не годятся. Ибо их использование приводит к невнятным питоновым стектрейсам, тупо об отсутствии каких-то атрибутов у объектов. И какая именно версия нужна — нигде не указано. Приходится вбивать стектрейс в Гугль, и находить в комментариях на каком-нибудь форуме, что "я откатился до версии 1.1.0 и это помогло".

assimoolated

Сам модуль docker действительно очень сырой. У меня к нему есть несколько замечаний. Может, доберусь, и сам запулреквестю фиксы.

Есть тупая недоработка. Можно работать только с именем контейнера, тем самым, которое по умолчанию Docker назначает веселенькими вроде 'tender_heisenberg', но нельзя с его id, которое хэш или часть хэша. Хотя все консольные команды Dockerа прекрасно воспринимают и то, и то.

Ansible в очередной раз старается изобразить идемпотентность задачи. Можно указать, сколько именно контейнеров из данного образа нужно запустить. И он запустит именно столько, сколько укажешь. Ни больше, и ни меньше. И вот совсем нельзя сказать, что нужен еще один контейнер из этого образа, +1 к общему количеству. Ну да, это же непредсказуемое изменение состояния, которое есть количество запущенных контейнеров.

Плоховата, точнее не совпадает с моими ожиданиями, проверка изменений в реестре Docker. Если я говорю, что хочу запустить контейнеры из образа, и точно знаю, что образ обновился, я все же хочу чтобы контейнеры были перезапущены/пересозданы из нового образа. Но Ansible не будет трогать/обновлять контейнеры, если они уже запущены.

Может, я продолжаю думать о запуске команд на сервере, а не о приведении его в некоторое состояние? Но в таком случае, я не могу выразить желаемое состояние теми средствами, что сейчас есть в docker модуле. Да вот, например, говорю Ansible, что хочу видеть запущенными меньше контейнеров данного образа, чем запущено сейчас. Я, в ожидании лучшего, хочу видеть убитыми более старые контейнеры, чтобы более новые, возможно, с более новой версией моего кода, остались жить. Но нет, Ansible прибивает первые контейнеры из списка, т.е. самые новые. Нет, не понимаем мы друг друга.

Ну ладно, Docker, хипстота и все такое. А вот посмотрим на вопрос копирования файлов. Есть модуль copy, в чьей документации честно написано: когда файлов много, я буду медленным, используйте вместо меня модуль synchronize. Ноу проблем. По синтаксису вызова модули похожи. Меняем слово 'copy' на слово 'synchronize'. Не работает.

Почему не работает? Потому что copy копирует файлы через то же ssh соединение, через которое Ansible взаимодействует с удаленным хостом. А synchronize запускает чудесный rsync. А rsynс создает свое собственное ssh подключение к удаленному узлу. А разработчики Ansible не приложили никаких усилий к тому, чтобы ssh, запущенный rsync, заполучил те же параметры подключения, что использует сам Ansible. Ну разве что имя ssh юзера задает правильно.

Может, это и хорошо. Может, это и правильно. Может, это и секурно. В конце концов, правильные настройки в ~/.ssh/ и запущенный ssh-agent — это вопрос личной гигиены каждого. Но в случае Vagrant окружения это не решается так просто. Ну и, в конце концов, можно же было типовые случаи хотя бы в документации описать?

Automate!

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

А посмотрите, как Ansible собирает факты. Это переменные со сведениями о каждом сервере. Тут сведения о CPU, памяти, сети, дисках, и тонне всякого прочего. Их можно использовать в выполняемых задачах, в шаблонах файлов, которые надо сгенерить и закинуть на удаленный хост, повсюду. И столько всего уже искаропки известно Ansible. И это уже запаковано в красивый JSON.

Вот только сведений о Docker контейнерах явно не хватает. Но ассортимент сборщиков фактов легко пополнить. Мне вот, пришлось написать башевые скрипты, которые перечисляют docker контейнеры, которые нужно балансировать. Да, баш скрипт, который выводит JSON. Костыльно, но работает. Можно будет модуль Ansible по мотивам наваять.

Good Luck

Cсылки:

UPD1

Почему Ansible не заботится об установке всех нужных зависимостей самостоятельно?

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

Вполне можно запустить любую команду на сервере - command: /usr/bin/make_database.sh arg1 arg2 и она будет выполняться всегда, ибо модуль command будет не в состоянии понять, в правильное ли состояние перешла система при выполнении команды, или же надо повторить.

Есть параметр creates=, и тогда если файл уже есть - считается что команда уже отработала. Отлично работает для всяких gem install и подобного. removes= тоже есть, но мне пока ни разу не пригодился.

А разработчики Ansible не приложили никаких усилий к тому, чтобы ssh, запущенный rsync, заполучил те же параметры подключения, что использует сам Ansible

Естессно. Если всего что надо у тебя не написано в ~/.ssh/config, то надо всем по отдельности сообщать отличия. Кстати, в наше время (год назад) synchronize не было вообще, и делали руками local_action: command rsync. (ссылка на монтипайтоновский скетч four yorkshiremen).

UPD2

По поводу ansible: Он каждый чих выполняет ssh(в смысле нативный) поэтому всё так медленно но его можно ускорить если использовать https://docs.ansible.com/playbooks_acceleration.html . Тогда он сделает один запрос по ssh, и откроет порт для акселератора. Если я правильно понял он запустит ZeroMQ на указанном порту и будет через него работать.

UPD3

По поводу docker: по идее это средство контэйнеризации сервисов. а сервисы надо масштабировать + сервисы время от времени имеют свойство зависать и падать + сервера на которых работают сервисы имеют свойство зависать и падать + нужно иметь возможность балансировать нагрузку промеж сервисов и тут уже ansible безсилен

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

Мне как то больше нравится подход автоматической оркестрации контэйнеров.

Маленький пример в kubernetes + flannel(vxlan) описывается контроллер + сервис который маршрутизирует траффик на нужные сервисы на нужных нодаж в кластере

чтобы добавить ноду в кластер просто настраиваешь несколько сервисов на новом железе, и кластер начинает её использовать

если нужно добавить или уменьшить количество контэйнеров по одной и той же задаче, просто изменяешь их количество в реплик контроллере, и кластер сам решает где кого пристрелить, а где кого запустить основываясь на нагрузке на те или иные ноды. все продолжают быть доступны по clusterIP баллансировка на уровне tcp

example kubectl scale —replicas=30 replicationcontrollers frontend-controller