Пишем первый плейбук Ansible

Статья подготовлена на основе уроков из открытой темы «Установка LEMP стека с помощью Ansible» курса по Ansible от Слёрм. Автор — Всеволод Севостьянов, Lead Engineer в Vene.io (Affiliate marketing solution). Первые две темы курса доступны на Youtube.

Материал этого урока будет интересен тем, кто разобрался с установкой Ansible и готов написать свой первый плейбук. Результатом будет плейбук, устанавливающий nginx на удалённой машине.

Hosts

Начнём наше знакомство с Ansible с мануала по командам.

Плох тот админ, который не читает маны — верно?

На что смотрим. Первое — это обязательный аргумент pattern, который указывает, какие хосты мы берем, на каких машинах мы будем выполнять наши команды. Мы будем выполнять на всех — all. Второе, персональная команда — вывести хосты, то есть, команда, которая выведет нашу машину.

ansible all --list-hosts

All означает — все хосты, которые у нас возможны. Увидим ответ, что список пустой. Почему так происходит? Потому что Ansible по умолчанию берет данные из etc/ansible/hosts. Посмотрим, что это за файл.

cat etc/ansible/hosts

Здесь всё задокументированоЗдесь всё задокументировано

Видим, что мы можем указывать хосты просто через enter и указывать их IP-адреса, либо их доменные имена. Можем их группировать.

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

Заходим в папочку ansible, в нашем проекте. И создаём файлик, я назову его hosts.ini. Ini — это как раз тот самый формат, когда вы всё перечисляете просто через enter и пишите строку, стандартный формат конфигурации.

Укажем в первой строке IP-адрес нашего второго хоста, то есть, нашего сервера непосредственно. У меня 192.168.50.5 и теперь запускаем команду Ansible ещё раз.

ansible all --list-hosts

Естественно, он никак не отреагирует, он по-прежнему берет данные из файлика etc/ansible/hosts.

Для того чтобы он брал данные из нашего файлика — вызовем команду h и увидим в списке команду –i. Ansible называет файлики hosts — эти файлики, где у нас все машины перечислены, inventory. То есть, как по аналогии с каким-то кладовщиком — у него есть инвентарь и в нём указаны все машины, которые нам принадлежат. Мне кажется, это вполне логичным, легко запоминается и указывается через –i.

Мы находимся в корневой папке пользователя и для того, чтобы нам перейти в папку выше — мы указываем ./ansible/hosts.ini.

ansible all --list-hosts -i ./ansible/hosts.ini

И давайте разберемся немножко с тем, как запускать команды из Ansible. Попробуем подключиться к удаленному хосту и запустить что-нибудь. В Ansible все команды являются модулями и указываются с помощью ключевого слова m. Запустим стандартный модуль, называемый ping, который ничего не сделает на машине, кроме того, как даст ей команду ping. Это команда, которая физически выполняется Ansible на удаленной машине. Он в неё заходит, загружает модуль Python, вызывает команду ping. И выдаёт ответ.

cd /ansible
ansible all -i hosts.ini -m ping

Нас просят подтвердить, поскольку подключается по SSH, нужно подтверждение сертификата. И мы увидим первую ошибку, что доступ закрыт. Стандартная ошибка, это значит, что нам нужно ввести пароль. На этот случай у Ansible существует команда --ask-pass.

ansible all -i hosts.ini -m ping --ask-pass

Которая спрашивает с каким паролем мы заходим. Следующая ошибка, с которой мы сталкиваемся: нельзя использовать пароли без установки команды sshpass. Проблема решается просто.

sudo apt install sshpass

Итак, команда sshpass установлена, и мы, соответственно, вызываем команду ping и задаём ему пароль.

ansible all -i hosts.ini -m ping --ask-pass

При этом, как мы увидим, команда выполнилась, вывела нам ответ в формате json. Написала, что на удаленной машине у нас Python3, который мы используем для выполнения этого модуля, ничего не изменилось на ней. Команда, не меняющая ничего, и ответ на ping — pong.

Возможно вы заметили, что в примере я не указывал пользователя Ansible, потому что нахожусь под пользователем vagrant и Ansible по умолчанию использует того же пользователя для подсоединения по ssh.

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

