Автоматический деплой приложений

В этой заметке пойдёт речь об автоматизации выкладки приложений на удалённые серверы и базовое управление процессами.

Проблематика

Обычно у админа есть несколько задач связанных с работой приложений на удалённых серверах:

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

Возьмём к примеру установку и администрирование простого почтового сервера на базе Postfix. Представьте объём работы, которую надо проделать для того чтоб поставить полноценный почтовый сервер. Необходимо настроить базовую функциональность, прикрутить антиспам, антивирус, сгенерировать сертификаты, ключи шифрования, фильтрацию обработки почты и обеспечить безопасность. По разным прикидкам у человека который делает это впервые может уйти до недели. А что если надо мигрировать на другой сервер? То есть задача повторить тоже самое, но в другом месте и возможно даже на другой операционной системе. Сможете вспомнить всё что надо сделать? Сомневаюсь. Всегда есть риск ошибиться, погрязнуть в зависимостях, конфигах, забыть настроить бекапы, безопасность, ещё что-то. Или вот другой случай, поделиться настройками с кем-то ещё. Вы заливаете все конфиги в гитхаб, кто-то их скачивает, но у него ничего не работает из-за отличий в системном окружении. А если работа идёт в команде и делиться надо постоянно? Ну, вы поняли.

Ранее традиционными способами решения таких проблем были скрипты. Однако, писать скрипты под каждую задачу это сложно и долго. На сегодняшний день подобные задачи принято решать через инструменты конфигурационного управления. Одним из таких инструментов является продукт компании Red Hat — Ansible. Главное его отличие от аналогов в том, что не нужна установка агента/клиента на целевые системы. Ну а использование декларативного языка разметки для описания конфигураций даёт низкий порог вхождения.

Установка

В macOS его можно установить через brew:

$ brew install ansible

Либо через python pip:

$ sudo easy_install pip
$ sudo -H pip install ansible

Ansible представляет из себя набор связанных модулей, работа с которыми описывается конфигурационными файлами в формате YAML. Продукт прекрасно документирован Ansible Documentation.

Демонстрационный проект

Давайте создадим типовой проект конечной целью которого будет публикация простого веб приложения на vps хостинг. Я заранее создал дроплет в Digital Ocean с предустановленной Ubuntu Server 18.04 в базовой конфигурации для демонстрации возможностей Ansible.

Предлагаю разбить проект на два этапа. На первом подготовим сервер к боевым задачам:

На втором произведём деплой приложения и настроим менеджмент вокруг него:

Структура

Подсматриваем в Best Practices и делаем соответствующую разметку:

group_vars/         # общие переменные
  all/
    vars.yml
    vault.yml
roles/
  common/           # менеджмент сервера
    tasks/
      main.yml
  blog/             # веб приложение 
    files/
    tasks/
      main.yml
inventory.ini       # список хостов
playbook.yml        # плейбук

Запишем в inventory.ini данные необходимые для подключения к серверу:

[webserver]
178.128.207.139 ansible_user=snupt ansible_port=22 ansible_python_interpreter="/usr/bin/env python3"

Группу хостов назовём webserver. В этом примере сервер один, но на практике их может быть сколько угодно. Пользователя snupt на сервере ещё не существует, но не волнуйтесь, мы решим эту задачу чуть позже. По умолчанию Ansible использует Python2, а в Ubuntu 18.04 установлен только Python3, поэтому нам нужно поменять это принудительно через переменную ansible_python_interpreter.

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

Создание пользователя

Давайте подготовимся к процессу создания учётной записи. Объявим переменные с содержанием имени пользователя и пароля. С именем всё просто, оно хранится в текстовом виде, а вот пароль, следуя документации нужно передавать в зашифрованном виде.

В macOS это делается следующим образом:

$ sudo -H pip install passlib
$ python -c "from passlib.hash import sha512_crypt; import getpass; print(sha512_crypt.using(rounds=5000).hash(getpass.getpass()))"

Общие переменные хранятся в каталоге group_vars/all/. Запишем в файл vars.yml переменную с именем пользователя:

