Пользователь
0,0
рейтинг
27 октября 2014 в 11:28

Разработка → Deploy Django приложений с использованием Ansible для чайников из песочницы

Доброго времени суток!

Совсем недавно мой коллега познакомил меня с замечательным инструментом автоматизации ручного труда под названием Ansbile. После чего моментально родилась идея написать что-то своё, что упрощает тот самый ручной труд. Что чаще всего приходится делать руками? Правильно, деплоиться.

В этой статье я расскажу о том, как с использованием ansible раскатать django-проект на чистом удаленном сервере ubuntu 14.04, создав при этом для проекта отдельного пользователя.

Что из себя представляет ansible? Набор команд, написаных на python, которые позволяют автоматически выполнять заданные действия на удаленных машинах. Замечательно! Давайте построим план действий, как-бы мы это делали ручками?
  • Часть 1 (и использованием прав суперпользователя)
    • Установить на чистую виртуальную машинку софт: mariadb, nginx, supervisor, python-mysqldb, python-virtualenv, python-pip, supervisor, uwsgi;
    • Сконфигурировать nginx config;
    • Сконфигурировать supervisor config;
    • Создать нового пользователя системы специально под этот проект (само собой опционально);
  • Часть 2 (от имени пользователя, хозяина проекта)
    • Скопировать проект;
    • Создать виртуальное окружение (с необходимыми пакетами внутри);
    • Сконфигурировать local_settings.py (в ней мы храним настройки доступа к БД, а так же пути STATIC_ROOT and MEDIA_ROOT);
    • Создать структуру таблиц в базе данных (syncdb для django<1.7, migrate);
    • Собрать всю статику в одном месте;
  • Часть 3 (опять переключаемся в права суперпользователя)
    • Перезапускаем mysql;
    • Перезапускаем nginx;
    • Перезапускаем supervisor;


Без лишних слов переходим к делу.

С чего начинается ansible — с файла hosts. В нем мы указываем все доступные нам машинки, над которыми будут производиться действия. В нашем случае машинка одна (о том как производить действия над несколькими машинками я рассказывать не буду, цель статьи не в этом) и файл выгляди следующим образом:

[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

Давайте разберем.
В секции [project-hosts] перечислены все имеющиеся в нашем распоряжении машинки, с указанием реквизитов доступа к ним. В нашем случае машинка одна, но указана два раза: первая содержит в себе root реквизиты, от имени которых будет производиться настройка системы, вторая — реквизиты еще не созданного пользователя, хозяина нашего django приложения. Первый параметр — алиас этой машинки.

Секции [user-hosts] и [root-hosts] объединяют машинки в группы по общему назначнию (надеюсь тут понятно. Рутовые в одной группе, не-рутовые — в другой).

Итак, с хостами разобрались. Теперь нужно как-то сказать ansible`у что делать.

Но для начала расскажу о переменных. Совершенно ясно, что, например, название проекта используется многократно: в конфиге nginx, supervisor, пути к проекту и т.д. У Ansible множество способов указывать переменные, но мне больше нравится следующий: в директории grpou_vars создаем файл с названием секции из файла hosts. Например, я не стал заморачиваться и создал файл с названием project-hosts. Теперь переменные, объявленные в этом файле будут доступны всем машинкам, входящим в секцию [project-hosts], т.е. глобально по нашему проекту.

Вот всё, что понадобилось мне для этого проекта (ansible использует синтаксис jinja2):

#
# 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'

Давайте сначала разберемся с настройкой системы. Создаем файл root-playbook.yml со следующим содержанием:
---
- hosts: root-hosts
  sudo: true
  roles:
    - system

Тут поясню две вещи:

1. hosts: root-hosts — директива, говорящая нам о том, для машинок какой группы выполнять следующие действия;
2. roles: system — список директорий с дальнейшими сценариями действий.

Давайте уже заглянем в директорию со сценариями. Она имеет следующий вид:

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

Здесь:
handlers — хранит описание о обработчиках. Например, содержит описание того как перезапустить nginx;
tasks — всему голова. Список заданий для Ansible;
templates — шаблоны файлов, необходимых нам для некоторых демонов;

Идем по порядку.

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

Yml синтаксис Ansible`а понятен: имя директивы, сама директива, действие с параметрами. Эти обработчики будут использованы в наших задачах, аля задача завершилась — будь добр запустить нужный хэндлер (секция notify)
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

Разберем построчно одну секцию:

— name: updating the system — имя, отображаемое в процессе деплоя
— apt: update_cache=yes cache_valid_time=86400: apt — имя директивы ansible (я называю их директивами). update_cache, cache_valid_time — параметры директивы;
— notify: — restart server — действие из handlers, которое необходимо сделать по заврешению задачи.

Собственно, синтаксис предельно прост. Если какие-то параметры не ясны — можно почитать в документации к Ansible. Но хотелось бы обратить внимание на директиву template. Она принимает два параметра: src — имя исходного файла, хранящегося в директории templates текущей роли и dest — куда этот файл нужно положить, предварительно отрендерив, используя все доступные переменные.

Например мой файл nginx.j2 template имеет следующий вид:

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 }};
        }
}