Одним из атрибутов нашего файлика hosts является то, что мы можем присвоить каждой из машин, находящихся в нём какие-то специфичные для неё параметры. И одним из набора специфичных параметров для этой машины являются переменные ansible_user и ansible_password, которые, соответственно, задают пользователя и пароль.

192.168.50.5 ansible_user=vagrant ansible_password=vagrant

Теперь, если мы запустим Ansible, если мы уберем --ask-pass, то есть, не будем запрашивать пароль, но оставим всё остальное, то, соответственно, все равно нам вернётся success, на этот раз никто у нас никакой пароль не спрашивал.

ansible all -i hosts.ini -m ping

Таким образом, если у нас будет ещё какая-то другая машина в системе, на которой другой пользователь, мы можем указать всё в файлике hosts и залогиниться на другую машину. Выполнить там точно также команды. Таким образом, мы разделяем сами команды и необходимые параметры, данные для этих машин. У нас всё готово к тому, чтобы писать первый плейбук.

NGINX

Итак, что такое плейбук? Это термин, взятый из футбола. Обозначает книгу тренера, который там что-то записывает и задаёт сценарий игре. Плейбук– это оно и есть, список сценариев для запуска Ansible. То, что мы делали командой Ansible –m, просто запущенная с разными командами, разными аргументами несколько раз. Плейбуки позволяют нам также определить, на каких хостах мы им запускаем и какие задачи мы им задаём. И мы посмотрим, как это работает.

В папке ansible создадим файл playbook.yml. Плейбуки пишутся в формате yaml. Мы будем знакомиться с этим форматом в процессе написания плейбуков. На данный момент, что нас интересует? Плейбуки начинаются с трёх дэшей или с трёх чёрточек и закачиваются тремя точками.

---
...

Этот формат необязательный. Я, например, не использую три точки, но начинаю с чёрточек, потому что это достаточно удобно. Плейбуки позволяют нам делать комментарии, поэтому я здесь сразу и напишу.

# LEMP PLAYBOOK
---

Соответственно, в плейбуке нужно обязательно ставить какие-то play. Play задаются с помощью массива, то есть в yaml следующая запись обозначает массив.

# LEMP PLAYBOOK
---
- test
- test1
- test2

И здесь мы можем написать, например, разные переменные. Наш плейбук в себе будет содержать play, и каждый play в себе обязан содержать hosts — это также как мы писали в консоли Ansible — all. Здесь то же самое. Это базовая вещь, базовый play, и мы можем запустить плейбук.

# LEMP PLAYBOOK
---
- hosts: "all"

Если мы хотим создать второй play — мы создаём второй элемент массива, ему определяем какой-то второй параметр и для yaml это значит, что это массив с двумя переменными. Сейчас нам это не нужно.

Переходим в консоль Ansible. Командой ls увидим, что у нас есть файлы hosts.ini и playbook.yml. И для того, чтоб запустись плейбук, у Ansible существует консольная команда: ansible-playbook. Она очень похожа на команду Ansible, за исключением того, что вместо передачи аргументов хостов, мы передаём аргумент плейбук, то есть путь к файлику playbook.yml. Укажем hosts.ini, как файлик инвентаря, то есть, это две базовые вещи, которые нужно ansible-playbook для того, чтобы работать. Какие хосты и что мы делаем.

ansible-playbook playbook.yml -i hosts.ini

Теперь он запускает PLAY [all] — это play, который нам говорит для всех хостов. И первый TASK [Gathering Facts], который мы даже не указывали — это собирание фактов, первый модуль, который загрузится — это модуль Ansible, который загружается на машину и собирает все факты с этой машины. На данном моменте нам достаточно знать, что каждый play в плейбуке собирает факты о машине — это может быть операционная система, версия Python, которая там стоит, IP-адреса, порты и так далее.

После сбора фактов Ansible возвращает нам команду ok и возвращает статистику. Какие задачи были ok, какие что-то изменили в системе, какие не смогли достичь удалённой системы, задачи провалившиеся при настройке, пропущенные, восстановленные, и какие задачи были проигнорированы системой.

