Changeset - 981584549895
[Not reviewed]
0 1 8
Branko Majic (branko) - 9 years ago 2015-05-10 00:48:54
branko@majic.rs
MAR-5: Adding the initial implementation of the WSGI website role.
9 files changed with 313 insertions and 1 deletions:
0 comments (0 inline, 0 general)
docs/rolereference.rst
Show inline comments
 
@@ -896,7 +896,7 @@ Here is an example configuration for setting-up web server:
 

	
 

	
 
PHP Website
 
--------
 
-----------
 

	
 
The ``php_website`` role can be used for setting-up a website powered by PHP on
 
destination machine.
 
@@ -1013,3 +1013,133 @@ Here is an example configuration for setting-up a (base) PHP website (for runnin
 
        - php5-json
 
        - php5-mysql
 
        - php5-curl
 

	
 

	
 
WSGI Website
 
------------
 

	
 
The ``wsgi_website`` role can be used for setting-up a website powered by Python
 
on destination machine. The website needs to use the WSGI specification for
 
making the Python web application(s) available.
 

	
 
This role is normally not supposed to be used directly, but should instead serve
 
as the basis for writing website-specific roles. Therefore the role is written
 
in quite generic way, allowing the integrator to write his/her own logic for
 
deploying the necessary Python applications/packages, while still reusing a
 
common base and reducing the workload.
 

	
 
The role implements the following:
 

	
 
* Creates a dedicated user/group for running the WSGI application.
 
* Creates a base directory where the website-specific code and data should be
 
  stored at.
 
* Adds nginx to website's group, so nginx could read the necessary files.
 
* Adds website administrator to website's group, so administrator could manage
 
  the code and data.
 
* Installs additional packages required for running the role (as configured).
 
* Sets-up a dedicated Python virtual environment for website.
 
* Install Gunicorn in Python virtual environment.
 
* Installs additional packages required for running the role in Python virtual
 
  environment (as configured).
 
* Configures systemd to run the website code (using Gunicorn)
 
* Configures nginx to serve the website (static files served directly, requests
 
  passed on to Gunicorn).
 

	
 
The role is implemented with the following layout/logic in mind:
 

	
 
* Website users are named after the ``FQDN`` (fully qualified domain name) of
 
  website, in format of ``web-ESCAPEDFQDN``, where ``ESCAPEDFQDN`` is equal to
 
  ``FQDN`` where dots have been replaced by underscores (for example,
 
  ``web-wiki_example_com``).
 
* All websites reside within a dedicated sub-directory in ``/var/www``. The
 
  sub-directory name is equal to the ``FQDN`` used for accessing the
 
  website. Owner of the directory is set to be the application administrator,
 
  while group is set to be the website group. Additionally, ``SGID`` bit is set
 
  on the directory. This allows admin, with correct umask, to create necessary
 
  files and directories that should be readable (and eventually writeable) by
 
  the website user (running the WSGI application) without having to become root.
 
* All files placed in the website directory should be either created there
 
  directly, or copied to the directory in order to make sure the ``SGID`` gets
 
  honored. **Do not move the files, the permissions will not be set correctly.**
 
* Within the website directory, Python virtual environment can be found within
 
  the ``virtualenv`` sub-directory. The virtual environment is also symlinked to
 
  website admin's ``~/.virtualenvs/`` directory for easier access (and
 
  auto-completion with virtualenvwrapper).
 
* Within the website directory, nginx will expect to find the static files
 
  within the ``htdocs`` sub-directory (this can be symlink too). Locations/aliases
 
  can be configured for static file serving.
 
* Within the website directory, systemd service will expect to find the website
 
  code within the ``code`` sub-directory (this can be symlink too).
 
* nginx communicates with WSGI server over a dedicated Unix socket for each
 
  website.
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**admin** (string, mandatory)
 
  Name of the operating system user in charge of maintaining the website. This
 
  user is capable of making modifications to website configuration anda data
 
  stored within the website directory.
 

	
 
**fqdn** (string, mandatory)
 
  Fully-qualified domain name where the website is reachable. This value is used
 
  for calculating the user/group name for dedicated website user, as well as
 
  home directory of the website user (where data/code should be stored at).
 

	
 
**packages** (list, optional)
 
  A list of additional packages to install for this particular WSGI
 
  website. This is usually going to be development libraries for building Python
 
  packages.
 

	
 
**rewrites** (list, optional)
 
  A list of rewrite rules that are applied to incoming requests. Each element of
 
  the list should be a string value compatible with the format of ``nginx``
 
  option ``rewrite``. The keyword ``rewrite`` itself should be omitted, as well
 
  as trailing semi-colon (``;``).
 

	
 