Внимательный читатель заметил, что директивой user мы создали нового пользователя нашей системы. Давайте от его имени развернем наш проект.

Создаем еще один playbook с именем user-playbook.yml и следующим содержанием:

---
- 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

И внутри мы видим, что сначала выполняется некая роль django, а потом опять используя права суперпользователя выполняются таски перезапуска демонов. Давайте разберемся с тем, что нам нужно для развертывания проекта django:

---
- 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


Вот, собственно, и всё. На чистую систему мы установили необходимый софт, создали нового юзера, от его имени развернули django проект и перезапустили все сервера.

Всё это счастье запускается так:

# если деплоимся первый раз (система еще не настраивалась) или в систему необходимо внести изменения
ansible-playbook -i hosts root-playbook.yml

# создаем архив с текущим состоянием проекта
tar -cf /tmp/django-deploy.tar *

# запустить разворачивалку проекта и перезапуск демонов
ansible-playbook -i hosts user-playbook.yml


Работающий проект по разворачиванию на ubuntu server 14.04 находится в репозитории.

Спасибо за уделенное мне время, надеюсь был полезен.
@freylis
карма
4,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (8)

  • 0
    ansible_ssh_pass=z ansible_sudo_pass=z
    Пароль так лучше не указывать. Делать авторизацию либо через ключ, либо через параметор -K -k
  • 0
    Не холивара ради — обучения для: скажите, зачем ставить uwsgi И supervisor? uWsgi может следить за своими процессами, позволяет перегружать их по touch, у него есть alarm system для оповещений, nagios для мониторинга и т.д. Зачем этой навороченной козе supervisor?
    • 0
      Возможно для единства, то есть в supervisord можно засунуть и другие процессы\демоны и их, всех, мониторить\запускать\останавливать через единый интерфейс?
      • 0
        У автора только uWSGI запускается через него в данном примере. Ставить лишние сервисы без особой нужды не стоит — новички ведь просто копипастят конфиг особо не вдумываясь, что и зачем.
  • 0
    Чем это лучше django-fab-deploy? Это не для холивора. Действительно интересно, есть ли преимущества, оправдывающие смену инструмента. Ответ «ничем» вполне приемлем.
    • 0
      В рамках этой задачи думаю очевидных плюсов нет
  • 0
    Первый параметр — алиас
    и вы выбрали для него root и user
    тем самым сломали мозг и естественное положение вещей в этом мире.
    юзеров перемешали с хостами.
    после прочтения статьи новички не смогут отличить, где груши у них, а где яблоки.
    • 0
      [хосты-груши]
      host-1
      host-2

      [хосты-яблоки]
      host-2
      host-3

      вполне отделимы между собой. В рамках моего решения группы поделены не по типам совершаемых действий, а по правам пользователей, совершающих эти действия. В этом случае не вижу проблем в именовании групп по правам

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.