Каждый хост, каждый play в плейбуке характеризуется целью, на какие хосты мы его применяем и набором задач, так называемыми task-ами. Task-и — это тоже массив, потому что task на play может быть много, соответственно, как play в плейбуке, здесь всё логично. И каждый task, как минимум, содержит в себе имя. Имя, также как и hosts, — это строка. Строки в yaml задаются любо через кавычки, либо без них. Я рекомендую кавычки, потому что в таком случае yaml не будет теряться при попытке скастовать что-то, то есть, если у вас там строка — это строка, если у вас число — это число и к тому же, это очень хорошо видно в IDE. И первая task-а для нас — это будет установка самого nginx на удалённой машине. Давайте, так её и назовём. Install nginx via apt. И поехали делать task.

# LEMP PLAYBOOK
---
- hosts: "all"
  tasks:
  - name: "Install nginx via apt"

После указания имени task, нам нужно указать модуль, который будет его выполнять. Как вот мы указывали -m ping, здесь надо сделать тоже самое. Поскольку nginx у нас установится через систему APT, мы можем посмотреть, как же она называется в Ansible.

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

Существует модуль ansible.builtin.apt, который как раз позволяет устанавливать нам софт через apt-packages. И здесь все его параметры и достаточно интересная документация с примерами. Я смотрю на примеры, как человек со стажем, как правило, они более актуальны, чем документация. В данном случае они совпадают. И смотрим, что для apt нужно указать имя и state.

Смотрим. Для начала: разница как раз между Ansible 2.9, 2.10 и 2.11 в том, что Ansible будет форсить, будет заставлять людей использовать полные имена модулей. Потому что, если кто-то еще напишет модуль apt, и он у вас будет установлен в системе, Ansible может потеряться. Он может не понять, какой apt ему запускать. Поэтому надо всегда указывать полный путь. И так мы и будем поступать. Соответственно, здесь указывается полный путь к модулю. И дальше указываются параметры модуля.

# LEMP PLAYBOOK
---
- hosts: "all"
  tasks:
  - name: "Install nginx via apt"
    ansible.builtin.apt:

Важно понимать, что это все словари. Имя — это ключ словаря и строка «Install nginx via apt» — это значение. Ansible.builtin.apt — это тоже ключ, а значение у него это список аргументов и они в yaml идут через 2 пробела. Это важно, потому что если у вас не будет нужного количества пробелов, yaml будет ругаться на то, что он не может ничего сконфигурировать. И поэтому я и рекомендую использовать какую-нибудь IDE, а не писать просто это все в блокноте и следить там руками за пробелами. Это неудобно.

Поэтому, указываем первый параметр — это имя пакета. Имя пакета у нас просто nginx.

И второй параметр — это state. Соответственно, что такое state? State, как написано в документации — это в каком состоянии мы хотим найти пакет. Ansible, он сам по себе декларативный, то есть, вы не указываете в плейбуке, что ему нужно сделать, вы указываете, что вы хотите видеть. И state — это как раз очень хороший пример. То есть вы не говорите, что я хочу установить такой-то nginx, вы говорите: я хочу видеть пакет nginx в apt в таком-то state-е. По умолчанию state — present. Это значит, что если у вас уже есть какой-то пакет в Ansible, то state у него, соответственно, будет неизменным. И есть state — latest. В таком случае, если apt найдет какую-то более свежую версию пакета, то он попробует ее обновить.

Последняя команда, которую я хотел бы задать здесь, это update_cache. Это тоже самое, что вы запускаете каждый раз перед запуском любой команды apt, соответственно sudo apt. Update, которая просто обновит cache всех библиотек, которые у вас лежат в архиве.

# LEMP PLAYBOOK
---
- hosts: "all"
  tasks:
  - name: "Install nginx via apt"
    ansible.builtin.apt:
      name: "nginx"
      state: "latest"
      update_cache: true

Запускаем Плейбук в консоли Ansible второй раз.

ansible-playbook playbook.yml -i hosts.ini

Первая команда Gathering Facts. Вторая команда Install nginx via apt. И она провалилась. То есть написано «Failed to lock apt for exclusive operation». Это не очень подробное описание, но оно нам позволяет понять, что не получилось у apt получить какой-то lock.

Почему? Потому что существует в Ansible политика выполнения команд от того пользователя, которым вы залогинились. То есть в данном случае пользователь vagrant пытается установить что-то через apt. И не может, потому что apt пишет в системные файлы, которым нужен root.