username: snupt

А в файл vault.yml переменную с паролем:

user_password: $6$smd6jF7Z5Ze9sWgF$56SxnDUaQ79GMIcVWDRIERSTKEKE103WOcFQLNvsGijlFcxWRJz1vrVWRwOqsg6xGkYF94ePUXuQISRITgVzI1

Для администрирования пользователей Ansible предлагает использовать модуль user. Откройте документацию, посмотрите секцию возможных параметров и примеры использования. Помимо создания пользователя нам желательно бы сразу настроить авторизацию по ключу. Для этого также есть подходящий модуль authorized_key. Воспользуемся модулем lineinfile для редактирования файла sudoers и заставим sudo не просить пароль у всех кто в группе admin.

Создаём файл roles/common/tasks/useradd.yml со следующей информацией:

- name: create new user
  user:
    name: "{{ username }}"
    password: "{{ user_password }}"
    shell: /bin/bash
    groups: admin
    append: yes

- name: set authorized key taken from file
  authorized_key:
    user: snupt
    state: present
    key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

- name: allow admin group to have passwordless sudo
  lineinfile:
    path: /etc/sudoers
    regexp: '^%admin'
    line: '%admin ALL=(ALL) NOPASSWD: ALL'
    validate: '/usr/sbin/visudo -cf %s'

По умолчанию роль выполнит всё, что описано в файле ../tasks/main.yml, но наши таски находятся в ../tasks/useradd.yml. Можно было и не создавать отдельный файл, но сделав это мы можем импортировать его куда угодно при помощи модуля import_tasks. Таким образом, в ряде случаев, нам не придётся дублировать уже написанный код, а также иметь возможность работать с импортируемыми данными как с отдельным слоем абстракции. Импортируем useradd.yml в main.yml и попросим Ansible выполнить эти задачи от имени пользователя root, так как в inventory.ini указан пока ещё несуществующий пользователь:

- name:
  import_tasks: useradd.yml
  vars:
    ansible_user: root

Последующие задачи будем вносить по этой же схеме, поэтому давайте создадим файлы в roles/common/tasks/ с описанием задач, а затем импортируем их в main.yml как показано на примере выше.

Смена часового пояса

Для смены временной зоны на сервере можно использовать модуль модуль timezone. Создадим файл timezone.yml со следующим кодом:

- name: set timezone
  timezone:
    name: Europe/Moscow

Обновление пакетов

Установка и удаление пакетов в Ubuntu делается через модуль apt. Обновим кеш, поставим все обновления и почистим оставшийся мусор. Опишем всё это в файле upgrade.yml:

- name: upgrade all packages to the latest version
  apt:
    name: "*"
    state: latest
    update_cache: yes
    autoremove: yes
    autoclean: yes
    force_apt_get: yes

Установка Docker

Инструкция по установке Docker предлагает подключить официальный репозиторий. Для этого потребуется применить модули apt_key, apt_repository и уже знакомый нам apt. А также добавить нашего пользователя в группу docker. Все этим задачи можно объединить в одном файле docker.yml:

- name: add docker’s official gpg key
  apt_key:
    url: https://download.docker.com/linux/ubuntu/gpg

- name: add docker’s official reposytory
  apt_repository:
    repo: deb https://download.docker.com/linux/ubuntu bionic stable

- name: install docker
  apt:
    name: "{{ packages }}"
    update_cache: yes
  vars:
    packages:
      - docker-ce
      - docker-compose

- name: add user to docker group
  user:
    name: "{{ username }}"
    groups: docker
    append: yes

Сразу желательно учесть, что для взаимодействия Ansible и Docker нужны соответствующие модули, которые ставятся через менеджер пакетов Python. Если этого не сделать, то при запуске плейбука будут ошибки с предложением поставить их. Можно создать отдельный файл, но правильней будет держать всю логику связанную с установкой Docker в одном месте, поэтому просто допишем в docker.yml следующий код:

- name: install dependencies
  apt:
    name: python3-pip
    update_cache: yes