**static_locations** (list, optional)
 
  List of locations that should be treated as static-only, and not processed by
 
  the WSGI application at all. This is normally used for designating serving of
 
  static/media files by Nginx (for example, in case of Django projects for
 
  ``/static/`` and ``/media/``).
 

	
 
**uid** (integer, mandatory)
 
  UID/GID (they are set-up to be the same) of the dedicated website
 
  user/group.
 

	
 
**use_paste** (boolean, optional)
 
  Tell Gunicorn to assume that the passed-in ``wsgi_application`` value is a
 
  filename of a Python Paste ``ini`` file instead of WSGI application.
 

	
 
**virtuaelnv_packages** (list, optional)
 
  A list of additional packages to install for this particular PHP
 
  appliction. This is usually going to be different PHP extensions.
 

	
 
**wsgi_application** (string, mandatory)
 
  WSGI application that should be started by Gunicorn. The format should be
 
  conformant to what the ``gunicorn`` command-line tool accepts. If the
 
  ``use_paste`` option is enabled, the value should be equal to filename of the
 
  Python Paste ini file, located in the ``code`` sub-directory.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up a (base) WSGI website (for
 
running a bare Django project):
 

	
 
.. code-block:: yaml
 

	
 
    ---
 

	
 
    - role: wsgi_website
 
      admin: admin
 
      fqdn: django.example.com
 
      static_locations:
 
        - /static
 
        - /media
 
      uid: 2004
 
      virtualenv_packages:
 
        - django
 
      wsgi_application: django_example_com.wsgi:application
roles/wsgi_website/defaults/main.yml
Show inline comments
 
new file 100644
 
---
 

	
 
packages: []
 
rewrites: []
 
static_locations: []
 
use_paste: False
 
virtualenv_packages: []
 
\ No newline at end of file
roles/wsgi_website/handlers/main.yml
Show inline comments
 
new file 100644
 
---
 

	
 
- name: Restart website
 
  service: name="{{ fqdn }}" state=restarted
roles/wsgi_website/tasks/main.yml
Show inline comments
 
new file 100644
 
---
 

	
 
- set_fact:
 
    user: "web-{{ fqdn | replace('.', '_') }}"
 
    home: "/var/www/{{ fqdn }}"
 

	
 
- name: Create WSGI website group
 
  group: name="{{ user }}" gid="{{ uid }}" state=present
 

	
 
- name: Create home directory for the user (avoid populating with skeleton)
 
  file: path="{{ home }}" state=directory
 
        owner="{{ admin }}" group="{{ user }}" mode=2750
 

	
 
- name: Create WSGI website user
 
  user: name="{{ user }}" uid="{{ uid }}" group="{{ user }}"
 
        system=yes createhome=no state=present
 

	
 
- name: Add nginx user to website group
 
  user: name="www-data" groups="{{ user }}" append="yes"
 
  notify:
 
    - Restart nginx
 

	
 
- name: Add admin to website group
 
  user: name="{{ admin }}" groups="{{ user }}" append="yes"
 

	
 
- name: Create directory for storing socket file
 
  file: path="/var/run/wsgi/{{ fqdn }}" state="directory"
 
        owner="{{ user }}" group="www-data" mode="750"
 

	
 
- name: Install extra packages for website
 
  apt: name="{{ item }}" state=present
 
  with_items: packages
 

	
 
- name: Create directory for storing the Python virtual environment
 
  file: path="{{ home }}/virtualenv" state=directory
 
        owner="{{ admin }}" group="{{ user }}" mode="2750"
 

	
 
- name: Create Python virtual environment
 
  sudo_user: "{{ admin }}"
 
  command: /usr/bin/virtualenv "{{ home }}/virtualenv" creates="{{ home }}/virtualenv/bin/activate"
 

	
 
- name: Create directory where virtualenvs will be symlinked to
 
  sudo_user: "{{ admin }}"
 
  file: path="~/.virtualenvs" state=directory mode=750
 

	
 
- name: Create convenience symlink for Python virtual environment wrapper utility
 
  sudo_user: "{{ admin }}"
 
  file: src="{{ home }}/virtualenv" dest="~/.virtualenvs/{{ fqdn }}" state=link
 

	
 
- name: Deploy virtualenv wrapper
 
  template: src="venv_exec.j2" dest="{{ home }}/virtualenv/bin/exec"
 
            owner="{{ admin }}" group="{{ user }}" mode="750"
 

	
 
- name: Install Gunicorn in Python virtual environment
 
  sudo_user: "{{ admin }}"
 
  pip: name=gunicorn state=present virtualenv="{{ home }}/virtualenv"
 

	
 
- name: Install additional packages in Python virtual environment
 
  sudo_user: "{{ admin }}"
 
  pip: name="{{ item }}" state=present virtualenv="{{ home }}/virtualenv"
 
  with_items: virtualenv_packages
 

	
 
