Deploy Django Applications Using Ansible for Dummies

Good day!

Most recently, my colleague introduced me to a wonderful manual labor automation tool called Ansbile . After that, the idea was instantly born to write something of their own, which simplifies the very manual labor. What do you have to do most often with your hands? That's right, deploy.

In this article, I will talk about how to use ansible to roll out a django project on a clean remote ubuntu 14.04 server, while creating a separate user for the project.

What is ansible? A set of commands written in python that allow you to automatically perform specified actions on remote machines. Wonderful! Let's build an action plan, how would we do it with pens?
  • Part 1 (and using superuser rights)
    • Install software on a clean virtual machine: mariadb, nginx, supervisor, python-mysqldb, python-virtualenv, python-pip, supervisor, uwsgi;
    • Configure nginx config;
    • Configure supervisor config;
    • Create a new user of the system specifically for this project (by itself optional);
  • Part 2 (on behalf of the user, the project owner)
    • Copy the project;
    • Create a virtual environment (with the necessary packages inside);
    • Configure local_settings.py (we store the database access settings in it, as well as the paths STATIC_ROOT and MEDIA_ROOT);
    • Create a table structure in the database (syncdb for django <1.7, migrate);
    • Collect all the statics in one place;
  • Part 3 (again, switch to superuser rights)
    • Restart mysql;
    • Restart nginx;
    • Restart supervisor;


Without further ado, we get down to business.

Where does ansible begin - with the hosts file. In it, we indicate all the machines available to us, over which actions will be performed. In our case, there’s only one machine (I won’t talk about how to perform actions on several cars, this is not the purpose of the article) and the file looks like this:

[project-hosts]
root ansible_ssh_host=192.168.0.102 ansible_ssh_user=freylis ansible_ssh_pass=z ansible_sudo_pass=z
user ansible_ssh_host=192.168.0.102 ansible_ssh_user=example2 ansible_ssh_pass=zz
[user-hosts]
user
[root-hosts]
root

Let's take a look.
The [project-hosts] section lists all the machines at our disposal, indicating the details of access to them. In our case, the machine is one, but it is indicated two times: the first contains the root details on behalf of which the system will be configured, the second contains the details of the user who has not yet been created, the owner of our django application. The first parameter is the alias of this machine.

The [user-hosts] and [root-hosts] sections combine the machines into groups for general purposes (I hope this is understandable. Routes in one group, non-root in another).

So, with the hosts sorted out. Now you need to somehow tell ansible what to do.

But first, I’ll talk about variables. It is clear that, for example, the name of the project is used repeatedly: in the nginx config, supervisor, the path to the project, etc. Ansible has many ways to specify variables, but I prefer the following: in the grpou_vars directory, create a file with the name of the section from the hosts file. For example, I did not bother and created a file called project-hosts. Now, the variables declared in this file will be accessible to all machines included in the [project-hosts] section, i.e. globally on our project.

Here is all I needed for this project (ansible uses jinja2 syntax):

#
# system options
#
# linux username
username:
# about password crypt
# http://docs.ansible.com/faq.html#how-do-i-generate-crypted-passwords-for-the-user-module
# or run `mkpasswd --method=SHA-512`
# here crypted
user_crypt_password:
# really password
user_password: z
user_homedir: "/home/{{ username }}"
mysql_root_user: root   # root mysql user
mysql_root_password: ""
#
# project options
#
# project slug ( if u have `example/manage.py` and example/example/settings.py` - `example` is project_slug)
project_slug:
# url or list urls for nginx
project_url:
project_dir: "{{ user_homedir }}/projects/{{ project_slug }}"
project_homedir: "{{ user_homedir }}/projects/{{ project_slug }}/{{ project_slug }}"
# virtualenv name
env: "{{ project_dir }}/env"
# port for uwsgi, must be unique for each project
uwsgi_port: 9000
# mysql database for current project
mysql_database: "{{ project_slug }}"
# mysql user for current project
mysql_user:
# mysql user password for current project
mysql_user_password:
#
# django settings
#
debug: True
local_settings: 'local_settings.py'
# set empty string if not used
requirements: 'requirements.txt'