- name: install docker modules
  pip:
    name: "{{ item }}"
  with_items:
    - docker
    - docker-compose

Выполнение роли Common

Импортируем все вышеперечисленные таски в том порядке, в котором они будут выполняться. Итоговый файл roles/common/tasks/main.yml должен получиться таким:

- name:
  import_tasks: useradd.yml
  vars:
    ansible_user: root

- name:
  import_tasks: timezone.yml

- name:
  import_tasks: upgrade.yml

- name:
  import_tasks: docker.yml

Теперь проверим, всё ли корректно отработает на сервере. Отредактируем файл playbook.yml:

- hosts: webserver
  gather_facts: no
  become: yes

  roles:
    - common

Ключ become означает, что задачи в ролях будут выполнены с правами пользователя root с помощью sudo, а gather_facts выключает сбор данных с хоста, нам они не нужны. Остальное, думаю, вопросов не вызывает. Запускаем плейбук следующей командой:

$ ansible-playbook playbook.yml -i inventory.ini

Статус changed показывает, что состояние было изменено. Если запустить команду повторно, то статус всех команд сменится на ok, ведь все задачи уже были выполнены и изменений не произошло.

Первый этап проекта можно считать завершённым, приступаем к деплою. Для работы веб приложения как минимум требуется программа веб-сервер, которая будет принимать HTTP-запросы от клиентов. В случае статического сайта этого будет достаточно, но бывает, что нужна ещё база данных, интерпретаторы кода и другие зависимости. Всё это требует дополнительной настройки. Для обеспечения предсказуемой работы принято использовать Docker. С его помощью можно собрать изолированное приложение, проверить работоспособность, доставить на сервер гарантированно рабочую и протестированную сборку не меняя при этом его внешней конфигурации.

Docker управляет независимыми контейнерами по одному, но если надо обеспечить взаимодействие нескольких друг с другом, то используется технология docker-compose. Конфигурационные файлы compose также описываются на языке YAML.

Доставка приложения

Создадим папку roles/blog/files/app, а в ней файл docker-compose.yml со следующим содержимым:

version: '2'

services:
  app:
    image: nginx:latest
    container_name: nginx
    volumes:
      - ./www:/var/www
      - ./conf:/etc/nginx/conf.d
    ports:
      - "80:80"
      - "443:443"
    restart: always

Там же создадим папку www в которой будут находится файлы приложения и conf с настройками веб сервера. Из структуры docker-compose.yml должно быть видно, что эти папки подключаются к контейнеру nginx и всё что мы положим туда будет доступно изнутри. Количество контейнеров может варьироваться в зависимости от приложения и всеми можно управлять через docker-compose.

Работу приложения можно проверить выполнив локально команду:

$ docker-compose up

Если всё устраивает, то попробуем запультнуть всё это добро на сервер. Создаём файл roles/blog/tasks/push.yml с разметкой:

- name: synchronization using rsync protocol (push)
  synchronize:
    src: app
    dest: /home/{{ username }}/app

Тут важно понимать, что synchronize далеко не единственный способ доставки файлов на сервер. Есть другие модули, есть git и так далее.

Управление состоянием

После того как файлы будут доставлены можно зайти на сервер и запустить docker-compose вручную, но с помощью Ansible мы можем управлять состоянием и поэтому сделаем так, чтоб приложение запускалось сразу после доставки.

Создадим файл roles/blog/tasks/app_state.yml:

- name: start app
  docker_service:
    project_src: /home/{{ username }}/app
    services:
     - app
    state: present
  register: output

- debug:
    var: output

Ansible имеет множество модулей для управления различными параметрами Docker. Использование docker_service целесообразно когда у вас уже есть написанный docker-compose файл или же вам надо описать взаимодействие между контейнерами с помощью собственной разметки Ansible. Если требуется обойтись без docker-compose, то можно использовать модуль docker_container, который больше подходит для управления независимыми контейнерами. Параметр state управляет состоянием, поменяв его значение на absent можно инициировать выполнение команды docker-compose down и остановить приложение. Каждый модуль Ansible помимо входящих параметров возвращает какие-то значения. Через параметр register можно сохранить эти значения в переменной и распечатывать их в момент выполнения через модуль debug. Бывает полезно.

Резервное копирование данных

Как гласит народная мудрость админы делятся на два типа — тех, кто делает бекапы и тех, кто пока их не делает. Для организации резервного копирования воспользуемся популярным приложением Duplicity в связке с облачным хранилищем Backblaze.

Создадим файл roles/blog/tasks/backup.yml:

- name: install duplicity
  apt:
    name: "{{ packages }}"
    update_cache: yes
  vars:
    packages:
      - duplicity
      - python-pip

- name: install b2 module
  pip:
    name: b2
    executable: pip

По умолчанию Duplicity нужен модуль b2 для работы с Backblaze и я сразу учёл это. Бекапы будем делать по расписанию, поэтому создадим файл roles/blog/tasks/cron.yml с содержимым:

- name: daily backup
  cron:
    name: "daily backup"
    minute: "0"
    hour: "3"
    job: "PASSPHRASE={{ duplicity_password }} duplicity --allow-source-mismatch /home/{{ username }}/app b2://{{ b2_account_id }}:{{ b2_application_key }}@my-vps/app"

Секретные данные спрячем в переменных groups_vars/all/vault.yml:

duplicity_password: strongpassword
b2_account_id: 1234
b2_application_key: 1234567890

Выполнение роли Blog

Импортируем все задачи в главный файл роли roles/blog/tasks/main.yml:

- name:
  import_tasks: push.yml

- name:
  import_tasks: app_state.yml

- name:
  import_tasks: backup.yml

- name:
  import_tasks: cron.yml

И подключим саму роль в playbook.yml:

- hosts: webserver
  gather_facts: no
  become: yes

  roles:
    - common
    - blog

Можно запускать:

$ ansible-playbook playbook.yml -i inventory.ini

Если зайти на сервер по SSH, то можно увидеть, что всё отработало как надо.

Безопасность

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

Введём команду:

ansible-vault encrypt group_vars/all/vault.yml

Теперь файл vault.yml зашифрован, а его содержимое выглядит вот так:

$ANSIBLE_VAULT;1.1;AES256
66643934633963316635633761323962613563303139653039623630303232396430376665303439
6231333464616135326339383332633262343436373131640a663238663339336338386464656263
35363731353432383662306235393463336132313835623939633261396430376630623961303437
3339643334346366310a383766613435313931393638626463383566393431323036386231333466
63633638633033626662653835333130306132616535383938323237356463663433356266643131
33646261366464333137356532656232393564356165663565663237666663303438663263623036
62313434663735613538393463343563343634343833383066323663363262663236326439386131
37306466383566313863376535366465623937346162623735636636666536353065383330313933
32323162633361386639303061613032363233646265316564366239363037353433393961336265
66383530386232353634376435303431656137646531636464323637313738323738623832636435
32633635656133356430333433626636323438396135633366356334303432643463386666646435
65386132383739393331396139613764383833626630383135353965333462656436643539376161
38636137386239333837303061666237303465363338643463613566303565633432343630376436
33633137393337666133373934386130653463623835333433326635616237643266633733666564
306564633965363963643365396463636232

Тем не менее ничего не сломается, а в случае когда потребуется расшифровать сделать это можно командой:

$ ansible-vault decrypt group_vars/all/vault.yml

Однако, будьте осторожны, есть риск забыть зашифровать и опубликовать как есть. Поэтому для редактирования зашифрованного файла лучше использовать команду ansible-vault edit.

В данной статье рассматриваются очень простые задачи Ansible и Docker. На практике приходится делать вещи сложнее, но принципы остаются теми же. Я использую эти инструменты для выкладки и управления такими приложениями как этот блог, почтовый сервер в связке с веб интерфейсом, openvpn и ikev2 серверы, mtproto telegram proxy сервер, медиасервер plex и многие другие. Больше примеров можно увидеть в моих публичных репозиториях на гитхабе:

comments powered by Disqus