- name: Deploy systemd socket configuration for website
 
  template: src="systemd_wsgi_website.socket.j2" dest="/etc/systemd/system/{{ fqdn }}.socket"
 
            owner=root group=root mode=644
 
  notify:
 
    - Reload systemd
 
    - Restart website
 

	
 
- name: Deploy systemd service configuration for website
 
  template: src="systemd_wsgi_website.service.j2" dest="/etc/systemd/system/{{ fqdn }}.service"
 
            owner=root group=root mode=644
 
  notify:
 
    - Reload systemd
 
    - Restart website
 

	
 
- name: Enable the website service
 
  service: name="{{ fqdn }}" enabled=yes state=started
 

	
 
- name: Create directory where static files can be served from
 
  file: path="{{ home }}/htdocs/" state=directory
 
        owner="{{ admin }}" group="{{ user }}" mode="2750"
 

	
 
- name: Deploy nginx configuration file for website
 
  template: src="nginx_site.j2" dest="/etc/nginx/sites-available/{{ fqdn }}"
 
            owner=root group=root mode=640 validate="/usr/local/bin/nginx_verify_site.sh -n '{{ fqdn }}' %s"
 
  notify:
 
    - Restart nginx
 

	
 
- name: Enable nginx website
 
  file: src="/etc/nginx/sites-available/{{ fqdn }}" dest="/etc/nginx/sites-enabled/{{ fqdn }}"
 
        state=link
 
  notify:
 
    - Restart nginx
roles/wsgi_website/templates/nginx_site.j2
Show inline comments
 
new file 100644
 
server {
 
    listen 80;
 

	
 
    root {{ home }}/htdocs/;
 

	
 
    server_name {{ fqdn }};
 

	
 
    # Site rewrites.
 
    {% for rewrite in rewrites -%}
 
    rewrite {{ rewrite }};
 
    {% endfor %}
 

	
 
    # Static locations
 
    {% for location in static_locations -%}
 
    location {{ location }} {
 
        try_files $uri $uri/ =404;
 
    }
 
    {% endfor %}
 

	
 
    # Pass remaining requests to the WSGI server.
 
    location / {
 
        try_files $uri @proxy_to_app;
 
    }
 

	
 
    location @proxy_to_app {
 
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
        proxy_set_header Host $http_host;
 
        proxy_redirect off;
 

	
 
        proxy_pass http://unix:/run/wsgi/{{ fqdn }}.sock;
 
    }
 

	
 
    access_log /var/log/nginx/{{ fqdn }}-access.log;
 
    error_log /var/log/nginx/{{ fqdn }}-error.log;
 
}
roles/wsgi_website/templates/supervisor_site.conf.j2
Show inline comments
 
new file 100644
 
[program:{{ fqdn }}]
 
command={{ home }}/virtualenv/bin/gunicorn --bind unix:/var/run/wsgi/{{ fqdn }}/socket {% if use_paste %}--paste{% endif %} {{ wsgi_application }}
 
directory={{ home }}/code
 
user={{ user }}
 
group={{ user }}
 
autostart=true
 
autorestart=true
 
umask=0007
 
redirect_stderr=true
 
environment=HOME="{{ home }}"
roles/wsgi_website/templates/systemd_wsgi_website.service.j2
Show inline comments
 
new file 100644
 
[Unit]
 
Description=Website {{ fqdn }}
 
Requires={{ fqdn }}.socket
 
After=network.target
 

	
 
[Service]
 
User={{ user }}
 
Group={{ user }}
 
WorkingDirectory={{ home }}/code
 
ExecStart={{ home }}/virtualenv/bin/gunicorn --bind unix:/run/wsgi/{{ fqdn }}.sock {% if use_paste %}--paste {{home}}/code/{{ wsgi_application }}{% else %}{{ wsgi_application }}{% endif %}
 

	
 
ExecReload=/bin/kill -s HUP $MAINPID
 
ExecStop=/bin/kill -s TERM $MAINPID
 
PrivateTmp=true
 

	
 
[Install]
 
WantedBy=multi-user.target
 
\ No newline at end of file
roles/wsgi_website/templates/systemd_wsgi_website.socket.j2
Show inline comments
 
new file 100644
 
[Unit]
 
Description=Socket for website {{ fqdn }}
 

	
 
[Socket]
 
ListenStream=/run/wsgi/{{ fqdn }}.sock
 
SocketUser=www-data
 
SocketGroup=www-data
 
SocketMode=0660
 

	
 
[Install]
 
WantedBy=sockets.target
roles/wsgi_website/templates/venv_exec.j2
Show inline comments
 
new file 100644
 
#!/bin/bash
 

	
 
source {{ home }}/virtualenv/bin/activate
 
$@
0 comments (0 inline, 0 general)