Let's first deal with the system setup. Create the root-playbook.yml file with the following contents:
---
- hosts: root-hosts
  sudo: true
  roles:
    - system

Here I will explain two things:

1. hosts: root-hosts - a directive that tells us about which machines in which group to perform the following actions;
2. roles: system - a list of directories with further action scenarios.

Let's look into the scripts directory already. It has the following form:

roles/
    system/
        handlers/
            main.yml
        tasks/
            main.yml
        templates/
            nginx.j2
            supervisor.j2

Here:
handlers - stores a description of handlers. For example, contains a description of how to restart nginx;
tasks - the head for everything. Job List for Ansible;
templates - file templates that we need for some demons;

Let's go in order.

handlers:
---
- name: restart site
  supervisorctl: name={{ project_url }} state=restarted
- name: restart mysql
  service: name=mysql state=restarted enabled=yes
- name: restart nginx
  service: name=nginx state=restarted enabled=yes

Ansible’s Yml syntax is clear: directive name, directive itself, action with parameters. These handlers will be used in our tasks, but the task has completed - be so kind as to start the necessary handler (notify section)
tasks:

---
# apt-get update
- name: updating the system
  apt: update_cache=yes cache_valid_time=86400
  notify:
  - restart server
# добавить apt-key для установки mariadb
- name: Add mariadb apt repository key
  apt_key: id=0xcbcb082a1bb943db keyserver=hkp://keyserver.ubuntu.com:80 state=present
# добавить репозиторий для установки mariadb
- name: Add mariadb apt repository
  apt_repository: repo='deb http://mirror.timeweb.ru/mariadb/repo/10.1/debian wheezy main' state=present
# установить необходимые пакеты
- name: install packages
  apt: pkg={{ item.name }} state=present
  with_items:
    - name: python-mysqldb
    - name: python-virtualenv
    - name: python-pip
    - name: supervisor
    - name: mariadb-server
    - name: nginx
    - name: uwsgi
    - name: uwsgi-plugin-python
# скопировать файл supervisor.conf.j2 из директории templates в директорию на удаленном сервере (об этом чуть ниже)
- name: copy supervisor config
  template: src=supervisor.conf.j2 dest=/etc/supervisor/conf.d/{{ project_url }}.conf
  notify:
    - restart site
# создать нового пользователя системы
- name: create linux user
  user: name={{ username }} shell=/bin/bash home={{ user_homedir }} password={{ user_crypt_password }}
# создать пользователя mysql для этого проекта
- name: Create MySQL user
  mysql_user: >
    name={{ mysql_user }}
    host=%
    password={{ mysql_user_password }}
    priv={{ mysql_database }}.*:ALL
    login_user={{ mysql_root_user }}
    login_password={{ mysql_root_password }}
    state=present
  notify:
    - restart mysql
# create database
- name: Create MySQL database
  mysql_db: >
    name={{ mysql_database }}
    collation=utf8_general_ci
    encoding=utf8
    login_user={{ mysql_root_user }}
    login_password={{ mysql_root_password }}
    state=present
  notify:
    - restart mysql
# скопировать nginx.j2 конфиг из templates в директорию на удаленно сервере
- name: copy nginx config
  template: src=nginx.j2 dest=/etc/nginx/sites-available/{{ project_url }}
  notify:
    - restart nginx
- name: create symlink nginx config
  file: src=/etc/nginx/sites-available/{{ project_url }} dest=/etc/nginx/sites-enabled/{{ project_url }} state=link

We will analyze one section line by line:

- name: updating the system - name displayed during the deployment process
- apt: update_cache = yes cache_valid_time = 86400: apt - name of the ansible directive (I call them directives). update_cache, cache_valid_time - directive parameters;
- notify: - restart server - action from handlers, which must be done upon completion of the task.

