Работа с Ansible: развертка LEMP на VPS

30.05.2023

В предыдущей статье мы разобрались в том, что такое инфраструктура как код и познакомились с Ansible. Теперь поработаем с ним вплотную — развернём LEMP (Linux, Nginx, MySQL, PHP mfp) стек на Ubuntu 18.

Мы будем работать с Ansible из под Ubuntu 20, которая крутится на WSL2. О том, что такое WSL2 и как им пользоваться можно узнать из этой статьи.

Структура проекта

Любая работа над проектом начинается с постановки задачи и проработки структуры проекта. Задача: одновременная установка LEMP-стека на несколько хостов (удаленных серверов).

Структура проекта:

.
├── files
│ ├── info.php.j2
│ └── nginx.conf.j2
├── hosts.yml
├── playbook.yml
├── tree.txt
└── vars
└── default.yml

Помимо уже хорошо знакомого нам yaml-формата в проекте есть и пока незнакомый нам j2-формат. Это формат шаблонизатора Jinja2. Мы его применим для подстановки динамических значений в конфигурационный Nginx-файл.

Механика взаимодействия элементов проекта следующая: Ansible подключается к хостам по IP-адресам, указанным в hosts.yml, выполняет задачи, прописанные в playbook.yml и загружает файлы из папки files на хосты. Помимо этого файлы связаны внутренней логикой, которая будет раскрыта далее.

С задачами и структурой проекта определились, можно устанавливать Ansible.

Установка Ansible

Ansible не входит в стандартные репозитории Linux, поэтому мы добавим альтернативный источник командой: sudo apt-add-repository ppa:ansible/ansible.

Команда вернёт вывод примерно такого вида:

После нажатия на Enter — будет произведена инициализация нового ppa. Теперь можно устанавливать Ansible привычным образом: sudo apt install ansible. Проверим установку Ansible командой: ansible --version. Результат работы команды:

Отчёт состоит из перечня параметров Ansible, среди которых конфигурационные файлы и пути их расположения. Для нас важным здесь является конфигурационный файл ansible.cfg. Вкратце рассмотрим его свойства.

ansible.cfg — конфигурация Ansible

Конфигурационный файл ansible.conf управляет такими параметрами поведения ансибла как:

Ansible.cfg расположен по адресу: /etc/ancible/ansible.conf и имеет декларативный ini-формат. Это удобно, так как количество возможных ошибок при его заполнении сильно сокращается.

По умолчанию ansible.cfg содержит только закомментированный текст. Мы же укажем в нём буквально один параметр, который облегчит нам работу с Ansible:

[defaults]
host_key_checking = False

Строка [defaults] — указывает на название секции, к которой относятся все последующие настройки до указания новой секции. Не дефолтный ansible.cfg обязательно должен иметь секцию defaults.

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

Больше ничего в ansible.cfg мы менять не будем. Нам достаточно и этих параметров. С первым компонентом Ansible — разобрались. Можно переходить к разбору следующего компонента — инвентаризационного файла hosts.

hosts — конфигурация подключения к серверам

Файл hosts содержит данные для подключения к удаленным серверам. По умолчанию дефолтный hosts-файл располагается по следующему пути: /etc/ansible/hosts — там же, где и ansible.cfg. Кстати, файл содержит отличную документацию по тому как его заполнять.

Менять оригинальный hosts-файл мы не будем, мы создадим новый файл в соответствии со структурой проекта и расположим его корне нашего проекта.

.
├── files
│ ├── info.php.j2
│ └── nginx.conf.j2
├── hosts.yml
├── playbook.yml
├── tree.txt
└── vars
└── default.yml

По умолчанию hosts-файл не имеет расширения, однако Ansible интерпретирует его как .ini-файл. Поэтому записи hosts и hosts.ini будут равнозначными. Но мы будем писать hosts-файл в формате yaml. Это нам позволит автоматизировать процесс раскатки LEMP-стека на несколько машин.

Вот как выглядит наш hosts.yml внутри:

all:
    hosts:
        server-1:
            ansible_host: 188.227.72.2
            ansible_connection: ssh
            ansible_user: root
            ansible_password: g5YLBOCx
        server-2:
            ansible_host: 188.227.75.26
            ansible_connection: ssh
            ansible_user: root
            ansible_password: c6Nbp0dk

Кратко разберём, что за что отвечает:

Теперь протестируем наш hosts-файл — запустим модуль ping на всех хостах, указанных в файле hosts.yml: ansible all -i hosts.yml -m ping.

Вот, что вернула нам команда:

В отчёте мы видим:

Всё работает корректно. Двигаемся дальше, на очереди центральный элемент Ansible — Playbook.

Playbook — сценарий работы Ansible

Playbook — это сценарий работы Ansible на хостах, написанный в формате YAML. Мы подробно разобрали работу с этим форматом в этой статье. Для корректной работы Playbook достаточно задать одну переменную — hosts: all:

---
- hosts: all #Название группы хостов в hosts-файле.
...

Это значит, что из hosts-файла, который будет передан через ключ i в Ansible будут взяты все хосты. Базовый вид команды для запуска плейбуков выглядит так: ansible-playbook playbook.yml -i hosts.ini. Команда вернет следующий результат:

По умолчанию Ansible всегда делает проверку фактов о хостах. Поэтому, запустив пустой Playbook мы увидим отчет о выполнении задач по сбору фактов на хостах: server-1 и server-2 со статусом «ok».

Помимо задач категории «ok» (выполненных) есть и другие задачи, такие как:

Обычно команда вида ansible-playbook playbook.yml -i hosts.ini, используется для проверки соединения с хостами. Наш тест работает исправно, можно добавлять блоки задач. Первый блок: преднастройка пакетного менеджера и установка пакетов LEMP-стека:

---
- hosts: all
  become: true
  vars_files:
    - vars/default.yml

  tasks:
    - name: Преднастройка пакетного менеджера aptitude
      apt: name={{ item }} update_cache=yes state=latest 
force_apt_get=yes
      loop: [ 'aptitude' ]

    - name: Установка LEMP-стека
      apt: name={{ item }} update_cache=yes state=latest
      loop: [ 'nginx', 'mysql-server', 'python3-pymysql', 'php-fpm', 'php-mysql' ]
...

Разберем вкратце, что здесь записано:

Обратите внимание, задачи имеют форму списка и вложены в словарь tasks. Правила создания задачи:

  1. задача должна обязательно начинаться с имени — name. Имя должно отражать логику выполнения задачи;
  2. задачи выполняется на удаленных хостах модулями, поэтому должен быть указан модуль.
  3. у модуля в зависимости от его параметров должны быть указаны обязательные аргументы.

Модулей для Ansible огромное количество. Узнать какой модуль за что отвечает и какими параметрами обладает можно из официальной документации Ansible.

Взглянем на финальный playbook целиком↓
---
- hosts: all
  become: true
  vars_files:
    - vars/default.yml

  tasks:
    - name: Преднастройка пакетного менеджера aptitude
      apt: name={{ item }} update_cache=yes state=latest
force_apt_get=yes
      loop: [ 'aptitude' ]

    - name: Установка LEMP-стека
      apt: name={{ item }} update_cache=yes state=latest
      loop: [ 'nginx', 'mysql-server', 'python3-pymysql', 'php-fpm', 'php-mysql' ]

     # Настройка Nginx
    - name: Скопировать и переименовать Nginx конфиг
      template:
        src: "files/nginx.conf.j2"
        dest: "/etc/nginx/sites-available/{{ http_conf }}"

    - name: Создать симлинк
      file:
        src: "/etc/nginx/sites-available/{{ http_conf }}"
        dest: "/etc/nginx/sites-enabled/{{ http_conf }}"
        state: link
      notify: Reload Nginx

         - name: Удалить дефолтный сайт
      file:
        path: "/etc/nginx/sites-enabled/default"
        state: absent
      notify: Reload Nginx

