0

Ansible: развёртывание Django-стека на 3 VM

25.02.2024

Это четвертая и завершающая статья из цикла: настройка и развертывание Django-стека на серверах. До этого мы освоили основы Terraform, научились управлять облачным ЦОДом через него и подробно рассмотрели работу Django-стека на одном сервере.

В этой статье — освежим знания по Ansible, познакомимся с ролями и развернем на нашей подготовленной заранее инфраструктуре Django-стек. Код проекта есть на github, там же краткий мануал в виде удобного readme-файла.

Важное из предыдущих шагов

Важное из предыдущих шагов

Обозначим буквально несколько моментов из предыдущих шагов (статей), которые важны для нас сейчас:

  1. Мы создали ВЦОД и получили IP адрес виртуального роутера — 89.22.173.56. Его мы будем указывать для подключения Ansible в инвентаризационном файле.
  2. Перед созданием ВМ мы развернули маршрутизируемую сеть — 10.10.0.1/24, чтобы ВМ могли общаться между собой и, чтобы у Ansible был доступ к ним по SSH.
  3. На виртуальном роутере мы настроили 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.
    Каждой VM был назначен свой внешний SSH-порт.

На локальной машине (с той, которой запускается Ansible) мы установили Go, Terraform, провайдер VDC и Ansible. Установили программу sshpass (sudo apt install sshpass). Теперь мы готовы к развертыванию Django-стека на серверах. Делать мы это будем с помощью Ansible-ролей.

Что такое 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 на практике: управляем vDC

Terraform на практике

Управляем VMware Cloud Director по API с помощью Terraform.

Основы Terraform

Основы Terraform

Изучаем основы Terraform для развертывания инфраструктуры в облаке.

Введение в IaC и знакомство с Ansible

Работа с Ansible

Разворачиваем LEMP стек на VPS под управлением Ubuntu 18.

Свежие комментарии

Подписка

Лучшие статьи


Fatal error: Uncaught Error: Call to a member function have_posts() on null in /home/host1867038/the-devops.ru/htdocs/www/wp-content/themes/fox/inc/blog.php:380 Stack trace: #0 /home/host1867038/the-devops.ru/htdocs/www/wp-content/themes/fox/widgets/latest-posts/widget.php(257): fox56_blog_grid(NULL, Array) #1 /home/host1867038/the-devops.ru/htdocs/www/wp-content/themes/fox/widgets/latest-posts/register.php(33): include('/home/host18670...') #2 /home/host1867038/the-devops.ru/htdocs/www/wp-includes/class-wp-widget.php(394): Wi_Widget_Latest_Posts->widget(Array, Array) #3 /home/host1867038/the-devops.ru/htdocs/www/wp-includes/widgets.php(837): WP_Widget->display_callback(Array, Array) #4 /home/host1867038/the-devops.ru/htdocs/www/wp-content/themes/fox/inc/single.php(417): dynamic_sidebar('sidebar') #5 /home/host1867038/the-devops.ru/htdocs/www/wp-content/themes/fox/inc/single.php(136): fox56_single_sidebar() #6 /home/host1867038/the-devops.ru/htdocs/www/wp-content/themes/fox/inc/single.php(7): fox56_single_inner() #7 /home/host1867038/the-devops.ru/htdocs/www/wp-content/themes/fox/single.php(23): fox56_single() #8 /home/host1867038/the-devops.ru/htdocs/www/wp-includes/template-loader.php(106): include('/home/host18670...') #9 /home/host1867038/the-devops.ru/htdocs/www/wp-blog-header.php(19): require_once('/home/host18670...') #10 /home/host1867038/the-devops.ru/htdocs/www/index.php(17): require('/home/host18670...') #11 {main} thrown in /home/host1867038/the-devops.ru/htdocs/www/wp-content/themes/fox/inc/blog.php on line 380