Это четвертая и завершающая статья из цикла: настройка и развертывание Django-стека на серверах. До этого мы освоили основы Terraform, научились управлять облачным ЦОДом через него и подробно рассмотрели работу Django-стека на одном сервере.
В этой статье — освежим знания по Ansible, познакомимся с ролями и развернем на нашей подготовленной заранее инфраструктуре Django-стек. Код проекта есть на github, там же краткий мануал в виде удобного readme-файла.
Важное из предыдущих шагов
Обозначим буквально несколько моментов из предыдущих шагов (статей), которые важны для нас сейчас:
- Мы создали ВЦОД и получили IP адрес виртуального роутера — 89.22.173.56. Его мы будем указывать для подключения Ansible в инвентаризационном файле.
- Перед созданием ВМ мы развернули маршрутизируемую сеть — 10.10.0.1/24, чтобы ВМ могли общаться между собой и, чтобы у Ansible был доступ к ним по SSH.
- На виртуальном роутере мы настроили S-NAT(исходящий трафик) и D-NAT(входящий трафик) правила. Получились следующие соотношение портов:
- 89.22.173.56:22 → 10.10.0.3:22 — Nginx;
- 89.22.173.56:80 → 10.10.0.3:80 — Nginx;
- 89.22.173.56:23 → 10.10.0.4:22 — Django;
- 89.22.173.56:24 → 10.10.0.5:22 — PostgreSQL.
На локальной машине (с той, которой запускается Ansible) мы установили Go, Terraform, провайдер VDC и Ansible. Установили программу sshpass (sudo apt install sshpass). Теперь мы готовы к развертыванию Django-стека на серверах. Делать мы это будем с помощью Ansible-ролей.
Что такое Ansible роли?
Роль в Ansible — это независимая сущность, решающая какой-то свой набор задач. Говоря проще, роль — это директория с поддиректориями и файлами, где расположены задачи.
Обычно роли независимы от проектов и хранятся удаленно, если их зависимость не продиктована логикой проекта. Роли имеют строго заданную структуру и создаются командой ansible-galaxy init [название роли].
Структура Ansible-ролей:
├── defaults — директория содержит файлы с переменными по умолчанию
│ └── main.yml
├── files — директория с файлами, которые будут скопированы на удалённый сервер(а)
├── handlers — директория содержит хендлеры*
│ └── main.yml
├── meta — директория содержит вспомогательную информацию
│ └── main.yml
├── README.md
├── tasks — директория содержит набор задач, которые должна выполнять роль
│ └── main.yml
├── templates — директория содержит шаблоны Jinja2
├── tests — директория содержит тесты**
│ ├── inventory
│ └── test.yml
└── vars — директория содержит пользовательские переменные
└── main.yml
* хендлеры — это специальные задачи, которые выполняются после выполнения основных задач (тасков).
** Тесты запускаются после выполнения плейбука.
В структуре ролей файл main.yaml встречается множество раз в разных директориях. Это сделано специально. Именно в него необходимо вносить изменения, так как по умолчанию Ansible сначала проверяет main.yaml, а затем смотрит в другие файлы.
Для нашего проекта мы создадим 3 роли: frontend, backend и postgresql. Задачи для каждой роли помещаются в tasks/main.yaml. Шаблоны файлов, которые будут скопированы на сервер с подменой данных размещаются в templates, а вот переменные у всех 3 ролей общие, поэтому мы вынесли их в отдельный файл, находящийся в верхней директории уровня проекта — group_vars/all.yaml.
Из group_vars/all.yaml данные также подтягиваются в инвентаризационный файл и мастер-playbook — side.yaml. Рассмотрим ближе структуру проекта и логику его работы.
Структура проекта и логика работы
Начнем со структуры проекта. Для Ansible очень важна строгая структура директорий, поэтому роли должны располагаться в отдельной поддиректории проекта. Название поддиректории должно быть лаконичным и понятным.
Вот структура проекта, которая получилась у нас:
.
├── Ansible_roles #Директория Ansible ролей
│ ├── backend #Директория backend Ansible роли
│ │ ├── defaults
│ │ ├── handlers
│ │ ├── meta
│ │ ├── README.md
│ │ ├── tasks
│ │ ├── templates
│ │ ├── tests
│ │ └── vars
│ ├── frontend #Директория frontend Ansible роли
│ │ ├── defaults
│ │ ├── files
│ │ ├── handlers
│ │ ├── meta
│ │ ├── README.md
│ │ ├── tasks
│ │ ├── templates
│ │ ├── tests
│ │ └── vars
│ └── postresql #Директория postresql Ansible роли
│ ├── defaults
│ ├── handlers
│ ├── meta
│ ├── README.md
│ ├── tasks
│ ├── tests
│ └── vars
├── group_vars #Общие переменные
│ └── all.yaml
├── hosts.ini #Инвентаризационный файл
├── main.tf
├── pars_state.py
├── README.md
├── settings.tf
├── side.retry
├── side.yaml #Мастер-playbook
├── terraform-provider-vcd
├── terraform.tfstate
└── terraform.tfstate.backup
Роли содержат задачи, которые Ansible будет выполнять на удаленных серверах, а hosts.ini и side.yaml содержат данные для подключения.
hosts.ini — инвентаризационный файл, в нём указываются IP-адреса серверов и данные для подключения. Наш hosts.ini внутри выглядит так:
[all]
frontend_server ansible_port=22 ansible_host={{vdc_ip}} ansible_username={{user}} ansible_password={{passwd}} ansible_python_interpreter={{interp}}
backend_server ansible_port=23 ansible_host={{vdc_ip}} ansible_username={{user}} ansible_password={{passwd}} ansible_python_interpreter={{interp}}
postgresql ansible_port=24 ansible_host={{vdc_ip}} ansible_username={{user}} ansible_password={{passwd}} ansible_python_interpreter={{interp}}
В квадратных скобках указывается название секции с серверами — [all]. Далее следует название сервера — frontend_server, затем идёт набор параметров для подключения в формате «ключ=значение». Параметры отделяются друг от друга пробелом, а «ключ=значение» — нет.
Конструкция {{какое-то название}} — это ссылка на переменную. В нашем случае переменные находятся в group_vars/all.yaml. Название директории и файла не случайны, они фиксированы, если бы у нас группа в инвентаризационном файле называлась — [back], то название файла в group_vars соответствовало бы названию группы.
side.yaml — Мастер-playbook, который соотносит роли и целевые серверы, на которых будут выполняться задачи ролей. Вот как выглядит наш мастер-playbook:
- name: Установка Nginx
hosts: frontend_server
roles:
- frontend
- name: Установка PostgreSQL
hosts: postgresql
roles:
- postresql
- name: Установка Django и Gunicorn
hosts: backend_server
roles:
- backend
Тут всё просто! Name — название задачи, hosts — указывает на сервер, на котором будет исполняться Ansible-роль, roles — роли, которые будут выполняться, на указанных в hosts серверах.
Ролей и серверов в одной задаче может быть огромное количество, так же как и задач. Важно! Задачи выполняются последовательно, поэтому если вы пишете роли, которые зависят от последовательности — располагайте их в правильной последовательности.
Например, второй задачей у нас идёт установка PostgreSQL, потому что Django в завершении установки будет делать миграцию БД и если PSQL не будет установлен и настроен — миграция не пройдёт.
Со структурой, ролями, инвентаризационным файлом и мастер-playbook разобрались, осталось основное — задачи в ролях. Вот они:
Задачи frontend
#Преднастройка
- name: Преднастройка пакетного менеджера aptitude
apt: name=aptitude update_cache=yes state=latest force_apt_get=yes
# Установка Nginx
- name: Установка последней версии Nginx
apt: name=nginx state=latest
- name: Запуск Nginx
service:
name: nginx
state: started
# Настройка Nginx
- name: Скопировать и переименовать Nginx конфиг
template:
src: "django.j2"
dest: "/etc/nginx/sites-available/django"
- name: Создать симлинк
file:
src: "/etc/nginx/sites-available/django"
dest: "/etc/nginx/sites-enabled/django"
state: link
notify: Reload Nginx
- name: Удалить дефолтный сайт
file:
path: "/etc/nginx/sites-enabled/default"
state: absent
notify: Reload Nginx
Задачи backend
# Установка Django
- name: Установка необходимых пакетов для Django
apt:
name: "{{ item }}"
update_cache: yes
state: latest
force_apt_get: yes
loop: ["zlib1g-dev", "libbz2-dev", "libreadline-dev", "llvm", "libncurses5-dev",
"libncursesw5-dev", "xz-utils", "tk-dev", "liblzma-dev", "python3-dev", "python-pil",
"python3-lxml", "libxslt-dev", "libffi-dev", "libssl-dev", "python-dev", "gnumeric",
"libsqlite3-dev", "libpq-dev", "libxml2-dev", "libxslt1-dev", "libjpeg-dev", "libfreetype6-dev",
"libcurl4-openssl-dev", "python-libxml2", "python3-venv", "imagemagick", "graphicsmagick",
"imagemagick-6.q16hdri", "python3-virtualenv","virtualenv"]
- name: Создание рабочей директории проекта
file: path={{project_name_path}} state=directory
- name: Установка Django
pip:
name: django
virtualenv: "{{project_name_path}}/env"
virtualenv_command: virtualenv
virtualenv_python: python3
- name: Установка psycopg2
pip:
name: psycopg2
virtualenv: "{{project_name_path}}/env"
virtualenv_command: virtualenv
virtualenv_python: python3
- name: Развёртывание демо-проекта
command: "{{ project_name_path }}/env/bin/django-admin.py startproject {{ project_name }} chdir={{ project_name_path }}/"
- name: Создание первого Web-приложения
django_manage:
command: startapp {{web_app}}
virtualenv: "{{ project_name_path }}/env"
app_path: "{{ project_name_path }}/{{ project_name }}"
- name: Копирование Django-конфига
template:
src: "settings.j2"
dest: "{{ project_name_path }}/{{ project_name }}/{{ project_name }}/settings.py"
- name: Make migrations
shell: "{{project_name_path}}/env/bin/python3 {{ project_name_path }}/{{ project_name }}/manage.py makemigrations"
- name: Migrate database
django_manage: app_path={{ project_name_path }}/{{ project_name }}
command=migrate
virtualenv={{project_name_path}}/env
# Установка Gunicorn
- name: Установка Gunicorn
pip:
name: gunicorn
virtualenv: "{{project_name_path}}/env"
virtualenv_command: virtualenv
virtualenv_python: python3
- name: Копирование Gunicorn-конфига
template:
src: "gunicorn_settings.j2"
dest: "{{ project_name_path }}/{{ project_name }}/{{ project_name }}/gunicorn_config.py"
- name: Копирование скрипта запуска Gunicorn
template:
src: "gunicorn.j2"
dest: "{{ project_name_path }}/{{ project_name }}/gunicorn.sh"
mode: a+x
- name: Создание пользователя www
user:
name: www
- name: Копирование Gunicorn юнита
template:
src: "gunicorn.serviсe.j2"
dest: "/etc/systemd/system/gunicorn.service"
mode: 0775
- name: Обновление юнитов
shell: "systemctl daemon-reload"
- name: Запуск Gunicorn при старте или перезагрузке
shell: "systemctl enable gunicorn"
- name: Запуск Gunicorn
shell: "systemctl start gunicorn"
Задачи PSQL
#Установка PostgreSQL
- name: Установка PSQL
apt:
pkg:
- postgresql
- postgresql-contrib
- postgresql-server-dev-all
- python3-psycopg2
- python-psycopg2
state: latest
- name: Запуск PSQL
service: name=postgresql enabled=yes state=started
- name: Создание БД
postgresql_db: name={{ bd_name }}
become_user: postgres
become: yes
- name: Создание пользователя для подключения Django
postgresql_user: db={{ bd_name }}
name={{ bd_user }}
password={{ bd_passwd }}
encrypted=yes
priv=ALL
state=present
role_attr_flags=NOSUPERUSER,NOCREATEDB
become: yes
become_user: postgres
#Настройка PostgreSQL
- name: Открываем все порты для прослушивания
lineinfile: dest=/etc/postgresql/10/main/postgresql.conf
regexp='^#listen_addresses'
insertbefore=BOF
line='listen_addresses = '*''
- name: Добавляем IP-адрес Django в разрешённые
lineinfile: dest=/etc/postgresql/10/main/pg_hba.conf
regexp='^'
line='host all all 0.0.0.0/0 md5'
state=present
- name: Перезапуск PSQL
systemd:
name: postgresql
state: restarted
Тут мы тоже использовали ссылки на переменные — {{ название переменной }}. Так же как и в шаблонах файлов, расположенных в директории templates каждой роли.
Используемые в проекте переменные выглядят так:
#Параметры подключения
vdc_ip: 89.22.173.56
user: "root"
passwd: "Pseudokatkov1"
interp: "/usr/bin/python3"
#Параметры Nginx
gunicorn_ip: 10.10.0.4:8001
domain: my_test_site.ru
#Параметры Django, Gunicorn и PSQL
project_name_path: /var/www/django_test
project_name: test_project
web_app: first
bd_name: dtb_db
bd_user: dc
bd_passwd: Django_Connecter
bd_host: 10.10.0.5
db_host_port: 5432
Они удобно сгруппированы по условным блокам и позволяют создать единую точку входа в проект. Единственное, что вам нужно поменять для воспроизведения ролей в виртуальном ЦОДе — параметры подключения.
Итог
В итоге мы имеем проект, который подключается к Cloud Director по API с помощью провайдера VCD, разворачивает 3 ВМ, связывает их маршрутизируемой сетью, настраивает S-NAT и D-NAT правила для трафика и устанавливает на серверы Django-стек с помощью Ansible.
Графически схема работы проекта выглядит так:
Кликнете на схему — она откроется в новой вкладке, и вы сможете детально её рассмотреть.
Преимущество использования связки Terraform + Ansible в том, что весь инфраструктурный код находится в одном месте. Из недостатков можно отметить отсутствие прямой интеграции ансибла в террафом. Вы можете лично убедиться в удобстве данного подхода с нашим проектом.
Достаточно скачать проект с github и развернуть его, на каком-нибудь удаленном сервере, например, VPS/VDS от 1cloud. В readme проекта есть краткая инструкция.
Если вы интересуетесь темой IaC и, в частности, Terraform или Ansible — вам могут быть полезны и интересны наши статьи:
Terraform на практике
Управляем VMware Cloud Director по API с помощью Terraform.
Основы Terraform
Изучаем основы Terraform для развертывания инфраструктуры в облаке.
Работа с Ansible
Разворачиваем LEMP стек на VPS под управлением Ubuntu 18.