# Настройка MySQL
    - name: Установка пользователя для подключения
      mysql_user:
        name: root
        password: "{{ mysql_root_password }}"
        login_unix_socket: /var/run/mysqld/mysqld.sock

    - name: Удаление всех анонимных аккаунтов
      mysql_user:
        name: ''
        host_all: yes
        state: absent
        login_user: root
        login_password: "{{ mysql_root_password }}"

    - name: Удаление тестовой базы данных
      mysql_db:
        name: test
        state: absent
        login_user: root
        login_password: "{{ mysql_root_password }}"

# Настройка UFW
    - name: "UFW - открытие {{ http_port }} HTTP порта"
      ufw:
        rule: allow
        port: "{{ http_port }}"
        proto: tcp

# Настройка PHP info страницы
    - name: Настройка PHP info страницы
      template:
        src: "files/info.php.j2"
        dest: "/var/www/html/info.php"

  handlers:
    - name: Перезагрузка Nginx
      service:
        name: nginx
        state: reloaded

    - name: Рестарт Nginx
      service:
        name: nginx
        state: restarted
 

В плейбуке помимо повторяющихся из модуля в модуль деклараций встречаются ещё конструкции заключенные в двойные фигурные скобки: {{ имя }}. В зависимости от условий это может быть переменная, значение которой подтягивается из файла, указанного в vars_files или переменная, значение которой подставляется из конструкции loop.

Например, вот задача по установки всех элементов LEMP-стека:

    - name: Установка LEMP-стека
      apt: name={{ item }} update_cache=yes state=latest
      loop: [ 'nginx', 'mysql-server', 'python3-pymysql', 'php-fpm', 'php-mysql' ]

В этой записи вместо name={{ item }} будет подставлено каждое значение из списка loop. Так по очереди будут установлены все компоненты LEMP-стека.

Вот другой пример:

    - name: Скопировать и переименовать Nginx конфиг
      template:
        src: "files/nginx.conf.j2"
        dest: "/etc/nginx/sites-available/{{ http_conf }}"

Здесь есть переменная {{ http_conf }}, её значение будет взято из файла, указанного в директиве vars_files. По основным механикам работы внутри Playbook — всё, осталось привести общую схему взаимодействия всех элементов проекта между собой.

Схема взаимодействия элементов проекта между собой:

Теперь, когда у нас все компоненты проекта взаимосвязаны и работают — можно запускать Ansible: ansible-playbook playbook.yml -i hosts.ini. После успешного выполнения всех задач, перейдём в браузер и обратимся к тестовой странице PHP: http://185.158.152.31/info.php. Если всё работает исправно, будет показана страница php-info:

Итак, у нас получился полноценный рабочий масштабируем проект по установки LEMP-стека на несколько хостов. Настало время собрать всё воедино.

Объединяем знания

Ansible — это мощный и расширяемый Open source инструмент, который позволяет управлять конфигурациями удаленных серверов и сохранять их состояние. Ansible использует безагентную push-модель, это значит, что для работы на хостах ему нужен только Python на Linux и открытый SSH-порт.

Принцип работы Ansible таков: DevOps-инженер или администратор указывает задачи (таски, от английского tasks) для Ansible в специальном файле сценариев — playbook, а сведения об хостах, на которых эти задачи должны быть исполнены указываются в инвентаризационном файле hosts.

Таски в плейбуке записываются, следуя логике установки и внесения изменений в систему. Так, чтобы установить LEMP-стек на хост, сначала нужно обновить менеджер пакетов, потом установить Nginx, php-fpm, mySQL/PostgreSQL, затем настроить доступы к БД и перезагрузить Nginx.

Пожалуй, главными свойствами Ansible являются идемпотентность, расширяемость и следование философии единого источника. Конечно, Ansible не единственная система работы с инфраструктурой как кодом с подобным подходом. Ближайший конкурент ансиблу — это Terraform.

В следующих публикациях: подружим 1cloud API и Ansible, автоматизируем процесс создания виртуальных серверов в обход Панели, познакомимся с Terraform, попробуем его на боевую и сравним с Ansible. Для большего погружения в будущий материал, рекомендуем ознакомиться с этими публикациями:

Зарегистрироваться