У меня есть три сервера, но я не профессиональный сисадмин. Это означает, что несмотря на четыре базы данных и стопяцот приложений, бэкапы нигде не ведутся, к любой проблеме на сервере я подхожу, шумно вздохнув и бросив тарелку в стену, а операционные системы там достигли EOL два года назад. Я бы рад обновить, но на это нужно выделить, наверное, неделю, чтобы всё забэкапить и переставить. Проще забыть про yum update
и apt-get upgrade
.
Конечно, это неправильно. Я давно присматривался к chef и Puppet, которые, как я думал, решат все мои проблемы. Но я смотрел на конфиги знакомых проектов и откладывал. Это же нужно изучать, разбираться с ruby, бороться с многочисленными, по отзывам, косяками и ограничениями. Две недели назад статья Георгия amarao стала животворящим пинком. Даже не сама статья, а перечисление систем управления конфигурацией. После чтения комментариев и лёгкого гугления решил: возьму Ansible. Потому что питон, и на проблемы никто не жалуется.
Что ж, тогда я первым буду.
Сначала я нарыл кучу документации и учебников по Ansible, начиная с бесполезного видеоролика Quick Start на официальном сайте. Их, конечно, много, сделаны для разных задач и написаны разными людьми, но объединяет их одно: учебники делали для людей, которые уже понимают Ansible. Для людей со сферическим сервером в вакууме, которым достаточно подсказать, что бывают роли, модули и таски. Но я пришёл с clean slate и собрал все грабли, какие нашёл. Надеюсь, эта заметка поможет вам их обойти.
От систем управления конфигурациями я ждал чудес, вроде автоматически обновляемых приложений из git. Но оказалось, что Ansible — это лишь способ сохранить последовательность действий при настройке нового сервера. Вы сможете сделать в Ansible только то, что умеете делать из консоли самостоятельно. Чудес нет.
Начало. Vagrant
Задача: не делаю новый хост, потому что хочу сохранить ip. То есть, очищу дроплет через контрольную панель, затем инициализирую с помощью Ansible. План: написать playbook и отладить его на Vagrant.
Начать очень сложно. Все учебники по Ansible начинаются с описания inventory, куда нужно прописывать адрес сервера. Но какой ip у вагранта? Чёрт его знает. В документации по Ansible есть инструкция, как запустить playbook в Vagrant; в документации по Vagrant есть инструкция по подключению Ansible, и они не то чтобы идентичны. В итоге, забил на поиск ip и взял общее: минимальный Vagrantfile
, который запускает playbook.
Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/xenial64"
config.vm.network "forwarded_port", guest: 80, host: 8080
# так и не знаю, зачем это:
config.ssh.insert_key = false
config.vm.provision "ansible" do |ansible|
ansible.verbose = "v"
ansible.playbook = "playbook.yml"
end
end
Набросал черновик playbook-а, создал заготовки ролей и запустил vagrant up
. Не взлетело. Поскольку официальный образ xenial — только для VirtualBox, а в Fedora Linux виртуализация через libvirt. Долго вспоминал правильную команду: vagrant up --provider virtualbox
. Затем правил синтаксические ошибки в yaml (зачем там в начале обязательные три дефиса?). Помним, что после запуска коробки для перезапуска Ansible пишем vagrant provision
.
И первый сюрприз: в коробке Ubuntu 16.04 нет python по умолчанию! Дикость для федоры, где пакетный менеджер написан на питоне. Ansible, как я узнал, загружает свои модули на сервер и выполняет их там. Идём на StackOverflow, находим волшебный таск (точнее, десять вариаций одного таска и непонятно, как лучше):
- name: Install python for Ansible
become: yes
raw: test -e /usr/bin/python || (apt -qy update && apt install -y python-minimal)
register: output
changed_when: output.stdout
Суперпользователь, become!
Даже с документацией и примерами многое непонятно. Не понимаю, например, почему Vagrant переопределяет remote_user
, и как так получается, что в каждой коробке свой суперпользователь. Я же буду запускать playbook на чистом сервере, где будет только root, и нужно будет сделать своего суперпользователя. Но делать это под вагрантом нужно иначе, чем на чистом сервере, видимо. Вообще непонятно: получится два playbook, для стейджинга и для продакшена?
Или вот become
и become_user
: одно не подразумевает другого. Что из этого нужно указывать в корневом playbook, если для настройки сервера постоянно нужно включать рута? Я сначала поставил туда become: yes
и в каждом втором таске писал become_user: root
. Потом оказалось, что без become_user
тоже всё работает от рута! Потому что root — это значение по умолчанию и я, по сути, с самого начала сделал sudo -i
без возможности отпустить.
Где-то тут я вспомнил, что давно не обновлял систему на своём ноутбуке, и запустил dnf update
. Продолжая колупаться с плейбуком. Vagrant работал, а dnf в соседней вкладке обновлял VirtualBox. Кажется, так делать не нужно, потому что очередной vagrant provision
сказал: «всё сломалось и я не виноват». Ему не хватало VirtualBox, который «terminated unexpectedly during startup with exit code 1 (0x1)» — и хоть ты тресни. Команда vboxheadless -h
(я не настоящий девопс, я гуглил) показала ошибку -1912. В интернете все как один отвечают: переустанови VirtualBox. Хрен там, не помогает. Отчаявшись, нашёл коробку xenial для libvirt и перешёл на него. Хорошо, когда есть выбор.
Из какого-то примера скопировал таск вызова apt с кучей параметров, а потом узнал, что update_cache=yes
хорошо бы сделать отдельной задачей. И эта задача, вот беда, всё время возвращает «changed». Оказалось, нужно прописать cache_valid_time=3600
, чтобы проверять обновления не чаще раза в час. Сначала подумал написать 86400 (сутки), но я же не в кроне буду Ansible вызывать, а раз в месяц — пусть живёт.
Развернём базу данных
Установка PostgreSQL — пять строчек в консоли или целая эпопея в Ansible. В определённый момент нужно сделать become_user: postgres
. И тут коробка выдала странную ошибку: «Failed to set permissions on the temporary files Ansible needs to create when becoming an unprivileged user». Помните, как Ansible загружает модули на сервер и там запускает? Ну так вот, загружает он их от root или от другого суперпользователя, а потом у пользователя postgres нет к ним доступа. Вот незадача.
StackOverflow снова в помощь: оказывается, есть три выхода. Один из них — сделать ansible.cfg
и прописать внутрь pipelining=True
(а для решения какой-то другой возникшей проблемы я временно ставил pipelining=False
). Второй выход — буквально, «не делайте так». И третий самый простой: ставите пакет acl
и всё волшебным образом работает. Вернее, не работает другим способом: «sudo: a password is required». Ну что за дела, откуда здесь вообще пароли, я же с ключом захожу?
Оказалось, захожу в виртуалку без ключа, пользователем vagrant. Который был сделан до нас и за нас. Ansible при become_user
, видимо, делает sudo -u postgres
, а оно требует пароль пользователя vagrant. Пароля нет.
Начинаю перебирать варианты. become_method: su
вылетает по таймауту, потому что сервер спрашивает пароль, а Ansible этого не понимает. Что он там делает — непонятно, потому что у меня sudo su postgres
пароль не спрашивает. Есть вариант в файле /etc/sudoers.d/vagrant
прописать «vagrant ALL=(ALL) ...
», потому что слово в скобках позволит делать sudo -u
без пароля. Но тогда playbook становится заточенным под Vagrant, а мне его ещё в проде запускать. Неаккуратненько.
От безысходности пробую вообще убрать become
. Постгрес ожидаемо цедит: «Peer authentication failed for user „postgres“». Выкапываю стюардессу. Новый план: запускать роль под пользователем zverik, у которого есть все на свете права. Разбиваю playbook на два: в первом устанавливаю питон и делаю пользователя, вторым ставлю и настраиваю всё остальное с remote_user: zverik
. Запускаю. И снова «sudo: a password is required». Почему? А, ну да, Vagrant передаёт значение remote_user
и не даёт его поменять. Ну блин.
Чтобы отвлечься, открыл текстовый редактор и начал писать эти заметки. К этому моменту я разбираюсь с Ansible вот уже неделю по полтора-два часа, а ещё даже базу данных в постгресе не создал. В учебниках это всё выглядит так просто… Посчитал вкладки, связанные с Ansible в фаерфоксе: 48 штук. Сорок восемь. Примерно одна шестая от общего числа.
Тут я отключил ansible.force_remote_user
в Vagrantfile и перезапустил provision
. Ура, новая ошибка! Напоминает, что вход пользователем zverik работает только по сертификату. Но у меня же есть сертификат, и vagrant ssh -p
работает и впускает без пароля. Нагуглил решение: нужно указать путь к сертификату в ansible.cfg
. Оно не сработает по той же причине, что и remote_user
: Vagrant побеждает. На этот раз проще переопределить главную переменную: добавляем в playbook «ansible_ssh_private_key_file: "{{ lookup('env', 'HOME') }}/.ssh/id_rsa"
» и всё работает! Не очень красиво получилось, но ура!
После того, как я разобрался с пользователями, написание ролей пошло как по маслу. Уже готова одна роль из шести, шестьдесят тасков. Но начать сложнее, чем кажется по учебникам.
Полезные штуки
Во время написания playbook-ов находишь или нагугливаешь много полезных мелочей. Какие-то описаны в документации, какие-то — в статьях (поищите «Ansible» на хабре). Вот несколько из них.
Для выполнения команд — только модули command
или shell
. Последний, как пишет документация, только в крайних случаях, поэтому забудьте про перенаправление вывода и &&
. Результат всегда «changed», что плохо. Управляйте результатом либо параметром creates
(удобнее — в блоке args
, вместе с chdir
), либо register
и changed_when
. Полезно проверить условия перед выполнением: сначала рекогносцировка command + register + changed_when: False
, а затем с помощью when
проверяем сохранённый stdout на необходимость запуска команды.
Чем меньше вызовов модуля command
, тем лучше. Гуглите: почти всегда есть модуль. Например, я сначала сделал command: npm install -g {{ item }}
, а потом обнаружил, что можно npm: name={{ item }} global=yes
. Модуль всегда лучше, чем команда, потому что не нужно проверять конфигурацию и потому что результат работы будет не в строке stdout, а в удобной структуре.
Файлы конфигурации почти всегда правим через lineinfile
, который ищет строчку по регулярному выражению и заменяет на другую. Модуль blockinfile
добавляет целые блоки текста. С ним есть нюанс: если несколько тасков пишут в один файл, то нужно переопределять marker: # {mark} block name
. Иначе все будут затирать чужие блоки.
Перед изменением таблиц PostgreSQL удобно проверять их состояние с помощью pg_tables. Например:
command: psql -A -t -d {{ gisdb }} -c "SELECT tableowner FROM pg_tables WHERE schemaname = 'public' AND tablename = 'spatial_ref_sys'"
Наследование — наше всё: если можно вместо двух почти одинаковых тасков написать один с условными выражениями и with_items
, то делайте так. Группу повторяющихся тасков с похожими параметрами выносите в отдельный файл и вызывайте через include_role
с vars
. Тут ещё должно быть про параметризацию ролей, но я ещё только учусь и роль у меня одна.
В одной из статей нашёл совет не переизобретать велосипед, а искать подходящие роли в каталоге Ansible Galaxy. Действительно, php-fpm и postfix ставили тысячи людей до вас, и часто найдётся хорошо написанная роль с удобными значениями по умолчанию.
С другой стороны, какой смысл качать роль geerlingguy.apache
, когда apt: pkg=apache2
решает все мои задачи? Или, вот, нашёл роль для установки osm2pgsql из исходников, а она 2014 года и там используется устаревший sudo: yes
. То есть, я, конечно, записал roles_path = roles.galaxy:roles
в ansible.cfg
и сделал playbook для установки всех ролей, но ставить пока нечего. Вот как он выглядит:
- hosts: localhost
vars:
galaxy_path: roles.galaxy
tasks:
- name: Remove old galaxy roles
file: path={{ galaxy_path }} state=absent
- name: Install Ansible Galaxy roles
local_action: command ansible-galaxy install -r requirements.yml --roles-path {{ galaxy_path }}
И в requirements.yml
пишете строчки для каждой роли из Galaxy:
- src: автор.роль
Написали playbook и он отработал в Vagrant до конца? Отлично, теперь сделайте vagrant destroy
и создайте коробку заново. Стопроцентно обнаружите несколько косяков: забытые sudo, пропущенные mode: 0755 для исполняемых файлов, недостающие пакеты (помогают dnf provides
или apt-file
, который нужно устанавливать). Наконец, самое главное: после второго запуска vagrant provision
должно быть «changed: 0».
***
Переводить серверы под систему управления конфигурациями сложно, какую бы систему вы ни выбрали. Но после начального поля граблей программирование playbook-а спорится. Главное — не забывать о цели, чтобы не перегореть: вон, сейчас у меня целевая операционка Ubuntu 16.04, а через месяц я без особых сложностей переведу сервер на 18.04. А удовольствие от полнофункционального сервера с нуля по одной команде в консоли поможет в пути.
Автор: Zverik