Actually, the syntax is extremely simple. If any parameters are not clear - you can read the documentation for Ansible. But I would like to pay attention to the template directive. It takes two parameters: src - the name of the source file stored in the templates directory of the current role and dest - where to put this file, after rendering, using all available variables.

For example, my nginx.j2 template file looks like this:

server {
        root {{ project_dir }}/{{ project_slug }};
        access_log {{ project_dir }}/logs/nginx-access.log;
        error_log {{ project_dir }}/logs/nginx-errors.log;
        server_name {{ project_url }};
        gzip             on;
        gzip_min_length  1000;
        gzip_proxied     expired no-cache no-store private auth;
        gzip_types       text/plain application/xml;
        location / {
                include uwsgi_params;
                uwsgi_pass 127.0.0.1:{{ uwsgi_port }};
        }
        location /static {
                root {{ project_dir }};
        }
        location /media {
                root {{ project_dir }};
        }
        location /robots.txt {
                root {{ project_dir }};
        }
}

The attentive reader noticed that with the user directive we created a new user of our system. Let us launch our project on his behalf.

Create another playbook named user-playbook.yml with the following contents:

---
- hosts: user-hosts
  sudo: false
  roles:
    - django
- hosts: root-hosts
  sudo: true
  tasks:
    - name: restart site in supervisor
      supervisorctl: name={{ project_url }} state=restarted
    - name: restart mysql
      service: name=mysql state=restarted enabled=yes
    - name: restart nginx
      service: name=nginx state=restarted enabled=yes

And inside we see that at first a certain role is played by django, and then again, using the superuser rights, the task of restarting the daemons is performed. Let's figure out what we need to deploy the django project:

---
- name: create project directory
  file: path={{ project_dir }} state=directory
- name: create logs directory
  file: path={{ project_dir }}/logs state=directory
- name: create project home directory
  file: path={{ project_homedir }} state=directory
# разархивируем архив, предварительно собранный на локальной машинке
- name: unarchive project archive
  unarchive: src=/tmp/django_deploy.tar dest={{ project_homedir }}
- name: create virtualenv
  pip: virtualenv={{ env }} virtualenv_site_packages=yes {% if requirements %}requirements={{ project_homedir }}/{{ requirements }}{% endif %}
# листинг uwsgi.j2 приводить не буду, дабы не растягивать статью. Файл есть в репозитории
- name: copy uwsg file
  template: src=uwsgi.j2 dest={{ project_homedir }}/uwsgi.{{ project_slug }}.ini
# аналогично
- name: copy local_settings.py
  template: src=local_settings.py dest={{ project_homedir }}/{{ project_slug }}/{{ local_settings }}
- name: syncdb (for django<1.7)
  django_manage: command=syncdb virtualenv={{ env }} app_path={{ project_homedir }}
- name: migrate database
  django_manage: command=migrate virtualenv={{ env }} app_path={{ project_homedir }}
- name: collectstatic
  django_manage: command=collectstatic virtualenv={{ env }} app_path={{ project_homedir }}
- name: create media directory
  file: path={{ project_dir }}/media state=directory
# я в своих проектах юзаю django-tinymce
- name: create `uploads` directory
  file: path={{ project_dir }}/media/uploads state=directory


That, in fact, is all. We installed the necessary software on a clean system, created a new user, deployed a django project on his behalf, and restarted all the servers.

All this happiness starts like this:

# если деплоимся первый раз (система еще не настраивалась) или в систему необходимо внести изменения
ansible-playbook -i hosts root-playbook.yml
# создаем архив с текущим состоянием проекта
tar -cf /tmp/django-deploy.tar *
# запустить разворачивалку проекта и перезапуск демонов
ansible-playbook -i hosts user-playbook.yml


A working deployment project on ubuntu server 14.04 is in the repository .

Thank you for your time, I hope it was useful.

Also popular now: