Практикум RHEL 8 Beta: Собираем работающие веб-приложения

RHEL 8 Beta предлагает разработчикам много новых возможностей, перечисление которых может занять страницы, однако, изучать новое всегда лучше на практике, поэтому ниже предлагаем пройти практикум реального создания инфраструктуры приложений на базе Red Hat Enterprise Linux 8 Beta.

Практикум RHEL 8 Beta: Собираем работающие веб-приложения

За основу возьмем Python, популярный среди разработчиков язык программирования, комбинацию Django и PostgreSQL, довольно распространенную связку для создания приложений, и сконфигурируем RHEL 8 Beta для работы с ними. Потом добавим еще пару (несекретных) ингредиентов.

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

Начнем с развертывания образа виртуальной машины RHEL 8 Beta VM. Можно установить виртуальную машину с нуля, или использовать гостевой образ KVM, доступный вместе с подпиской на Beta. При использовании гостевого образа потребуется настроить виртуальный CD, который будет содержать метаданные и пользовательские данные для облачной инициализации (cloud-init). Ничего особенного со структурой диска или доступными пакетами делать не нужно, подойдет любая конфигурация.

Давайте рассмотрим весь процесс подробнее.

Установка Django

С новейшей версией Django потребуется виртуальное окружение (virtualenv) с Python 3.5 или более поздней версией. В примечаниях к Beta можно увидеть, что доступен Python 3.6, давайте проверим, действительно ли это так:

[cloud-user@8beta1 ~]$ python
-bash: python: command not found
[cloud-user@8beta1 ~]$ python3
-bash: python3: command not found

Red Hat активно использует Python в качестве системного инструментария в RHEL, так почему же получается такой результат?

Дело в том, что многие разработчики, использующие Python, все еще размышляют о переходе с Python 2 на Python 2, при этом сам Python 3 находится в стадии активной разработки, и постоянно появляются все новые и новые версии. Поэтому, чтобы удовлетворить потребность в стабильных системных инструментах, и одновременно предложить пользователям доступ к различным новым версиям Python, системный Python перенесли в новый пакет и обеспечили возможность установки как Python 2.7, так и 3.6. Более подробную информацию об изменениях и о том, почему это было сделано, можно почерпнуть из публикации в блоге Лэнгдона Уайта (Langdon White).

Итак, чтобы получить работающий Python, необходимо установить всего два пакета, при этом python3-pip подтянется в виде зависимости.

sudo yum install python36 python3-virtualenv

Почему не стоит использовать непосредственные обращения к модулю, как предлагает Лэнгдон, и не установить pip3? Помня о предстоящей автоматизации, известно, что для работы Ansible потребуется установленный pip, поскольку модуль pip не поддерживает виртуальные окружения (virtualenvs) с кастомным исполняемым файлом pip.

Имея в своем распоряжении работающий интерпретатор python3, можно продолжить процесс установки Django и получить работающую систему вместе с другими нашими компонентами. В сети представлено множество вариантов реализации. Здесь представлена одна версия, но пользователи могут использовать свои собственные процессы.

Версии PostgreSQL и Nginx, доступные в RHEL 8 по умолчанию будем устанавливать с помощью Yum.

sudo yum install nginx postgresql-server

Для PostgreSQL потребуется psycopg2, но нужно, чтобы оно было доступно только в окружении virtualenv, поэтому его будем устанавливать его с помощью pip3 вместе с Django и Gunicorn. Но сначала нам нужно настроить virtualenv.

На тему правильного выбора места установки проектов Django всегда ведется много споров, но, когда возникают какие-то сомнения, всегда можно обратиться к стандарту Linux Filesystem Hierarchy Standard. В частности, в FHS сказано, что /srv используется для: «хранения данных, специфических для конкретного узла, – данных, которые выдает система, например, данных и скриптов веб-серверов, данных, хранящихся на FTP серверах, а также репозиториев систем контроля версий (появившихся в FHS-2.3 в 2004 году)».

Это как раз наш случай, поэтому складываем все необходимое в /srv, владельцем которой является наш пользователь приложения (cloud-user).

sudo mkdir /srv/djangoapp
sudo chown cloud-user:cloud-user /srv/djangoapp
cd /srv/djangoapp
virtualenv django
source django/bin/activate
pip3 install django gunicorn psycopg2
./django-admin startproject djangoapp /srv/djangoapp

Настройка PostgreSQL и Django не вызывает сложностей: создаем базу данных, создаем пользователя, настраиваем разрешения. Существует один момент, о котором следует помнить при исходной установке PostgreSQL – это скрипт postgresql-setup, который устанавливается вместе с пакетом postgresql-server. Этот скрипт помогает выполнять базовые задачи, связанные с администрированием кластера баз данных, например, инициализацию кластера или процесс обновления. Для настройки нового экземпляра PostgreSQL на системе RHEL нам нужно выполнить команду:

sudo /usr/bin/postgresql-setup -initdb

После этого можно запустить PostgreSQL с помощью systemd, создать базу данных и настроить проект в Django. Не забудьте перезапустить PostgreSQL после внесения изменений в файл конфигурации аутентификации клиента (обычно это pg_hba.conf) для настройки хранения пароля для пользователя приложения. Если вы столкнулись с другими сложностями, убедитесь, что изменены настройки IPv4 и IPv6 в файле pg_hba.conf.

systemctl enable -now postgresql

sudo -u postgres psql
postgres=# create database djangoapp;
postgres=# create user djangouser with password 'qwer4321';
postgres=# alter role djangouser set client_encoding to 'utf8';
postgres=# alter role djangouser set default_transaction_isolation to 'read committed';
postgres=# alter role djangouser set timezone to 'utc';
postgres=# grant all on DATABASE djangoapp to djangouser;
postgres=# q

В файле /var/lib/pgsql/data/pg_hba.conf:

# IPv4 local connections:
host    all        all 0.0.0.0/0                md5
# IPv6 local connections:
host    all        all ::1/128                 md5

В файле /srv/djangoapp/settings.py:

# Database
DATABASES = {
   'default': {
       'ENGINE': 'django.db.backends.postgresql_psycopg2',
       'NAME': '{{ db_name }}',
       'USER': '{{ db_user }}',
       'PASSWORD': '{{ db_password }}',
       'HOST': '{{ db_host }}',
   }
}

После конфигурирования файла settings.py в проекте и настройки конфигурации базы данных, можно запустить сервер разработки, чтобы убедиться, что все работает. После запуска сервера разработки неплохо создать пользователя admin с целью протестировать соединение с базой данных.

./manage.py runserver 0.0.0.0:8000
./manage.py createsuperuser

WSGI? Вай?

Сервер разработки полезен при тестировании, но для запуска приложения необходимо настроить соответствующие сервер и прокси для Web Server Gateway Interface (WSGI). Существует несколько распространенных связок, например, Apache HTTPD с uWSGI или Nginx с Gunicorn.

Задача Web Server Gateway Interface – перенаправлять запросы от веб-сервера к веб-фреймворку Python. WSGI – это эдакое наследие ужасного прошлого, когда в ходу были механизмы CGI, и сегодня WSGI является фактически стандартом, независимо от используемого веб-сервера или Python-фреймворка. Но несмотря на его широкое распространение, все же существует множество нюансов при работе с этими фреймворками, и множество возможностей выбора. В данном случае постараемся наладить взаимодействие Gunicorn и Nginx через сокет.

Поскольку оба эти компонента установлены на одном и том же сервере, попробуем использовать вместо сетевого сокета сокет UNIX. Так как для коммуникаций в любом случае нужен сокет, попробуем сделать еще один шаг и настроить активацию сокета для Gunicorn через systemd.

Процесс создания сервисов, активируемых сокетами (socket activated services), достаточно прост. Сначала создается unit-файл, который содержит директиву ListenStream, указывающую на точку, в которой будет создан сокет UNIX, затем — unit-файл для сервиса, в котором директива Requires будет указывать на unit-файл сокета. Затем в unit-файле сервиса останется только вызвать Gunicorn из виртуального окружения и создать WSGI-привязку для сокета UNIX и приложения Django.

