Ansible не так прост +27


У меня есть три сервера, но я не профессиональный сисадмин. Это означает, что несмотря на четыре базы данных и стопяцот приложений, бэкапы нигде не ведутся, к любой проблеме на сервере я подхожу, шумно вздохнув и бросив тарелку в стену, а операционные системы там достигли 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. А удовольствие от полнофункционального сервера с нуля по одной команде в консоли поможет в пути.




К сожалению, не доступен сервер mySQL