RHEL 8 Beta oferuje programistom wiele nowych możliwości, których lista могłaby занимать strony, однако najlepiej jest poznawać nowości poprzez praktykę. Dlatego poniżej proponujemy przeprowadzenie praktycznego warsztatu na temat rzeczywistego tworzenia infrastruktury aplikacji na bazie Red Hat Enterprise Linux 8 Beta.

Jako podstawę weźmiemy Pythona, popularny język programowania wśród programistów, oraz kombinację Django i PostgreSQL, dość powszechną parę do tworzenia aplikacji, i skonfigurujemy RHEL 8 Beta do pracy z nimi. Potem dodamy jeszcze kilka (niesekretnych) składników.
Środowisko testowe będzie się zmieniać, ponieważ ciekawi mnie, jak działają możliwości automatyzacji, praca z kontenerami i próba środowisk z wieloma serwerami. Na początek pracy nad nowym projektem można zacząć od stworzenia małego, prostego prototypu ręcznie – w ten sposób można zobaczyć, co dokładnie powinno się wydarzyć i jak odbywa się interakcja, a potem przejść do automatyzacji i tworzenia bardziej złożonych konfiguracji. Dziś opowiem o tworzeniu takiego prototypu.
Zaczniemy od rozwoju obrazu maszyny wirtualnej RHEL 8 Beta VM. Można zainstalować maszynę wirtualną od podstaw lub użyć obrazu gościa KVM, dostępnego wraz z subskrypcją na Beta. Przy użyciu obrazu gościa trzeba skonfigurować wirtualny CD, który będzie zawierał metadane i dane użytkownika do inicjalizacji w chmurze (cloud-init). Nie trzeba dokonywać żadnych szczególnych zmian w strukturze dysku ani dostępnych pakietach, wystarczy dowolna konfiguracja.
Przyjrzyjmy się całemu procesowi bliżej.
Instalacja Django
Z najnowszą wersją Django będzie potrzebne wirtualne środowisko (virtualenv) z Pythonem 3.5 lub nowszą wersją. W uwagach do Beta można zobaczyć, że dostępny jest Python 3.6, sprawdźmy, czy rzeczywiście tak jest:
[cloud-user@8beta1 ~]$ python
-bash: python: command not found
[cloud-user@8beta1 ~]$ python3
-bash: python3: command not found
Red Hat aktywnie wykorzystuje Pythona jako narzędzie systemowe w RHEL, więc dlaczego otrzymujemy taki wynik?
Wielu programistów korzystających z Pythona wciąż rozważa przejście z Pythona 2 na Pythona 2, podczas gdy Python 3 jest aktywnie rozwijany i pojawiają się nowe wersje. Aby zaspokoić potrzebę stabilnych narzędzi systemowych i jednocześnie zapewnić użytkownikom dostęp do różnych nowych wersji Pythona, systemowy Python został przeniesiony do nowego pakietu, umożliwiając instalację zarówno Pythona 2.7, jak i 3.6. Szczegółowe informacje na temat zmian i powodów, dla których to zrobiono, można znaleźć w publikacji na (Langdon White).
Aby uzyskać działający Python, należy zainstalować tylko dwa pakiety, przy czym python3-pip będzie ciągnięty jako zależność.
sudo yum install python36 python3-virtualenv
Dlaczego nie należy używać bezpośrednich odniesień do modułu, jak sugeruje Langdon, i nie zainstalować pip3? Mając na uwadze przyszłą automatyzację, wiadomo, że do działania Ansible potrzebny jest zainstalowany pip, ponieważ moduł pip nie obsługuje wirtualnych środowisk (virtualenvs) z niestandardowym plikiem wykonywalnym pip.
Mając działający interpreter python3, można kontynuować proces instalacji Django i uzyskać działający system wraz z innymi naszymi komponentami. W sieci dostępnych jest wiele opcji realizacji. Tutaj przedstawiona jest jedna wersja, jednak użytkownicy mogą korzystać ze swoich własnych procesów.
Wersje PostgreSQL i Nginx dostępne w RHEL 8 zainstalujemy za pomocą Yum.
sudo yum install nginx postgresql-server
Dla PostgreSQL potrzebny będzie psycopg2, ale musi być dostępny tylko w środowisku virtualenv, więc zainstalujemy go za pomocą pip3 wraz z Django i Gunicorn. Najpierw musimy jednak skonfigurować virtualenv.
Na temat właściwego wyboru miejsca instalacji projektów Django toczy się wiele dyskusji, ale gdy pojawiają się wątpliwości, zawsze można odwołać się do standardu Linux Filesystem Hierarchy Standard. W szczególności w FHS stwierdzono, że /srv jest używane do: „przechowywania danych specyficznych dla konkretnego węzła – danych wydawanych przez system, na przykład danych i skryptów serwerów WWW, danych przechowywanych na serwerach FTP oraz repozytoriów systemów kontroli wersji (pojawiających się w FHS-2.3 w 2004 roku)”.
To jest dokładnie nasz przypadek, dlatego wkładamy wszystko, co potrzebne, do /srv, którego właścicielem jest nasz użytkownik aplikacji (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
Konfiguracja PostgreSQL i Django nie sprawia problemów: tworzymy bazę danych, tworzymy użytkownika, konfigurujemy uprawnienia. Jest jedna rzecz, o której należy pamiętać przy początkowej instalacji PostgreSQL – to skrypt postgresql-setup, który jest instalowany razem z pakietem postgresql-server. Ten skrypt pomaga w wykonywaniu podstawowych zadań związanych z administracją klastra baz danych, na przykład, inicjalizacją klastra lub procesem aktualizacji. Aby skonfigurować nowy egzemplarz PostgreSQL w systemie RHEL, musimy wykonać polecenie:
sudo /usr/bin/postgresql-setup -initdb
Po tym można uruchomić PostgreSQL za pomocą systemd, stworzyć bazę danych i skonfigurować projekt w Django. Pamiętaj, aby ponownie uruchomić PostgreSQL po wprowadzeniu zmian w pliku konfiguracyjnym uwierzytelniania klienta (zwykle jest to pg_hba.conf) w celu ustawienia przechowywania hasła dla użytkownika aplikacji. Jeśli napotkasz inne problemy, upewnij się, że zmienione są ustawienia IPv4 i IPv6 w pliku 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
W pliku /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
W pliku /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 }}',
}
}
Po skonfigurowaniu pliku settings.py w projekcie i ustawieniu konfiguracji bazy danych, można uruchomić serwer deweloperski, aby upewnić się, że wszystko działa. Po uruchomieniu serwera deweloperskiego dobrze jest stworzyć użytkownika admin, aby przetestować połączenie z bazą danych.
./manage.py runserver 0.0.0.0:8000
./manage.py createsuperuser
WSGI? Co to takiego?
Serwer deweloperski jest przydatny przy testowaniu, ale aby uruchomić aplikację, należy skonfigurować odpowiedni serwer i proxy dla Web Server Gateway Interface (WSGI). Istnieje kilka powszechnie stosowanych kombinacji, na przykład Apache HTTPD z uWSGI lub Nginx z Gunicorn.
Zadaniem Web Server Gateway Interface jest przekierowywanie zapytań od serwer WWW do frameworka WSGI w Pythonie. WSGI to dziedzictwo przestarzałych rozwiązań, kiedy używane były mechanizmy CGI, a dzisiaj WSGI stanowi standard niezależnie od używanego serwera WWW czy frameworka Python. Mimo jego powszechnego zastosowania, istnieje wiele niuansów związanych z pracą z tymi frameworkami oraz liczne możliwości wyboru. W tym przypadku spróbujemy nawiązać współpracę pomiędzy Gunicorn a Nginx przez socket.
Ponieważ oba te komponenty są zainstalowane na tym samym serwerze, spróbujemy użyć socketu UNIX zamiast socketu sieciowego. Skoro do komunikacji w każdym przypadku potrzebny jest socket, przypuśćmy krok dalej i skonfigurujmy aktywację socketu dla Gunicorn za pomocą systemd.
Proces tworzenia usług aktywowanych socketami jest stosunkowo prosty. Najpierw tworzy się plik jednostki, który zawiera dyrektywę ListenStream wskazującą punkt, w którym zostanie utworzony socket UNIX, następnie — plik jednostki dla usługi, w którym dyrektywa Requires wskazuje na plik jednostki socketu. W pliku jednostki usługi pozostaje tylko uruchomić Gunicorn z wirtualnego środowiska i utworzyć powiązanie WSGI dla socketu UNIX i aplikacji Django.
Oto kilka przykładów plików jednostek, które można wziąć za podstawę. Najpierw konfigurujemy socket.
[Unit]
Description=Socket WSGI Gunicorn
[Socket]
ListenStream=\/run\/gunicorn.sock
[Install]
WantedBy=sockets.target
Teraz konieczne jest skonfigurowanie demona Gunicorn.
[Unit]
Description=Daemon Gunicorn
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
Dla Nginx wystarczy po prostu utworzyć pliki konfiguracyjne proxy i skonfigurować katalog do przechowywania statycznej zawartości, jeśli go używasz. W RHEL pliki konfiguracyjne Nginx znajdują się w \/etc\/nginx\/conf.d. Możesz skopiować poniższy przykład do pliku \/etc\/nginx\/conf.d\/default.conf i uruchomić usługę. Upewnij się, że ustawiłeś server_name zgodnie z nazwą hosta.
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;
}
}
Uruchom socket Gunicorn i Nginx za pomocą systemd, a następnie możesz przystąpić do testowania.
Błąd Bad Gateway?
Jeśli wpiszesz adres w przeglądarce, najprawdopodobniej otrzymasz błąd 502 Bad Gateway. Może on być spowodowany niewłaściwie skonfigurowanymi uprawnieniami dla gniazda UNIX lub bardziej złożonymi problemami związanymi z zarządzaniem dostępem w SELinux.
W dzienniku błędów nginx można znaleźć wpis podobnego typu:
2018/12/18 15:38:03 [crit] 12734#0: *3 connect() do unix:/run/gunicorn.sock nie powiodło się (13: Odrzucone uprawnienia) podczas łączenia z upstream, klient: 192.168.122.1, serwer: 8beta1.example.com, żądanie: "GET / HTTP/1.1", upstream: "http://unix:/run/gunicorn.sock:/", host: "8beta1.example.com"
Jeśli testujemy Gunicorn bezpośrednio, otrzymamy pustą odpowiedź.
curl —unix-socket /run/gunicorn.sock 8beta1.example.com
Zastanówmy się, dlaczego to się dzieje. Jeśli otworzymy dziennik, najprawdopodobniej zobaczymy, że problem jest związany z SELinux. Ponieważ mamy uruchomioną usługę, dla której nie stworzono własnej polityki, jest ona oznaczana jako init_t. Sprawdźmy tę teorię w praktyce.
sudo setenforce 0
Może to wywołać krytykę i łzy, ale to tylko debugowanie prototypu. Wyłączymy sprawdzanie, aby upewnić się, że problem tkwi w tym, a potem przywrócimy wszystko na swoje miejsca.
Odświeżając stronę w przeglądarce lub ponownie uruchamiając naszą komendę curl, można zobaczyć stronę testową Django.
Zatem upewniwszy się, że wszystko działa i nie ma więcej problemów z uprawnieniami, ponownie włączamy SELinux.
sudo setenforce 1
Nie będzie tutaj opowieści o audit2allow i o tworzeniu polityk na podstawie powiadomień za pomocą sepolgen, ponieważ na chwilę obecną nie ma rzeczywistej aplikacji Django, więc nie ma pełnej mapy tego, do czego Gunicorn może chcieć uzyskać dostęp i czego ten dostęp należy zabronić. Dlatego konieczne jest zachowanie działania SELinux dla ochrony systemu, a jednocześnie zezwolenie aplikacji na uruchamianie i zapisywanie wpisów w dzienniku audytu, aby na ich podstawie można było następnie stworzyć prawdziwą politykę.
Określenie dozwolonych domen (permissive domains)
Nie wszyscy słyszeli o dozwolonych domenach w SELinux, ale nie ma w nich nic nowego. Wiele osób nawet z nimi pracowało, nie zdając sobie z tego sprawy. Kiedy tworzona jest polityka na podstawie wiadomości audytowych, stworzona polityka to dozwolona domena. Spróbujmy stworzyć najprostszą politykę zezwalającą.
Aby stworzyć konkretny dozwolony domenę dla Gunicorn, potrzebna jest odpowiednia polityka oraz konieczne będzie oznaczenie odpowiednich plików. Ponadto potrzebne są narzędzia do zbudowania nowych polityk.
sudo yum install selinux-policy-devel
Mechanizm dozwolonych domen to doskonałe narzędzie do identyfikacji problemów, szczególnie w przypadku aplikacji niestandardowych lub aplikacji dostarczanych bez wcześniej utworzonych polityk. W tym przypadku polityka dozwolonej domeny dla Gunicorn będzie maksymalnie prosta – zdefiniujemy główny typ (gunicorn_t), określimy typ, który będziemy używać do oznaczania kilku plików wykonywalnych (gunicorn_exec_t), a następnie skonfigurujemy przejście (transition) dla systemu, aby poprawnie oznaczać uruchomione procesy. Ostatnia linia ustawia politykę jako dozwoloną domyślnie w momencie jej ładowania.
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;
Można skompilować ten plik polityk i dodać go do systemu.
make -f /usr/share/selinux/devel/Makefile
sudo semodule -i gunicorn.pp
sudo semanage permissive -a gunicorn_t
sudo semodule -l | grep permissive
Sprawdźmy, czy SELinux blokuje coś poza tym, do czego odwołuje się nasz nieznany demon.
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 nie zezwala Nginxowi na zapisywanie danych w gnieździe UNIX, używanym przez Gunicorn. Zazwyczaj w takich przypadkach zaczyna się zmieniać polityki, jednak z przodu czekają inne zadania do rozwiązania. Można również zmienić ustawienia domeny, przekształcając ją z domeny restrykcyjnej w domenę zezwalającą. Teraz przenieśmy httpd_t do domeny zezwalającej. Zapewni to Nginxowi niezbędny dostęp i będziemy mogli kontynuować dalsze prace nad debugowaniem.
sudo semanage permissive -a httpd_t
Zatem, gdy udało się zachować zabezpieczenia SELinux (w rzeczywistości nie należy pozostawiać projektu w trybie ograniczeń SELinux) i domeny zezwalające są załadowane, należy ustalić, co dokładnie trzeba oznaczyć jako gunicorn_exec_t, aby wszystko znów działało jak należy. Spróbujmy odwołać się do strony internetowej, aby zobaczyć nowe komunikaty o ograniczeniach dostępu.
sudo ausearch -m AVC -c gunicorn
Można zobaczyć wiele komunikatów zawierających 'comm="gunicorn"', które wykonują różne działania na plikach w /srv/djangoapp, dlatego oczywiście jest to jedna z komend, którą warto oznaczyć.
Ale poza tym pojawia się komunikat w takim stylu:
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
Jeśli spojrzysz na status usługi gunicorn lub wykonasz komendę ps, nie pojawią się żadne uruchomione procesy. Wygląda na to, że gunicorn próbuje uzyskać dostęp do interpretera Pythona w naszym środowisku virtualenv, prawdopodobnie w celu uruchomienia skryptów roboczych (workers). Dlatego teraz oznaczymy te dwa pliki wykonywalne i sprawdzimy, czy uda się otworzyć naszą testową stronę Django.
chcon -t gunicorn_exec_t /srv/djangoapp/django/bin/gunicorn /srv/djangoapp/django/bin/python3.6
Będzie trzeba zrestartować usługę gunicorn, aby można było wybrać nową etykietę. Można ją zrestartować od razu lub zatrzymać usługę i pozwolić gniazdu uruchomić ją przy otwieraniu strony w przeglądarce. Upewnij się, że procesy otrzymały odpowiednie etykiety, używając ps.
ps -efZ | grep gunicorn
Nie zapomnij później stworzyć odpowiedniej polityki SELinux!
Jeśli spojrzysz na komunikaty AVC teraz, ostatni komunikat zawiera permissive=1 dla wszystkiego, co dotyczy aplikacji, oraz permissive=0 dla całego systemu. Rozumiejąc, jaki dokładnie dostęp potrzebuje rzeczywista aplikacja, można szybciej znaleźć optymalny sposób rozwiązania podobnych problemów. Ale do tego czasu lepiej, aby system był zabezpieczony, i aby uzyskać zrozumiałą i przydatną audyt na projekcie Django.
sudo ausearch -m AVC
Udało się!
Pojawił się działający projekt Django z frontendem na Nginx i Gunicorn WSGI. Ustawiliśmy Python 3 i PostgreSQL 10 z repozytoriów RHEL 8 Beta. Teraz możemy pójść dalej i tworzyć (lub po prostu wdrażać) aplikacje Django lub badać inne dostępne narzędzia w RHEL 8 Beta do automatyzacji procesu konfiguracji, zwiększania wydajności lub nawet konteneryzacji tej konfiguracji.
Źródło: habr.com