Вот несколько примеров unit файлов, которые можно взять за основу. Сначала настраиваем сокет.

[Unit]
Description=Gunicorn WSGI socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

Теперь необходимо настроить демон Gunicorn.

[Unit]
Description=Gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
User=cloud-user
Group=cloud-user
WorkingDirectory=/srv/djangoapp

ExecStart=/srv/djangoapp/django/bin/gunicorn 
         —access-logfile - 
         —workers 3 
         —bind unix:gunicorn.sock djangoapp.wsgi

[Install]
WantedBy=multi-user.target

Для Nginx, достаточно просто создать файлы конфигурации прокси и настроить директорию для хранения статичного контента, если вы ее используете. В RHEL файлы конфигурации Nginx находятся /etc/nginx/conf.d. Вы можете скопировать туда следующий пример в файл /etc/nginx/conf.d/default.conf, и запустить сервис. Убедитесь, что указали server_name в соответствии с названием вашего хоста.

server {
   listen 80;
   server_name 8beta1.example.com;

   location = /favicon.ico { access_log off; log_not_found off; }
   location /static/ {
       root /srv/djangoapp;
   }

   location / {
       proxy_set_header Host $http_host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Proto $scheme;
       proxy_pass http://unix:/run/gunicorn.sock;
   }
}

Запускайте сокет Gunicorn и Nginx с помощью systemd, и можно приступать к тестированию.

Ошибка Bad Gateway?

Если вы введете адрес в браузер, то, вероятнее всего, получите ошибку 502 Bad Gateway. Она может быть вызвана некорректно настроенными разрешениями для сокета UNIX, или обусловлена более сложными проблемами, связанными с управлением доступом в SELinux.

В журнале ошибок nginx можно встретить строчку подобного вида:

2018/12/18 15:38:03 [crit] 12734#0: *3 connect() to unix:/run/gunicorn.sock failed (13: Permission denied) while connecting to upstream, client: 192.168.122.1, server: 8beta1.example.com, request: "GET / HTTP/1.1", upstream: "http://unix:/run/gunicorn.sock:/", host: "8beta1.example.com"

Если тестировать Gunicorn напрямую, то получим пустой ответ.

curl —unix-socket /run/gunicorn.sock 8beta1.example.com

Давайте разберемся, почему это происходит. Если открыть журнал, то вероятнее всего, увидим, что проблема связана с SELinux. Поскольку у нас запущен демон, для которого не было создано своей политики, он помечается как init_t. Проверим эту теорию на практике.

sudo setenforce 0

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

Обновив страницу в браузере или перезапустив нашу команду curl, можно лицезреть тестовую страницу Django.

Итак, убедившись, что все работает, и больше никаких проблем с разрешениями нет, мы вновь включаем SELinux.

sudo setenforce 1

Здесь не будет рассказа об audit2allow и о создании политик на основе оповещений с помощью sepolgen, поскольку на данный момент нет реального приложения Django, то нет и полной карты того, к чему Gunicorn может захотеть получить доступ, и к чему этот доступ следует запретить. Поэтому необходимо сохранить работу SELinux для защиты системы, и в то же время разрешить приложению запускаться и оставлять сообщения в журнале аудита, чтобы можно было затем на их основе создать реальную политику.

Указание разрешенных доменов (permissive domains)

О разрешенных доменах в SELinux не все слышали, но в них нет ничего нового. Многие даже работали с ними, сами об этом не догадываясь. Когда создается политика на основе сообщений аудита, созданная политика представляет собой разрешенный домен. Попробуем создать простейшую разрешительную политику.

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

sudo yum install selinux-policy-devel

Механизм разрешенных доменов – прекрасный инструмент для выявления проблем, особенно когда речь идет о кастомном приложении или о приложениях, которые поставляются без уже созданных политик. В данном случае политика разрешенного домена для Gunicorn будет максимально простой – объявим основной тип (gunicorn_t), объявим тип, который мы будем использовать для пометки нескольких исполняемых файлов (gunicorn_exec_t), и затем настроим переход (transition) для system, чтобы корректно отмечать запущенные процессы. Последняя строчка устанавливает политику как разрешенную по умолчанию на момент ее загрузки.