Для того, чтобы Ansible стал root, мы используем команду в play become true, которая будет выполнятся для всех task в этом play. Можем ее использовать внутри task, можем ее использовать внутри play. В данном случае я хочу, чтобы все, что мы делали, делалось через sudo под root. И поэтому ставлю ее в непосредственно play.

# LEMP PLAYBOOK
---
- hosts: "all"
  become: true
  tasks:
  - name: "Install nginx via apt"
    ansible.builtin.apt:
      name: "nginx"
      state: "latest"
      update_cache: true

Теперь команда будет запущенна от root. Можно видеть, что true в данном случае не строка, а булево. То есть истина или ложь. Точно также, как и update_cache. Это еще один тип переменных в формате yaml.

Запускаем Плейбук в консоли Ansible третий раз.

ansible-playbook playbook.yml -i hosts.ini

И мы видим, что nginx успешно установился. Также видим новый статус — changed. Это значит, что nginx что-то изменил в системе — установил пакет.

Здесь самое время сказать, что Ansible является идемпотентным, то есть, он задуман, как идемпотентная система. Что это значит? При запуске одного и того же плейбука 2 раза на одной и той же системе, у вас в этой системе ничего не изменится. Второй раз apt даже пытаться не будет, он проверит список пакетов, он увидит nginx и не будет ничего дальше делать. То есть статус ok — это значит, что система соответствует тому статусу, который вы от неё ожидаете.

Теперь чтобы проверить, что nginx действительно настроен, возьмём любой шаблон лендига и запустим.

Создаём task. Удаляем папку по умолчанию var/www/html. Для того, чтобы скопировать нашу папку с нашим лендингом для заглушки на веб-сайт. Воспользуемся другой командой Ansible — ansible.builtin.file. Указываем путь — path. И дальше помним про идемпотентность — указываю state — absent. Просто указываю state. То есть, как там всё будет происходить, как эти файлы будут удалятся. Ansible написан в таком духе, что мы описываем состояние системы.

# LEMP PLAYBOOK
---
- hosts: "all"
  become: true
  tasks:
  - name: "Install nginx via apt"
    ansible.builtin.apt:
      name: "nginx"
      state: "latest"
      update_cache: true

  - name: "Delete /var/www/html folder"
    ansible.builtin.file:
      path: "/var/www/html"
      state: "absent"

Ну, и последнее, что мы делаем, копируем наш лендинг в папку var/www/html. У меня сейчас лендинг в папке files в директории Ansible. Пишем последнее имя task «Copy our lending to /var/www/html folder». Будем пользоваться командой ansible.builtin.copy, которая будет копировать файлы с нашей локальной машины на удаленную машину.

Указываем путь. Пути поддерживаются абсолютные и относительные. На нашей машине, скорее всего, мы не знаем, откуда мы будем запускать Ansible, поэтому пишем относительные пути — scr: «files/html» И в dest мы указываем »/var/www/». В чем смысл? Когда мы копируем через vagrant upload. Vagrant upload нам копирует содержимое папки внутрь. В данном случае Ansible будет копировать всю папку. И, поэтому, если мы хотим скопировать что-то в var/www/html, нам нужно назвать папку соответствующим образом и скопировать ее корневую директорию.

Далее мы указываем owner и group. Это просто пользователь и группа. И указываем mode — режим. Если не указать пользователя, группу и режим, то Ansible будет пытаться скопировать это все под пользователем, под которым он запущен. И с become true это, скорее всего, будет root.

# LEMP PLAYBOOK
---
- hosts: "all"
  become: true
  tasks:
  - name: "Install nginx via apt"
    ansible.builtin.apt:
      name: "nginx"
      state: "latest"
      update_cache: true

  - name: "Delete /var/www/html folder"
    ansible.builtin.file:
      path: "/var/www/html"
      state: "absent"

  - name: "Copy our lending to /var/www/html folder"
    ansible.builtin.copy:
      scr: "files/html"
      dest: "/var/www/"
      owner: "vagrant"
      group: "vagrant"
      mode: "0644"

Запускаем наш Playbook ещё раз. Он идемпотентный, то есть второй раз nginx у нас не установится. Наш объект будет удален и копируется наш новый лендинг в var/www/html.

Осталось проверить, как это работает. Открываем IP-адрес нашего сервиса. Всё, поздравляю, nginx настроен. Буквально 3 команды в плейбуке.

© Habrahabr.ru