С расширением инфраструктуры перед компанией встает задача масштабирования. Если ваши веб-приложения обслуживаются Django, то эта статья для вас. В ней мы рассмотрим эффективный способ масштабирования и защиты приложений на этом фреймворке, а для удобства оформим материал в виде пошагового руководства. Добавим, что руководство предназначено для серверов разработки Django на Ubuntu. Данные решения были протестированы на Ubuntu 20.04, но теоретически могут работать и на других версиях.
Уровень отказов понижается, а пропускная способность системы, наоборот, увеличивается при использовании горизонтального масштабирования приложения Django. Для этого предоставляются дополнительные серверы, на которых запускается само приложение и интерфейс WSGI. Для распределения входящих запросов применяют обратный прокси. Этот тип прокси обрабатывает и перераспределяет клиентские запросы на сервер. В дополнение к балансировке рабочих нагрузок между серверами обратные прокси-серверы могут предоставлять услуги, не всегда предлагаемые серверами приложений, такие как кэширование, сжатие и шифрование SSL, а также прерывание TLS. Одним из вариантов такого прокси является Nginx.
С помощью Nginx развертываются SSL-сертификаты, обеспечивающие безопасное подключение через HTTPS. Также Nginx обеспечивает кэширование статического контента, чтобы минимизировать нагрузку на сервер. Конфигурирование каждого из этих компонентов в отдельности и обеспечение их связности может оказаться сложной задачей. Поэтому для запуска приложений Django задействуем Docker. Это упростит процесс настройки и обеспечит одинаковое поведение компонентов, причем безотносительно среды их развертывания.
Сначала подготовим серверы приложений с установленным Docker, а потом защитим наше приложение с помощью SSL-сертификата Let's Encrypt: для этого понадобится дополнительный сервер с обратным прокси Nginx и контейнером Certbot. Certbot — это пакет, который помогает управлять SSL-сертификатами центра сертификации Let’s Encrypt. Он извлекает сертификат, настраивает блоки сервера Nginx с указанием местоположения сертификата и управляет его автоматическим продлением.
С поддержкой обновления SSL-сертификата у вашего сайта всегда будет повышенный уровень безопасности. Кроме того, прокси будет находиться перед серверами с распределенной архитектурой и обрабатывать весь входящий внешний трафик, а затем распределять его на ваши серверы приложений. При этом серверы приложений находятся за брандмауэром, позволяя получать к ним доступ только прокси-серверу.
Для корректной настройки нам понадобится следующее:
Начнем с входа на наш первый сервер приложений и выполним команду git
, чтобы клонировать ветку django-testdb-docker
репозитория django-
testdb
. Затем перейдем в каталог django-testdb
при помощи команды cd
. В этом каталоге найдите Dockerfile
, используемый для создания образа приложения, каталог с кодом приложения Python, и файл env
с переменными, которые будут переданы в контейнер при запуске для изменения его поведения.
В Dockerfile
определите зависимости пакетов Django через текстовый файл requirements.txt
. Кроме того, нужно объявить порт 8000 для приема входящего трафика, и настроить его для запуска сервера Gunicorn. Можно собрать образ Docker по команде:
docker build -t django testdb:v1
После того как Docker создаст образ, выведем список доступных образов на сервере с помощью команды docker images
.
Далее нам нужно изменить файл env
, используемый при настройке среды выполнения. Он передается в контейнер Docker при запуске. Откройте файл env
редактором nano
. В файле env
уже есть шаблонный текст, который нужно изменить и проставить там актуальные значения:
DJANGO_SECRET_KEY
— генерирует рандомное значение (есть в документации Django). Поэтому используйте команду для генерации случайной строки и помещения ее в переменную.DJANGO_ALLOWED_HOSTS
— это значение используется для защиты нашего приложения от атак хоста. Можно установить там значение *
, соответствующее всем хостам (например, при работе в режиме разработки). Но когда вы разворачиваете свое приложение в рабочей среде, установите в качестве значения имя вашего домена. Для нашего гайда это test_domain.com
.DB_DATABASE
— здесь укажите имя БД PostgreSQL, возьмем за пример testdb
.DB_USERNAME
— укажите имя пользователя, которое вы выбрали для своей БД.DB_PASSWORD
— установите пароль, который вы выбрали для своей базы данных.DB_HOST
— укажите хост, на котором запущен экземпляр вашей базы данных.DB_PORT
— укажите порт вашей базы данных.После того как закончите редактирование, сохраните и закройте файл. С этими учетными данными мы можем создать схему базы данных, запустив контейнер и переопределив набор команд CMD
в Dockerfile
. Дополнительно о точке входа Dockerfile
можно почитать в официальной документации. Далее выполните следующую команду:
docker run --env-file env django-testdb:v1 sh -c "python manage.py makemigrations && python manage.py migrate"
Так запускается образ django-testdb:v1
и передается измененный ранее файл env
. Код, начинающийся с sh
, создает схему БД. Если вы выполняете команду в первый раз, то увидите вывод, указывающий на создание схемы базы данных.
После создания схемы создаем суперпользователя Django. Выполните следующую команду, чтобы запустить контейнер с интерактивной оболочкой:
docker run -i -t --env-file env django-testdb:v1 sh
Команда запускает контейнер для взаимодействия с оболочкой Python. Теперь создаем суперпользователя с помощью следующей команды:
python manage.py createsuperuser
Далее введите имя пользователя, адрес электронной почты и пароль, повторите его и нажмите Enter, чтобы создать пользователя.
Выйдите из оболочки и уничтожьте контейнер, нажав CTRL+D. Затем нужно снова запустить контейнер, переопределив команду по умолчанию инструкцией collectstatic
. Команда сгенерирует статический контент и загрузит его в облачное хранилище MinIO. В результате файл после создания загружается в настроенную службу хранилища объектов. Теперь вы можете запустить приложение без указания какой-либо дополнительной инструкции для переопределения команды CMD
, определенной в Dockerfile
по умолчанию.
Посмотрите интерфейс приложения в своем браузере, обратившись к IP-адресу первого сервера в адресной строке: http://FIRST_SERVER_IP
. Разумеется, высветится ошибка 404. Страница не найдена, потому что мы ничего не определили для пути /
. Перейдите по адресу
http://FIRST_SERVER_IP/testdb
, а затем в панель администратора: http://FIRST_SERVER_IP/admin
и введите установленные учетные данные, чтобы получить доступ к панели. Убедившись, что контейнер настроен должным образом, вы можете закрыть его, нажав CTRL+C в терминале.
Далее нужно сделать, чтобы контейнер работал автономно. Это необходимо сделать, чтобы мы могли выйти из сеанса SSH первого сервера. Это оставит контейнер в режиме detached
. Для этого выполните команду с флагом -d
, чтобы он мог продолжать работать в режиме detached
. Также желательно дать контейнеру имя testdb
(будет совпадать с именем нашей БД), чтобы видеть его при перечислении контейнеров.
Выйдите из сеанса SSH вашего первого сервера и перейдите по адресу http://FIRST_SERVER_IP/testdb
в браузере, чтобы убедиться, что сервер работает должным образом. Если вы можете просматривать интерфейс testdb
, значит первый сервер приложений настроен успешно. Теперь давайте настроим масштабируемое приложение на втором.
Шаг 3. Настройка второго сервера приложений Django
Перед началом этого этапа у вас должен быть запущен второй сервер, добавлен пользователь sudo
и установлен Docker. Следующим шагом является настройка для подключения к серверу, на котором размещена PostgreSQL. Для этого разрешите IP-адрес второго сервера через конфигурации UFW и PostgreSQL.
Сначала войдите в базу данных PostgreSQL под своим пользователем sudo
без права root-доступа. Чтобы добавить правило UFW, выполните следующую команду:
sudo ufw allow from SECOND_SERVER_IP to any port 5432
Затем выполните эту команду и добавьте IP-адрес второго сервера в файл аутентификации клиента PostgreSQL:
sudo nano /etc/postgresql/12/main/pg_hba.conf
Теперь добавьте эту строку в раздел hosts
, указав свой IP-адрес:
host all all SECOND_SERVER_IP/24 md5
После окончания редактирования сохраните файл, далее перезапустите службу PostgreSQL, чтобы изменения вступили в силу:
sudo service postgresql restart
Выйдите из базы данных PostgreSQL и продолжите настройку. Войдите на второй сервер, используя SSH, а затем клонируйте django-testdb-branch
из репозитория django-testdb
с помощью:
git clone --single-branch --branch django-testdb-docker --depth 1
Зайдите в директорию django-testdb
и создайте образ:
docker build -t django-testdb:v1
Далее измените файл env
с нужными значениями конфигурации (см. предыдущий шаг). Не забудьте изменить переменную DJANGO_ALLOWED_HOSTS
.
Теперь обновите свои учетные данные MinIO в файле env
и запустите контейнер в автономном режиме с помощью следующей команды:
docker run -d --rm --name testdb --env-file env -p 80:8000 django-testdb:v1
Команда запускает контейнер и поддерживает его работу в автономном режиме. Выйдите из SSH второго сервера и перейдите на http://SECOND_SERVER_IP/testdb
в браузере, чтобы удостовериться, что всё работает должным образом. Теперь у нас есть два сервера, на которых запущена одна и та же копия масштабируемого приложения Django.
Существует несколько способов реализовать обратный прокси и балансировку нагрузки Nginx. Один из них — настроить обратный прокси-сервер Nginx отдельно от внутреннего сервера приложений, как мы сделали в этом руководстве. Эта настройка является гибкой и позволяет масштабировать как уровень прокси-сервера Nginx, так и уровень приложения. Например, есть возможность добавить несколько прокси Nginx или внедрить облачное решение для балансировки.
Другой способ реализации обратного прокси — использование одного из внутренних серверов приложений в качестве прокси-сервера Nginx. Затем можно проксировать входящие запросы локально и на другие серверы приложений. При желании контейнер Nginx довольно легко настраивается на всех внутренних серверах приложений и при этом устанавливается баланс нагрузки облачных мощностей, чтобы получать входящий трафик и распределять его на внутренние серверы приложений.
Итак, зайдите на четвертый сервер Ubuntu, который вы настроили для использования в качестве прокси-сервера Nginx, и создайте каталог конфигурации командой mkdir conf
. Теперь откройте конфигурацию внутри каталога: nano conf/nginx.conf
и добавьте в файл следующее:
upstream django {
server FIRST_SERVER_IP;
server SECOND_SERVER_IP;
}
server {
listen 80 default_server;
return 444;
}
server {
listen 80;
listen [::]:80;
server_name test_domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name test_domain.com;
# SSL
ssl_certificate /etc/letsencrypt/live/test_domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/test_domain.com/privkey.pem;
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
client_max_body_size 4G;
keepalive_timeout 5;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://django;
}
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
}
}
В этом файле мы добавляем блоки server
, upstream
и location
, чтобы заставить Nginx перенаправлять HTTP-запросы на HTTPS и распределять запросы между двумя серверами приложений, которые мы настроили на предыдущих этапах. Обратите внимание, что некоторые значения и пути могут отличаться от представленных в этом образце. Дополнительную информацию о структуре файла конфигурации Nginx можно почерпнуть из официальной документации проекта, а мы переходим к следующему шагу.
Прежде всего у нас должна быть запись DNS A, указывающая на IP нашего прокси. Вы можете проверить это, запустив образ Certbot Docker и передав флаг --staging
. Это загрузит образ Certbot и запустит его в интерактивном режиме. Он сопоставляет порт 80 хоста с портом 80 внутри контейнера. Мы используем флаг -v для добавления в контейнер двух хост-директорий: /etc/letsencrypt/
и /var/lib/letsencrypt/
. Флаг --standalone
указывает, что мы хотим, чтобы образ Certbot запускался без использования Nginx. Наконец, у нас есть флаг --staging
, который заставит Certbot выполняться на промежуточных серверах и проверять ваше доменное имя.
Разумеется сертификат необходимо обновлять до истечения срока его действия. Certbot может автоматически обновлять сертификат в фоновом режиме, но вам может потребоваться предпринять определенные шаги, чтобы включить эту функцию. Для этого рекомендуем обратиться к официальной документации Certbot.
Прокси-сервер расшифровывает SSL-соединение и пересылает незашифрованные пакеты на внутренние серверы приложений. Поскольку мы будем защищать внутренние серверы от любого внешнего доступа, этот уровень безопасности должен работать в большинстве случаев. Однако если вы разворачиваете приложения, которые передают конфиденциальные данные, такие как банковская информация или данные о состоянии здоровья, вам следует использовать сквозное шифрование.
Итак, убедитесь, что все запросы проходят через прокси-сервер. У Docker случается проблема, когда он обходит конфигурации брандмауэра UFW и открывает внешние порты, что может сделать вашу инфраструктуру уязвимой. Это очевидно, поскольку мы настроили наши серверы приложений, не добавив порт 80 в правила UFW. Однако вы по-прежнему можете получить доступ к веб-страницам при посещении любого из общедоступных IP-адресов сервера в браузере. Один из способов решить эту проблему — напрямую использовать iptables, минуя UFW. Для этого советуем изучить официальную документацию Docker и iptables.
Другой рекомендуемый способ — использование облачных брандмауэров. Здесь можно изменить настройки UFW так, чтобы заблокировать внешний доступ ко всем портам, которые могли быть открыты Docker. Поскольку в процессе настройки мы непреднамеренно открыли порт 80 на хосте, то можем и отключить этот доступ, изменив конфигурацию UFW. О том, как это сделать, подробно описано в readme репозитория UFW-Docker.
В нашей инструкции мы продемонстрировали, как контейнеры Docker позволяют масштабировать приложения на основе Django. Инфраструктура включает в себя отдельный сервер базы данных PostgreSQL, два внутренних сервера приложений и прокси Nginx для балансировки нагрузки и распределения трафика между двумя серверами. Хотя мы создали наше приложение на основе Django, вы можете настроить эту архитектуру и с другими фреймворками: Node.js, Laravel и прочими.
Одно из улучшений, которые вы можете добавить, — это размещение вашего образа в репозитории (например, в Docker Hub), что позволяет легко распространять образ на несколько серверов. Вы также можете добавить конвейеры непрерывной интеграции и развертывания для автоматической сборки, тестирования и развертывания образов на серверах приложений всякий раз, когда происходит определенное событие. Например, событием может быть отправка нового кода в указанную ветку репозитория Git. Вы также можете автоматизировать то, что происходит, когда контейнер сталкивается с ошибкой. Официальные документы Docker содержат детальное руководство по автоматическому запуску контейнеров в случае ошибок или перезагрузки системы.