gunicorn.te:

policy_module(gunicorn, 1.0)

type gunicorn_t;
type gunicorn_exec_t;
init_daemon_domain(gunicorn_t, gunicorn_exec_t)
permissive gunicorn_t;

Можно скомпилировать этот файл политик и добавить его в систему.

make -f /usr/share/selinux/devel/Makefile
sudo semodule -i gunicorn.pp

sudo semanage permissive -a gunicorn_t
sudo semodule -l | grep permissive

Давайте проверим, не блокирует ли SELinux что-то еще, помимо того, к чему обращается наш неизвестный демон.

sudo ausearch -m AVC

type=AVC msg=audit(1545315977.237:1273): avc:  denied { write } for pid=19400 comm="nginx" name="gunicorn.sock" dev="tmpfs" ino=52977 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:var_run_t:s0 tclass=sock_file permissive=0

SELinux не дает Nginx записывать данные в сокет UNIX, используемый Gunicorn. Обычно в таких случаях начинают менять политики, но впереди предстоит решить и другие задачи. Можно также изменить настройки домена, превратив его из домена ограничений в домен разрешений. Теперь перенесем httpd_t в домен разрешений. Это обеспечит Nginx необходимый доступ, и мы сможем продолжить дальнейшую работу по отладке.

sudo semanage permissive -a httpd_t

Итак, когда удалось сохранить защиту SELinux (на самом деле не следует оставлять проект с SELinux в режиме ограничений) и домены разрешений загружаются, необходимо выяснить, что именно нужно пометить как gunicorn_exec_t, чтобы все вновь заработало как положено. Попробуем обратиться к веб-сайту, чтобы увидеть новые сообщения об ограничениях в доступе.

sudo ausearch -m AVC -c gunicorn

Можно видеть множество сообщений, содержащих ‘comm=«gunicorn»’, которые выполняют различные действия над файлами в /srv/djangoapp, поэтому, очевидно, это как раз одна из команд, которую стоит пометить.

Но кроме того, появляется сообщение вот такого вида:

type=AVC msg=audit(1545320700.070:1542): avc:  denied { execute } for pid=20704 comm="(gunicorn)" name="python3.6" dev="vda3" ino=8515706 scontext=system_u:system_r:init_t:s0 tcontext=unconfined_u:object_r:var_t:s0 tclass=file permissive=0

Если посмотреть статус сервиса gunicorn или выполнить команду ps, то не появится каких-либо запущенных процессов. Похоже, что gunicorn пытается обратиться к интерпретатору Python в нашем окружении virtualenv, возможно, для запуска рабочих скриптов (workers). Поэтому сейчас пометим эти два исполняемых файла и проверим, получится ли открыть нашу тестовую страницу Django.

chcon -t gunicorn_exec_t /srv/djangoapp/django/bin/gunicorn /srv/djangoapp/django/bin/python3.6

Потребуется перезапустить сервис gunicorn, чтобы можно было выбрать новую метку. Его можно перезапустить сразу или остановить сервис и дать сокету запустить его при открытии сайта в браузере. Убедитесь, что процессы получили нужные метки, используя ps.

ps -efZ | grep gunicorn

Не забудьте потом создать нормальную политику SELinux!

Если посмотреть сообщения AVC сейчас, то последнее сообщение содержит permissive=1 для всего, что относится к приложению, и permissive=0 для всей остальной системы. Если понимать, какой именно доступ необходим реальному приложению, можно быстрее найти оптимальный способ решения подобных проблем. Но до тех пор лучше, чтобы система была защищена, и чтобы получить понятный и пригодный к использованию аудит по проекту Django.

sudo ausearch -m AVC

Получилось!

Появился работающий проект Django с фронтендом на Nginx и Gunicorn WSGI. Мы настроили Python 3 и PostgreSQL 10 из репозиториев RHEL 8 Beta. Теперь можно двигаться дальше и создавать (или просто развертывать) приложения Django или изучать другие доступные инструменты в RHEL 8 Beta для автоматизации процесса настройки, повышения производительности или даже контейнеризации этой конфигурации.

Источник: habr.com