
Ansible w Enterprise Linuksie – cz. V: role

Po przerwie powracam do Państwa z artykułem o Ansible – popularnej platformie automatyzacji. Pierwotna wersja tego artykułu zawierała w sobie tylko rozdział o vault (skarbiec, sejf), jednak szczerze przywyknąłem do pisania bardziej treściwych tekstów. Pozwoliłem więc sobie opisać role, a samym vault zająć się w następnej serii.
Po przerwie powracam do Państwa z artykułem o Ansible – popularnej platformie automatyzacji. Pierwotna wersja tego artykułu zawierała tylko rozdział o vault (skarbiec, sejf), jednak szczerze przywyknąłem do pisania bardziej treściwych tekstów. Pozwoliłem więc sobie opisać role, a samym vault zająć się w następnej serii.
Jest to ostatnia część naszej bazowej serii o Ansible. Już teraz mogę Państwu obiecać, iż powstanie następna – musimy przecież zarządzać serwerami z części pierwszej :). Będzie się ona jednak skupiać bardziej na tworzeniu działających ról dla poszczególnych części projektu, integrowaniu Ansible z innymi narzędziami i praktycznym użytkowaniem.
W tej części ustawimy webserwer tak, by przy pomocy nginxa i uWSGI serwował nam bardzo prostą aplikację napisaną w Django.
Django, uWSGI, nginx – krótkie wyjaśnienie
Django – Pythonowy framework do pisania aplikacji webowych.
uWSGI – jest zarówno serwerem aplikacji, jak i protokołu WSGI (Web Server Gateway Interface). WSGI został po raz pierwszy omówiony w PEP (Python Enhancement Proposal) 333.
Nginx – lekki i wyjątkowo wydajny serwer www. Jest to olbrzymie uproszczenie, gdyż Nginx posiada naprawdę wiele możliwości.
Prosty schemat działania
Jako fan prostego ASCII artu pozwoliłem sobie wykonać ten schemat.
+-------+ +-------+ +-------+ | NGINX <---+ uWSGI <---+ DJANGO| | +---> +---> | +-------+ +-------+ +-------+
Jak widać, uWSGI pośredniczy pomiędzy nginxem a aplikacją.
Każdy ma jakąś rolę w życiu – ja mam ją w Ansible :)!
Rola to zbiór zadań (tasks), uchwytów (handlers), plików (files), metainformacji (meta), szablonów (templates) i zmiennych (vars).
Stwórzmy więc odpowiadające im foldery.
mkdir p roles/web_server/{files,handlers,meta,templates,tasks,vars}
Do skrócenia zapisu użyliśmy funkcji rozwinięcia powłoki (shell expansion).
Wyprzedzając trochę fakty – mogliśmy użyć ansible-galaxy init web_server
. Jednak w celu przećwiczenia pozwoliłem sobie wykonać ten krok ręcznie.
Przejdźmy zatem do katalogu webserwera. cd roles/web_server/
Meta
Meta zawiera metainformację – np. zależność od innych ról, minimalną wersję ansible, nazwę autora dla ansible-galaxy itp.
Stwórzmy więc plik vim meta/main.yml
, w którym podkreślimy brak dependencji.
dependencies: []
Files
Do files wrzucamy statyczne pliki, które umieszczamy na serwerze. Mogą być to gotowe konfiguracje niewymagające od nas szablonowania. W związku z faktem, iż nasz webserwer będzie na nginxie, proponuję skorzystać z gotowej konfiguracji. Bardzo dobra konfiguracja wraz z różnymi przepisami podana jest na stronie https://github.com/h5bp/server-configs-nginx. Celem tego artykułu nie jest jednak zapoznanie czytelnika z możliwościami nginxa, a stworzenie roli dostarczającej działającą aplikację.
Oryginalna (out-of-box) konfiguracja nginxa po zainstalowaniu pakietu wygląda następująco.
# For more information on configuration, see: # * Official English Documentation: http://nginx.org/en/docs/ # * Official Russian Documentation: http://nginx.org/ru/docs/ user nginx; worker_processes auto; error_log /var/log/nginx/error.log; pid /run/nginx.pid; # Load dynamic modules. See /usr/share/nginx/README.dynamic. include /usr/share/nginx/modules/*.conf; events { worker_connections 1024; } http { log_format main '$remote_addr $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octetstream; # Load modular configuration files from the /etc/nginx/conf.d directory. # See http://nginx.org/en/docs/ngx_core_module.html#include # for more information. include /etc/nginx/conf.d/*.conf; server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } } # Settings for a TLS enabled server. # # server { # listen 443 ssl http2 default_server; # listen [::]:443 ssl http2 default_server; # server_name _; # root /usr/share/nginx/html; # # ssl_certificate "/etc/pki/nginx/server.crt"; # ssl_certificate_key "/etc/pki/nginx/private/server.key"; # ssl_session_cache shared:SSL:1m; # ssl_session_timeout 10m; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; # # # Load configuration files for the default server block. # include /etc/nginx/default.d/*.conf; # # location / { # } # # error_page 404 /404.html; # location = /40x.html { # } # # error_page 500 502 503 504 /50x.html; # location = /50x.html { # } # } }
Zgodnie ze wskazówkami w repozytorium oraz własnym doświadczeniem zapisujemy zmodyfikowany files/nginx.conf
# For more information on configuration, see: # * Official English Documentation: http://nginx.org/en/docs/ # * Official Russian Documentation: http://nginx.org/ru/docs/ user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; # /var/run is symlink to /run pid /run/nginx.pid; # Load dynamic modules. See /usr/share/nginx/README.dynamic. include /usr/share/nginx/modules/*.conf; worker_rlimit_nofile 8192; events { worker_connections 8000; } http { server_tokens off; log_format main '$remote_addr $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 20; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; charset_types text/css text/plain text/vnd.wap.wml application/javascript application/json application/rss+xml application/xml; gzip on; gzip_comp_level 5; gzip_min_length 256; gzip_proxied any; gzip_vary on; gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; # Load modular configuration files from the /etc/nginx/conf.d directory. # See http://nginx.org/en/docs/ngx_core_module.html#include # for more information. include /etc/nginx/conf.d/*.conf; server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } } }
Ważne informacje dotyczące zmian – nginx ma włączoną między innymi kompresję gzip oraz dostrojonych workerów (robotniczych). Odpowiednie zestrojenie serwera jest ważnie nie tylko ze względu na optymalizacje zasobów, które posiadamy, ale także na zewnętrzny odbiór naszej aplikacji, czy nawet jej dobry ranking w wyszukiwarkach.
Drugim ważnym plikiem który trzymamy jest serwis uWSGI zogdny z systemd. Dzięki niemu będziemy mogli dużo łatwiej kontrolować deployment. files/uwsgi.service
[Unit] Description=uWSGI Emperor service [Service] ExecStart=/usr/bin/uwsgi --emperor /etc/uwsgi/vassals --uid nginx --gid nginx Restart=always KillSignal=SIGQUIT Type=notify NotifyAccess=all [Install] WantedBy=multi-user.target
Vars
W naszych zmiennych będziemy trzymać nazwę domenową projektu, jego położenie, środowisko virtualne i nazwę aplikacji i adres repozytorium gitowego.
Tworzmy więc vars/main.yml
--- domain: helloworld.local virtenv_dir: /var/app/hello_virt_env app_dir: /var/app/hello uwsgi_ini_name: hello_uwsgi.ini app_name: hello git_repo: https://github.com/EuroLinux/hello_world_django2.git
Templates
W templates tworzymy uniwersalny config dla nginx templates/nginx_my_site.j2
upstream django { server unix:///{{app_dir}}/mysite.sock; # for a file socket } server { listen 80; listen [::]:80; server_name {{domain}} www.{{domain}}; charset utf8; location /static { alias {{app_dir}}/static; # your Django project's static files amend as required } location / { uwsgi_pass django; #include {{app_dir}}/uwsgi_params; # the uwsgi_params file you ins talled include /etc/nginx/uwsgi_params; # the uwsgi_params file you insta lled } }
Teraz tworzymy config uWSGI templates/uwsgi.j2
# mysite_uwsgi.ini file [uwsgi] # Djangorelated settings # the base directory (full path) chdir = {{app_dir}} # Django's wsgi file module = {app_name}.wsgi # the virtualenv (full path) home = {{virtenv_dir}} # processrelated settings # master master = true # maximum number of worker processes processes = 10 # the socket (use the full path to be safe socket = {{app_dir}}/mysite.sock # ... with appropriate permissions may be needed chmodsocket = 664 # clear environment on exit vacuum = true
Handlers
Po zainstalowaniu odpowiednich dependencji, zmianach w konfiguracji czy wydaniu nowej wersji oprogramowania, powinniśmy zrestartować lub przeładować nasze usługi. Jak już wcześniej pisałem, służą do tego uchwyty lub jak ktoś woli chwytaki :) – handlers/main.yml
--- - name: Enable and restart nginx service: name: nginx state: restarted enabled: yes listen: "restart web services" - name: Enable and restart uwsgi service: name: uwsgi state: restarted enabled: yes listen: "restart web services" - name: Touch uwsgi (restart vassal). file: path: "{{app_dir}}/{{uwsgi_ini_name}}" state: touch listen: "restart vassals" - name: reboot shell: sleep 5 && shutdown r now "Host restart triggered" async: 1 poll: 0 ignore_errors: true
Nowością jest listen: "restart web services"
, które pojawiło się w Ansible 2.2 i umożliwia tworzenie group handlerów do wywołania.
Tasks
Mając już absolutnie wszystko na miejscu, możemy stworzyć nasze taski. Pozwolę sobie podzielić je ze względu na pełnione funkcje.
tasks/setup_yum.yml
--- -name: Install EPEL yum: name: https://dl.fedoraproject.org/pub/epel/epelreleaselatest7.noarch.rpm state: present - name: Update Packages yum: name: '*' state: latest - name: Install Python 3.4 and virtualenv and git yum: state=present name={{ item }} with_items: - python34 - python34devel - python34virtualenv - python34pip - git - '@development' - name: Install nginx yum: state: present name: nginx
Jest to prosta instalacja repozytorium EPEL, aktualizacji systemu oraz niezbędnych pakietów. Dzięki grupie development będziemy w stanie zainstalować uWSGI. Instalacja serwera nginx jest też dosyć oczywista.
task/setup_app.yml
--- - name: Mkdir for app file: path: "{{app_dir}}" state: directory recurse: yes - name: Clone hello world app git: repo: "{{ git_repo }}" dest: "{{ app_dir }}" version: master accept_hostkey: yes force: yes notify: "restart web services" - name: Make virtual env pip: virtualenv: "{{ virtenv_dir }}" virtualenv_command: virtualenv-3 virtualenv_python: python3.4 requirements: "{{ app_dir }}/requirements.txt" - name: Django makemigrations django_manage: app_path: "{{ app_dir }}" command: makemigrations virtualenv: "{{ virtenv_dir }}" - name: Django migrate django_manage: app_path: "{{ app_dir }}" command: migrate virtualenv: "{{ virtenv_dir }}" # TODO we don't need static files for this example #- name: Django collect static # django_manage: # app_path: "{{ app_dir }}" # command: collectstatic # virtualenv: "{{ virtenv_dir }}" - name: Disable Django debug lineinfile: path: "{{app_dir}}/{{app_name}}/settings.py" regexp: "^DEBUG" line: "DEBUG = False" - name: Django ALLOW Hosts to * lineinfile: path: "{{app_dir}}/{{app_name}}/settings.py" regexp: "^ALLOWED_HOSTS" line: "ALLOWED_HOSTS = [\'*\']"
Same zadania zawarte w setup_app.yml są typowymi czynnościami, które należy wykonać w celu uruchomienia aplikacji Django. Znajdziemy tu zarówno tworzenie migracji, migrację bazy danych, jak i wykomentowane zbieranie plików statycznych. Dalsza część odpowiada za wyłączenie trybu developerskiego (DEBUG = True
) i zezwolenie na łączenie się z aplikacją z dowolnego źródła.
tasks/setup_fixes.yml
--- - name: Put SELinux in permissive selinux: policy: targeted state: permissive - name: Make nginx "{{ app_dir }}" owner file: path: "{{ app_dir }}" owner: nginx group: nginx recurse: yes notify: "restart web services"
Powyższe taski służą, jak sama nazwa wskazuje, naprawieniu uprawnień i ustawieniu SELinuxa w tryb dopuszczający (permissive). W trybie tym SELinux nie blokuje naszych akcji, ale loguje naruszenia polityk. Tryb ten może być używany także do gromadzenia logów w celu stworzenia odpowiednich polityk SELinuxa lub dobrania właściwych ustawień. Poprawne ustawienie SELinuxa wychodzi poza zakres tego artykułu i znacząco by go wydłużyło.
tasks/setup_uwsgi.yml
--- - name: Copy uwsgi template template: src: templates/uwsgi.j2 dest: "{{app_dir}}/{{uwsgi_ini_name}}" - name: Setup uWSGI vasslas dir file: path: /etc/uwsgi/vassals state: directory recurse: yes - name: Setup uWSGI vassals softlink file: src: "{{app_dir}}/{{uwsgi_ini_name}}" dest: "/etc/uwsgi/vassals/{{uwsgi_ini_name}}" state: link - name: Install uwsgi pip pip: name: uwsgi executable: pip3.4 - name: Copy uwsgi service copy: src: files/uwsgi.service dest: /etc/systemd/system/uwsgi.service notify: "restart web services"
Powyższe zadania ustawiają naszego pośrednika pomiędzy nginx i aplikacją. uWSGI będzie pracował w trybie emmperor (imperator). W związku z tym w odpowiednim katalogu (/etc/uwsgi/vassals) będzie poszukiwał plików opisujących aplikację, które ma podawać. Katalog /etc/uwsgi/vassals jest w regularnych odstępach skanowany i w momencie zmiany daty modyfikacji pliku przeładowuje zadanego vassla (wasala).
tasks/setup_nginx.yml
--- - name: Copy nginx conf copy: src: files/nginx.conf dest: /etc/nginx/nginx.conf notify: "restart web services" - name: Copy nginx template template: src: templates/nginx_my_site.j2 dest: /etc/nginx/conf.d/my_site_nginx.conf notify: "restart web services"
Część odpowiedzialna za ustawienia naszego serwera www należy do samotłumaczących się.
Ostatni yaml w roli
tasks/main.yml
W ostatnim playbooku importujemy podplaybooki.
--- - import_tasks: setup_yum.yml - import_tasks: setup_app.yml - import_tasks: setup_fixes.yml - import_tasks: setup_uwsgi.yml - import_tasks: setup_nginx.yml
Urchomienie roli
W katalogu nadrzędnym w stosunku do roli tworzymy playbook uruchomieniowy. setup_apps.yml
--- - hosts: apps user: ansible become: true roles: - web_server
Dla pliku inventory wyglądająceo następująco:
[apps] 192.168.121.173
Wywołanie komendy: ansible-playbook -i inventory setup_apps.yml
zwróci nam:
PLAY [apps] ****************************************************************** ** TASK [Gathering Facts] ******************************************************* ** ok: [192.168.121.173] TASK [web_server : Install EPEL] ********************************************* ** changed: [192.168.121.173] TASK [web_server : Update Packages] ****************************************** ** changed: [192.168.121.173] TASK [web_server : Install Python 3.4 and virtualenv and git] **************** ** changed: [192.168.121.173] => (item=[u'python34', u'python34-devel', u'python3 4-virtualenv', u'python34-pip', u'git', u'@development']) TASK [web_server : Install nginx] ******************************************** ** changed: [192.168.121.173] TASK [web_server : Mkdir for app] ******************************************** ** changed: [192.168.121.173] TASK [web_server : Clone hello world app] ************************************ ** changed: [192.168.121.173] TASK [web_server : Make virtual env] ***************************************** ** changed: [192.168.121.173] TASK [web_server : Django makemigrations] ************************************** ok: [192.168.121.173] TASK [web_server : Django migrate] ******************************************* ** changed: [192.168.121.173] TASK [web_server : Disable Django debug] ************************************* ** changed: [192.168.121.173] TASK [web_server : Django ALLOW Hosts to *] ********************************** ** changed: [192.168.121.173] TASK [web_server : Put SELinux in permissive] ******************************** ** ok: [192.168.121.173] TASK [web_server : Make nginx "/var/app/hello" owner] ************************ ** changed: [192.168.121.173] TASK [web_server : Copy uwsgi template] ************************************** ** changed: [192.168.121.173] TASK [web_server : Setup uWSGI vasslas dir] ********************************** ** changed: [192.168.121.173] TASK [web_server : Setup uWSGI vassals softlink] ***************************** ** changed: [192.168.121.173] TASK [web_server : Install uwsgi pip] **************************************** **changed: [192.168.121.173] TASK [web_server : Copy uwsgi service] *************************************** ** changed: [192.168.121.173] TASK [web_server : Copy nginx conf] ****************************************** ** changed: [192.168.121.173] TASK [web_server : Copy nginx template] ************************************** ** changed: [192.168.121.173] RUNNING HANDLER [web_server : Enable and restart nginx] ********************** ** changed: [192.168.121.173] RUNNING HANDLER [web_server : Enable and restart uwsgi] ********************** ** changed: [192.168.121.173] PLAY RECAP ******************************************************************* ** 192.168.121.173 : ok=23 changed=20 unreachable=0 failed=0
By sprawdzić, czy rzeczywiście udało nam się osiągnąć zamierzony efekt, możemy użyc curla z odpowiednim nagłówiekm.
curl -H 'Host: helloworld.local' 192.168.121.173 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF8"> <title>BFG APP</title> </head> <body><h1>Hello World!</h1> </body> </html>
Bonus – repozytorium gitowe
Cały kod wraz z propozycją dodatkowych ćwiczeń można znaleść w repozytorium: https://github.com/EuroLinux/articles/tree/ansible_5. Zawiera ono także Vagrantfile dla CentOSa 7. Niestety CentOSowe obrazy Vagranta mają domyślnie wyłączone logowanie do ssh za pomocą hasła. W celu użycia pomocniczego playbooka setup_user.yml
należy takie logowanie włączyć. Integrację Ansible z Vagrantem opiszę w dalszych częściach nowej serii.
Zakończenie
Na końcu naszego cyklu stworzyliśmy dość zaawansowaną rolę. Kolejny cykl zaczniemy od stworzenia roli basic_setup, która będzie bardzo podobna do tej poznanej z obowiązkowego playbooka. Połączymy ze sobą role, a także w końcu wykorzystamy vault. To jednak w przyszłym miesiącu.