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/octet­stream;

    # 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     utf­8;
   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]
# Django­related settings
# the base directory (full path)
chdir           = {{app_dir}}
# Django's wsgi file
module          = {app_name}.wsgi
# the virtualenv (full path)
home            = {{virtenv_dir}}
# process­related 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
chmod­socket     = 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/epel­release­latest­7.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
­    - python34­devel
­    - python34­virtualenv
­    - python34­pip
­    - 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="UTF­8">
    <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.

blank Autorzy

Artykuły na blogu są pisane przez osoby z zespołu EuroLinux. 80% treści zawdzięczamy naszym developerom, pozostałą część przygotowuje dział sprzedaży lub marketingu. Dokładamy starań, żeby treści były jak najlepsze merytorycznie i językowo, ale nie jesteśmy nieomylni. Jeśli zauważysz coś wartego poprawienia lub wyjaśnienia, będziemy wdzięczni za wiadomość.