Tutorial

Масштабирование и обеспечение безопасности приложения Django с помощью Docker, Nginx и Let's Encrypt

Published on August 26, 2020
Русский
Масштабирование и обеспечение безопасности приложения Django с помощью Docker, Nginx и Let's Encrypt

Введение

В облачных средах существует множество способов масштабирования и защиты приложения Django. Используя горизонтальное масштабирование и запустив несколько экземпляров приложения, вы можете создать более отказоустойчивую систему с высоким уровнем доступности, а также увеличить ее пропускную способность для одновременной обработки запросов. Одним из способов горизонтального масштабирования приложения Django является предоставление дополнительных серверов приложения, запускающих ваше приложение Django и его HTTP-сервер WSGI (например, Gunicorn или uWSGI). Чтобы направить и распределить входящие запросы через эту группу серверов приложения, вы можете использовать балансировщик нагрузки и обратный прокси-сервер, например Nginx. Nginx также может кэшировать статический контент и прерывать протокол безопасности Transport Layer Security (TLS), который используется для активации HTTPS и безопасных подключений к вашему приложению.

Запуск приложения Django и прокси-сервера Nginx в контейнерах Docker гарантирует одинаковое поведение этих компонентов, независимо от среды развертывания. Кроме того, контейнеры предоставляют много возможностей, облегчающих упаковку и настройку вашего приложения.

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

Также вы активируете HTTPS, предоставив и настроив третий прокси-сервер, который будет выполнять запуск контейнера обратного прокси-сервера Nginx и контейнера клиента Certbot. Certbot будет предоставлять сертификаты TLS для Nginx от имени органа сертификации Let’s Encrypt. Это позволит вашему сайту получить высокий рейтинг безопасности от SSL Labs. Этот прокси-сервер будет получать все внешние запросы приложения и находиться перед двумя исходящими серверами приложения Django. И наконец, вы усилите данную распределенную систему, оставив внешний доступ только к прокси-серверу.

Предварительные требования

Для данного обучающего модуля вам потребуется следующее:

  • Три сервера Ubuntu 18.04:

    • Два сервера будут выполнять функции серверов приложения, используемых для запуска приложения Django и Gunicorn.
    • Один сервер будет выполнять роль прокси-сервера, используемого для запуска Nginx и Certbot.
    • Все должны иметь пользователя non-root user с привилегиями sudo и активный брандмауэр. Дополнительную информацию о настройке этих параметров см. в руководстве по первоначальной настройке сервера.
  • Установленный на всех трех серверах Docker. Инструкции по установке Docker см. в шагах 1 и 2 руководства Установка и использование Docker в Ubuntu 18.04.

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

  • Запись DNS A, где your_domain.com​​​ указывает на публичный IP-адрес вашего прокси-сервера. Вы можете воспользоваться введением в работу с DigitalOcean DNS, чтобы получить подробную информацию о добавлении доменов в учетную запись DigitalOcean, если вы используете этот способ.

  • Хранилище объектов S3, например пространство DigitalOcean, для хранения статических файлов проекта Django и набор ключей доступа к этому пространству. Чтобы узнать, как создать пространство, ознакомьтесь с документом Как создавать пространства. Чтобы узнать, как создать ключи доступа, ознакомьтесь со статьей Предоставление доступа к пространству с помощью ключей доступа. Внеся незначительные изменения, вы можете использовать любой сервис хранения, который поддерживает плагин django-storages.

  • Экземпляр сервера PostgreSQL, база данных и пользователь для приложения Django. Внеся незначительные изменения, вы можете использовать любую базу данных, которую поддерживает Django.

Шаг 1 — Настройка первого сервера приложения Django

Для начала мы клонируем репозиторий приложения Django на первый сервер приложения. Затем настроим и создадим образ приложения Docker, после чего протестируем приложение, запустив контейнер Django.

Примечание. Если вы уже выполнили инструкции руководства Создание приложения Django и Gunicorn с помощью Docker, то вы уже выполнили шаг 1 и можете переходить к шагу 2, чтобы настроить второй сервер приложения.

Начните с входа в первый из двух серверов приложения Django и используйте git для клонирования ветки polls-docker репозитория GitHub для приложения опросов Django Tutorial Polls App. В этом репозитории содержится код документации Django для образца приложения «Опросы». Ветка polls-docker содержит контейнеризированную версию приложения «Опросы». Чтобы узнать об изменениях, внесенных в приложение «Опросы» для эффективной работы в контейнеризированной среде, см. руководство Создание приложения Django и Gunicorn с помощью Docker.

  1. git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

Перейдите в каталог django-polls:

cd django-polls

В этом каталоге содержится код Python приложения Django, Dockerfile, который Docker будет использовать для построения образа контейнера, а также файл env, содержащий список переменных среды для прохождения в рабочую среду контейнера. Проверьте файл Dockerfile с помощью cat:

cat Dockerfile
Output
FROM python:3.7.4-alpine3.10 ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex \ && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ && python -m venv /env \ && /env/bin/pip install --upgrade pip \ && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ && runDeps="$(scanelf --needed --nobanner --recursive /env \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u)" \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"]

Dockerfile использует официальный образ Docker Python 3.7.4 в качестве базы и устанавливает пакетные требования Python для Django и Gunicorn в соответствии с файлом django-polls/requirements.txt. Затем он удаляет некоторые ненужные файлы сборки, копирует код приложения в образ и устанавливает параметр выполнения PATH. И в заключение заявляет, что порт 8000 будет использоваться для принятия входящих подключений контейнера, и запускает gunicorn с тремя рабочими элементами, прослушивающими порт 8000.

Дополнительную информацию о каждом этапе в Dockerfile см. в шаге 6 руководства Создание приложения Django и Gunicorn с помощью Docker.

Теперь создайте образ с помощью docker build:

  1. docker build -t polls .

Назовем образ polls, используя флаг -t, и передадим в текущий каталог как контекст сборки набор файлов для справки при построении образа.

После того как Docker создаст и отметит образ, перечислите доступные образы, используя docker images:

docker images

Вы должны увидеть образ polls:

Output
REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB

Перед запуском контейнера Django нам нужно настроить его рабочую среду с помощью файла env, присутствующего в текущем каталоге. Этот файл будет передан в команду docker run, которая используется для запуска контейнера, а Docker будет вводить настроенные переменные среды в рабочую среду контейнера.

Откройте файл env с помощью nano или своего любимого редактора:

nano env

Мы настроим файл примерно так, и вам потребуется добавить некоторые дополнительные значения, как показано ниже.

django-polls/env
DJANGO_SECRET_KEY=
DEBUG=True
DJANGO_ALLOWED_HOSTS=
DATABASE_ENGINE=postgresql_psycopg2
DATABASE_NAME=polls
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_HOST=
DATABASE_PORT=
STATIC_ACCESS_KEY_ID=
STATIC_SECRET_KEY=
STATIC_BUCKET_NAME=
STATIC_ENDPOINT_URL=
DJANGO_LOGLEVEL=info

Заполните недостающие значения для следующих ключей:

  • DJANGO_SECRET_KEY: задает уникальное, непрогнозируемое значение, как описано в документации Django. Один метод генерирования этого ключа представлен в разделе Изменение настроек приложения руководства Масштабируемое приложение Django.
  • DJANGO_ALLOWED_HOSTS: эта переменная защищает приложение и предотвращает атаки через заголовки хоста HTTP. С целью тестирования установите * как подстановочный символ, подходящий для всех хостов. В производственной среде вы должны задать значение your_domain.com. Дополнительную информацию об этой настройке Django см. в разделе Базовые настройки документации Django.
  • DATABASE_USERNAME: задает пользователя базы данных PostgreSQL, созданного на предварительных этапах.
  • DATABASE_NAME: задает polls или имя базы данных, созданной на предварительных этапах.
  • DATABASE_PASSWORD: задает пароль пользователя базы данных PostgreSQL, созданного на предварительных этапах.
  • DATABASE_HOST: задает имя хоста базы данных.
  • DATABASE_PORT: задает порт базы данных.
  • STATIC_ACCESS_KEY_ID: задает хранилище S3 или ключ доступа к пространству.
  • STATIC_SECRET_KEY: задает хранилище S3 или секретный ключ доступа к пространству.
  • STATIC_BUCKET_NAME: задает хранилище S3 или имя пространства.
  • STATIC_ENDPOINT_URL: задает соответствующее хранилище S3 или URL-адрес пространства, например https://space-name.nyc3.digitaloceanspaces.com, если ваше пространство находится в регионе nyc3.

После завершения редактирования сохраните и закройте файл.

Теперь мы будем использовать docker run, чтобы переопределить CMD, установленный в Dockerfile, и создать схему базы данных с помощью команд manage.py makemigrations и manage.py migrate:

docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate"

Мы запускаем образ контейнера polls:latest, передаем в только что измененный файл переменной среды и переопределяем команду Dockerfile с помощью sh -c "python manage.py makemigrations && python manage.py migrate", которая создаст схему базы данных, определяемую кодом приложения. Если вы проводите запуск впервые, вы должны увидеть следующее:

Output
No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK

Это означает, что схема базы данных успешно создана.

Если в следующий раз вы запускаете migrate, Django будет выполнять холостую команду, если не изменить схему базы данных.

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

docker run -i -t --env-file env polls sh

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

python manage.py createsuperuser

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

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

docker run --env-file env polls sh -c "python manage.py collectstatic --noinput"

После создания и загрузки файлов вы получите следующий вывод.

Output
121 static files copied.

Теперь мы можем запустить приложение:

docker run --env-file env -p 80:8000 polls
Output
[2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9

Мы запустим команду по умолчанию, определенную в Dockerfile, gunicorn --bind :8000 --workers 3 mysite.wsgi:application и раскроем порт контейнера 8000, чтобы сопоставить порт 80 на сервере Ubuntu с портом 8000 контейнера polls.

Теперь вы сможете перейти к приложению polls с использованием веб-браузера, набрав в адресной-строке URL http://APP_SERVER_1_IP. Поскольку маршрут для пути / не определен, скорее всего, вы получите ошибку 404 Страница не найдена.

Предупреждение. При использовании с Docker брандмауэра UFW, Docker обходит любые настроенные правила брандмауэра UFW. Об этой ситуации можно прочитать в GitHub. Это объясняет, почему у вас есть доступ к порту 80 сервера, хотя вы не создавали правило доступа UFW на предварительном этапе. На шаге 5 мы рассмотрим эту брешь в системе безопасности и скорректируем настройки UFW. Если вы не используете UFW, а используете облачные брандмауэры DigitalOcean, вы можете спокойно игнорировать это предупреждение.

Перейдите в адрес http://APP_SERVER_1_IP/polls, чтобы увидеть интерфейс приложения «Опросы»:

Интерфейс приложения «Опросы»

Чтобы посмотреть административный интерфейс, перейдите по адресу http://APP_SERVER_1_IP/admin. Вы должны увидеть окно аутентификации администратора приложения «Опросы»:

Страница аутентификации администратора приложения

Введите имя администратора и пароль, которые вы создали с помощью команды createsuperuser.

После аутентификации вы сможете получить доступ к административному интерфейсу приложения «Опросы»:

Основной интерфейс администратора приложения

Обратите внимание, что статические активы приложений admin и polls поступают напрямую из хранилища объекта. Чтобы убедиться в этом, ознакомьтесь с материалами Тестирование доставки статических файлов пространства.

После завершения изучения данных нажмите CTRL+C в окне терминала, где запущен контейнер Docker, чтобы удалить контейнер.

Когда мы убедились, что контейнер приложения работает, как и ожидалось, можно запустить его в раздельном режиме, при котором контейнер будет работать в фоновом режиме, что позволит вам выйти из сеанса SSH.

docker run -d --rm --name polls --env-file env -p 80:8000 polls

Флаг -d предписывает Docker запустить контейнер в раздельном режиме, флаг -rm очищает файловую систему контейнера после выхода контейнера, а мы называем контейнер polls.

Выйдите из первого сервера приложения Django и перейдите к адресу http://APP_SERVER_1_IP/polls для подтверждения того, что контейнер работает корректно.

После запуска и активации первого сервера приложения Django вы можете настроить второй сервер приложения Django.

Шаг 2 — Настройка второго сервера приложения Django

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

Начните с входа на второй сервер приложения Django.

Клонируйте ветку polls-docker репозитория GitHub django-polls:

  1. git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

Перейдите в каталог django-polls:

cd django-polls

Создайте образ с помощью docker build:

  1. docker build -t polls .

Откройте файл env с помощью nano или своего любимого редактора:

nano env
django-polls/env
DJANGO_SECRET_KEY=
DEBUG=True
DJANGO_ALLOWED_HOSTS=
DATABASE_ENGINE=postgresql_psycopg2
DATABASE_NAME=polls
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_HOST=
DATABASE_PORT=
STATIC_ACCESS_KEY_ID=
STATIC_SECRET_KEY=
STATIC_BUCKET_NAME=
STATIC_ENDPOINT_URL=
DJANGO_LOGLEVEL=info

Заполните недостающие значения, как показано в шаге 1. После завершения редактирования сохраните и закройте файл.

Наконец, запустите контейнер приложения в раздельном режиме:

docker run -d --rm --name polls --env-file env -p 80:8000 polls

Перейдите на http://APP_SERVER_2_IP/polls для подтверждения того, что контейнер работает корректно. Вы можете безопасно выйти из второго сервера приложения без завершения работы контейнера.

Когда вы настроили и запустили оба контейнера приложения Django, можно переходить к настройке обратного прокси-сервера Nginx.

Шаг 3 — Настройка контейнера Docker Nginx

Nginx — это универсальный веб-сервер, предлагающий ряд функций, включая обратное проксирование, балансирование нагрузки и кэширование. В этом обучающем руководстве мы вызгрузили статические активы Django в хранилище объектов, поэтому мы не будем использовать возможности кэширования Nginx. Однако мы будем использовать Nginx как обратный прокси-сервер для двух серверов приложения Django и распределим входящие запросы между ними. Кроме того, Nginx будет осуществлять прекращение TLS и перенаправление с помощью сертификата TLS, предоставленного Certbot. Это означает, что он заставит клиентов использовать HTTPS, перенаправляя входящие запросы HTTP в порт 443. Затем он расшифрует запросы HTTPS и передаст их на серверы Django.

В этом обучающем руководстве мы приняли проектное решение отделить контейнеры Nginx от серверов. В зависимости от своих потребностей вы можете запускать контейнер Nginx на одном из серверов приложения Django, обрабатывая запросы с помощью прокси-сервера локально или на другом сервере Django. Еще один вариант архитектуры: запуск двух контейнеров Nginx, по одному на каждом сервере, и облачного балансировщика нагрузки перед ними. Каждая архитектура имеет свои преимущества безопасности и производительности, и вам потребуется провести нагрузочное тестирование, чтобы определить узкие места. Гибкая архитектура, описанная в данном руководстве, позволяет масштабировать как серверный слой приложения Django, так и прокси-сервер Nginx. Если узким местом станет единственный контейнер Nginx, вы сможете масштабировать несколько прокси-серверов Nginx и добавить облачный балансировщик нагрузок или быстрый балансировщик нагрузок L4, например HAProxy.

Когда вы создадите и запустите оба сервера приложения Django, можно начинать настройку прокси-сервера Nginx. Войдите на ваш прокси-сервер и создайте каталог с именем conf:

mkdir conf

Создайте файл конфигурации nginx.conf с помощью nano или своего любимого редактора:

nano conf/nginx.conf

Вставьте следующую конфигурацию Nginx:

conf/nginx.conf

upstream django {
	server APP_SERVER_1_IP;
	server APP_SERVER_2_IP;
}

server {
	listen 80 default_server;
	return 444;
}

server {
	listen 80;
	listen [::]:80;
	server_name your_domain.com;
	return 301 https://$server_name$request_uri;
}

server {
	listen 443 ssl http2;
	listen [::]:443 ssl http2;
	server_name your_domain.com;

	# SSL
	ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/your_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;
	}

}

Блоки upstream, server и location настраивают Nginx таким образом, чтобы перенаправлять HTTP-запросы на HTTPS и распределять нагрузку на два сервера приложения Django, настроенных в шагах 1 и 2. Дополнительную информацию о структуре файла конфигурации Nginx см. в статье Понимание структуры файла конфигурации и контекста конфигурации. Также может быть полезна статья Понимание работы сервера Nginx и алгоритмов выбора блоков расположения.

Эта конфигурация была собрана из примеров файлов конфигурации из Gunicorn, Certbot и Nginx и является минимальной конфигурацией Nginx, необходимой для построения и запуска данной архитектуры. Настройка данной конфигурации Nginx выходит за рамки этой статьи, но вы можете использовать такой инструмент, как NGINXConfig, для генерирования исполнительных и безопасных файлов конфигурации для вашей архитектуры.

Блок upstream определяет группу серверов, которые используются для передачи запросов с использованием директивы proxy_pass:

conf/nginx.conf
upstream django {
	server APP_SERVER_1_IP;
	server APP_SERVER_2_IP;
}
. . .

В этом блоке мы используем имя django и включаем IP-адреса обоих серверов приложения Django. Если серверы приложения работают на DigitalOcean с активированной сетью VPC, вам следует использовать их частные IP-адреса. Чтобы узнать, как активировать сеть VPC на DigitalOcean, см. Как активировать сеть VPC на существующих дроплетах.

Первый блок server захватывает запросы, которые не соответствуют вашему домену, и прерывает соединение. Например, в этом блоке будет обрабатываться прямой запрос HTTP на IP-адрес вашего сервера:

conf/nginx.conf
. . .
server {
	listen 80 default_server;
	return 444;
}
. . .

Следующий блок server перенаправляет запросы HTTP на ваш домен на HTTPS, используя перенаправление HTTP 301. Эти запросы рассматриваются в последнем блоке server:

conf/nginx.conf
. . .
server {
    listen 80;
    listen [::]:80;
    server_name your_domain.com;
    return 301 https://$server_name$request_uri;
}
. . .

Эти две директивы определяют путь к сертификату TLS и секретному ключу. Они предоставляются с помощью Certbot и монтируются в контейнер Nginx на следующем шаге.

conf/nginx.conf
. . .
ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
. . .

Это по умолчанию параметры безопасности SSL, рекомендованные Certbot. Дополнительную информацию о них см. в разделе Модуль ngx_http_ssl_module в документации Nginx. Еще одним полезным руководством, которое вы можете использовать для настройки конфигурации SSL, является инструкция по безопасности от Mozilla Security/Server Side TLS.

conf/nginx.conf
. . .
	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";
. . .

Эти две директивы из примерной конфигурации Nginx от Gunicorn определяют максимальный разрешенный размер тела запроса клиента и назначают временной интервал поддержки соединения с клиентом. Nginx закроет соединения с клиентом после keepalive_timeout.

conf/nginx.conf
. . .
client_max_body_size 4G;
keepalive_timeout 5;
. . .

Первый блок location предписывает Nginx направлять запросы на серверы upstream django через HTTP. Также он сохраняет клиентские заголовки HTTP, захватывающие исходный IP-адрес, протокол, используемый для соединения, и целевой хост:

conf/nginx.conf
. . .
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;
}
. . .

Дополнительную информацию об этих директивах см. в разделах развертывание Gunicorn и Модуль ngx_http_proxy_module из документации Nginx.

Последний блок location захватывает запросы на путь /well-known/acme-challenge/, используемый Certbot для задач HTTP-01 по проверке вашего домена с помощью Let’s Encrypt и предоставлению или обновлению сертификатов TLS. Дополнительную информацию о задаче HTTP-01, используемой Certbot, см. в разделе Виды задач из документации Let’s Encrypt.

conf/nginx.conf
. . .
location ^~ /.well-known/acme-challenge/ {
		root /var/www/html;
}

После завершения редактирования сохраните и закройте файл.

Теперь вы можете использовать этот файл конфигурации для запуска контейнера Nginx Docker. В этом обучающем руководстве мы будем использовать образ nginx:1.19.0, версия 1.19.0 официального образа Docker, обслуживаемого Nginx.

При первом запуске контейнера Nginx будет выдавать ошибку и отказывать в работе, так как мы еще не предоставили сертификаты, определенные в файле конфигурации. Однако мы все равно будем выполнять команду для локальной загрузки образа Nginx и протестируем корректность работы остальных элементов.

docker run --rm --name nginx -p 80:80 -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

Давайте назовем контейнер nginx и свяжем порты хоста 80 и 443 с соответствующими портами контейнера. Флаг -v монтирует файл конфигурации в контейнер Nginx на /etc/nginx/conf.d/nginx.conf, который образ Nginx предварительно настраивает для загрузки. Он монтируется в режиме ro или «только для чтения», поэтому контейнер не может изменять файл. Также в контейнер монтируется корневой веб-каталог /var/www/html. Наконец, nginx:1.19.0 предписывает Docker вытащить и запустить образ nginx:1.19.0 из Dockerhub.

Docker вытащит и запустит образ, затем Nginx выдаст ошибку, т.к. не найдет настроенный сертификат TLS и секретный ключ. На следующем шаге мы предоставим их с помощью клиента Dockerized Certbot и органа сертификации Let’s Encrypt.

Шаг 4 — Настройка Certbot и обновление сертификата Let’s Encrypt

Certbot — это клиент Let’s Encrypt, разработанный фондом Electronic Frontier Foundation. Он предоставляет бесплатные сертификаты TLS от органа сертификации Let’s Encrypt, которые позволяют браузерам идентифицировать ваши веб-серверы. Поскольку мы установили Docker на нашем прокси-сервере Nginx, мы будем использовать образ Docker Certbot для предоставления и обновления сертификатов

Для начала проверьте наличие записи А DNS, соединенной с публичным IP-адресом прокси-сервера. Затем на вашем прокси-сервере представьте тестовую версию сертификатов с помощью образа Docker certbot:

docker run -it --rm -p 80:80 --name certbot \
         -v "/etc/letsencrypt:/etc/letsencrypt" \
         -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
         certbot/certbot certonly --standalone --staging -d your_domain.com

Эта команда запускает образ Docker certbot в интерактивном режиме и направляет порт 80 на хосте на порт контейнера 80. Она создает и монтирует два каталога хоста в контейнер: /etc/letsencrypt/ и /var/lib/letsencrypt/. certbot работает в режиме standalone без Nginx и будет использовать серверы staging Let’s Encrypt для валидации домена.

Когда появится строка ввода, укажите свой электронный адрес и примите Условия обслуживания. Если валидация домена прошла успешно, вы увидете следующий вывод:

Output
Obtaining a new certificate Performing the following challenges: http-01 challenge for stubb.dev Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain.com/privkey.pem Your cert will expire on 2020-09-15. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal.

Вы можете проверить сертификат с помощью cat:

sudo cat /etc/letsencrypt/live/your_domain.com/fullchain.pem

После предоставления сертификата TLS мы можем протестировать конфигурацию Nginx, собранную на предыдущем шаге:

docker run --rm --name nginx -p 80:80 -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /etc/letsencrypt:/etc/letsencrypt \
    -v /var/lib/letsencrypt:/var/lib/letsencrypt \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

Задаем те же команды, что и на шаге 3, добавив оба только что созданных каталога Let’s Encrypt.

После установки и активации Nginx перейдите на http://your_domain.com. Вы можете получить предупреждение в браузере о том, что орган сертификации недействителен. Это ожидаемо, так как мы предоставили тестовые, а не рабочие сертификаты Let’s Encrypt. Проверьте строку URL-адреса вашего браузера, чтобы убедиться, что ваш запрос HTTP был перенаправлен на HTTPS.

Нажмите CTRL+C на вашем терминале, чтобы выйти из Nginx, и снова запустите клиент certbot, на этот раз опустив флаг --staging:

docker run -it --rm -p 80:80 --name certbot \
         -v "/etc/letsencrypt:/etc/letsencrypt" \
         -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
         certbot/certbot certonly --standalone -d your_domain.com

Когда появится окно выбора: сохранить действующий сертификат или обновить и заменить, нажмите 2, чтобы обновить, а затем ENTER, чтобы подтвердить свой выбор.

После предоставления рабочего сертификата TLS запустите сервер Nginx снова:

docker run --rm --name nginx -p 80:80 -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /etc/letsencrypt:/etc/letsencrypt \
    -v /var/lib/letsencrypt:/var/lib/letsencrypt \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

В вашем браузере перейдите на http://your_domain.com. В строке URL-адреса подтвердите, что запрос HTTP был перенаправлен на HTTPS. Поскольку приложение «Опросы» не имеет настроенного по умолчанию маршрута, вы увидите ошибку Django Страница не найдена. Перейдите на https://your_domain.com/polls и вы увидите стандартный интерфейс приложения «Опросы»:

Интерфейс приложения «Опросы»

На этом этапе вы предоставили рабочий сертификат TLS с помощью клиента Docker Certbot, направляете внешние запросы с помощью обратного прокси-сервера и распределяете нагрузку между двумя серверами приложения Django.

Срок действия сертификатов Let’s Encrypt истекает каждые 90 дней. Чтобы удостовериться в действительности вашего сертификата, необходимо регулярно обновлять его до истечения срока действия. После запуска Nginx вы должны использовать клиент Certbot в режиме webroot вместо режима standalone. Это означает, что Certbot будет выполнять проверку путем создания файла в каталоге /var/www/html/.well-known/acme-challenge/, и запросы Let’s Encrypt о валидации на этом пути будут захватываться правилом location, определенном в конфигурации Nginx на шаге 3. Certbot будет менять сертификаты, и вы сможете перезагрузить Nginx, чтобы он использовал уже новый предоставленный сертификат.

Существует множество способов автоматизации этой процедуры, но автоматическое обновление сертификатов TLS выходит за рамки данного обучающего руководства. Для аналогичного процесса с помощью утилиты планирования cron см. шаг 6 руководства Обеспечение безопасности контейнеризированного приложения Node.js с помощью Nginx, Let’s Encrypt и Docker Compose.

На вашем терминале нажмите CTRL+C, чтобы удалить контейнер Nginx. Запустите его снова в раздельном режиме, добавив флаг -d:

docker run --rm --name nginx -d -p 80:80 -p 443:443 \
	-v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
	-v /etc/letsencrypt:/etc/letsencrypt \
	-v /var/lib/letsencrypt:/var/lib/letsencrypt \
  -v /var/www/html:/var/www/html \
	nginx:1.19.0

При запущенном в фоновом режиме Nginx используйте следующую команду для выполнения сухого запуска процедуры обновления сертификата:

docker run -it --rm --name certbot \
	-v "/etc/letsencrypt:/etc/letsencrypt" \
  -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
  -v "/var/www/html:/var/www/html" \
  certbot/certbot renew --webroot -w /var/www/html --dry-run

Мы используем плагин --webroot, указываем корневой путь и используем флаг ---dry-run, чтобы убедиться, что все работает корректно без обновления сертификата.

Если моделирование обновления завершится успешно, вы увидите следующий вывод:

Output
Cert not due for renewal, but simulating renewal for dry run Plugins selected: Authenticator webroot, Installer None Renewing an existing certificate Performing the following challenges: http-01 challenge for your_domain.com Using the webroot path /var/www/html for all unmatched domains. Waiting for verification... Cleaning up challenges - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new certificate deployed without reload, fullchain is /etc/letsencrypt/live/your_domain.com/fullchain.pem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/your_domain.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

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

docker kill -s HUP nginx

Эта команда будет отправлять сигнал Unix HUP для процесса Nginx, работающего внутри контейнера Docker nginx. После получения этого сигнала Nginx перезагрузит конфигурацию и обновленные сертификаты.

После активации HTTPS и всех компонентов данной архитектуры итоговым шагом является блокировка настройки от внешнего доступа к двум серверам приложения. Все запросы HTTP должны проходить через прокси-сервер Nginx.

Шаг 5 — Предотвращение внешнего доступа к серверу приложения Django

В описанной в этом руководстве архитектуре прекращение SSL происходит на прокси-сервере Nginx. Это означает, что Nginx расшифровывает соединение SSL, а пакеты передаются на серверы приложения Django без шифрования. Для многих случаев использования этого уровня безопасности достаточно. Для приложений, содержащих финансовые данные или данные о состоянии здоровья, вам может понадобится внедрение сквозного шифрования. Вы можете сделать это, передавая зашифрованные пакеты через балансировщик нагрузки и расшифровывая их на серверах приложения, или зашифровывая на прокси-сервере и снова расшифровывая на серверах приложения Django. Эти методы выходят за рамки данного руководства, но для получения более подробной информации можно ознакомиться со статьей Сквозное шифрование.

Прокси-сервер Nginx действует как шлюз между внешним трафиком и внутренней сетью. Теоретически ни один внешний клиент не должен иметь прямого доступа к внутренним серверам приложения, и все запросы должны проходить через сервер Nginx. В примечании на шаге 1 кратко описывается проблема с Docker, где Docker обходит настройки брандмауэра по умолчанию ufw и открывает порты внешним способом, что может быть небезопасным. Чтобы решить эту проблему безопасности, рекомендуется использовать облачные брандмауэры при работе с серверами на базе Docker. Дополнительную информацию о создании облачного брандмауэра с помощью DigitalOcean можно найти в руководстве Создание брандмауэра. Также вы можете использовать непосредственно iptables вместо ufw. Дополнительную информацию об использовании iptables с Docker можно найти в статье Docker and iptables.

На этом шаге мы изменим конфигурацию UFW, чтобы заблокировать внешний доступ к портам хоста, которые открывает Docker. При запуске Django на серверах приложения мы передаем флаг -p 80:8000 на docker, который направляет порт 80 на хосте в порт контейнера 8000. Это также открывает порт 80 для внешних клиентов, которых вы можете верифицировать, посетив http://your_app_server_1_IP. Чтобы предотвратить прямой доступ, мы изменим конфигурацию UFW с помощью метода, описанного в репозитории GitHub ufw-docker.

Начните с входа на первый сервер приложения Django. Затем откройте файл /etc/ufw/after.rules с привилегиями суперпользователя, используя nano или свой любимый редактор:

sudo nano /etc/ufw/after.rules

Введите свой пароль при запросе и нажмите ENTER для подтверждения.

Вы должны увидеть следующие правила ufw:

/etc/ufw/after.rules
#
# rules.input-after
#
# Rules that should be run after the ufw command line added rules. Custom
# rules should be added to one of these chains:
#   ufw-after-input
#   ufw-after-output
#   ufw-after-forward
#

# Don't delete these required lines, otherwise there will be errors
*filter
:ufw-after-input - [0:0]
:ufw-after-output - [0:0]
:ufw-after-forward - [0:0]
# End required lines

# don't log noisy services by default
-A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input

# don't log noisy broadcast
-A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input

# don't delete the 'COMMIT' line or these rules won't be processed
COMMIT

Прокрутите вниз и вставьте следующий блок правил конфигурации UFW:

/etc/ufw/after.rules
. . .

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER

Эти правила ограничивают публичный доступ к портам, которые открывает Docker, и позволяют получить доступ от частных диапазонов IP-диапазонов 10.0.0.0/8, 172.16.0.0/12 и 192.168.0.0/16. Если вы используете VPC с DigitalOcean, тогда дроплеты в вашей сети VPC получат доступ к открытому порту через закрытый интерфейс, а внешние клиенты не получат. Дополнительную информацию о VPC см. в официальной документации VPC. Дополнительную информацию о правилах, которые использованы в этом фрагменте, ищите в разделе Как это работает? инструкции ufw-docker README.

Если вы не используете VPC с Digitalocean и ввели публичные IP-адреса серверов приложений в блоке upstream вашей конфигурации Nginx, вам придется изменить брандмауэр UFW, чтобы разрешить трафик с сервера Nginx через порт 80 на серверы приложения Django. Инструкции по созданию правил allow с брандмауэром UFW см. в руководстве Основы UFW: общие правила и команды брандмауэра.

После завершения редактирования сохраните и закройте файл.

Перезапустите ufw для выбора новой конфигурации:

sudo systemctl restart ufw

Перейдите на http://APP_SERVER_1_IP в вашем браузере для подтверждения того, что вы больше не можете получить доступ к серверу приложения через порт 80.

Повторите этот процесс на втором сервере приложения Django.

Выйдите из первого сервера приложения или откройте другое окно терминала и войдите на второй сервер приложения Django. Затем откройте файл /etc/ufw/after.rules с привилегиями суперпользователя, используя nano или свой любимый редактор:

sudo nano /etc/ufw/after.rules

Введите свой пароль при запросе и нажмите ENTER для подтверждения.

Прокрутите вниз и вставьте следующий блок правил конфигурации UFW:

/etc/ufw/after.rules
. . .

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER

После завершения редактирования сохраните и закройте файл.

Перезапустите ufw для выбора новой конфигурации:

sudo systemctl restart ufw

Перейдите на http://APP_SERVER_2_IP в вашем браузере для подтверждения того, что вы больше не можете получить доступ к серверу приложения через порт 80.

В заключение перейдите на https://your_domain_here/polls, чтобы подтвердить, что прокси-сервер Nginx все еще имеет доступ к серверам Django. Вы должны увидеть интерфейс приложения «Опросы» по умолчанию.

Заключение

В этом обучающем руководстве мы настроили масштабируемое приложение Django «Опросы» при помощи контейнеров Docker. По мере увеличения трафика и загрузки системы вы можете масштабировать каждый слой отдельно: слой прокси-сервера Nginx, слой серверного приложения Django и слой базы данных PostgreSQL.

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

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

При работе в масштабе с несколькими хостами на одном и том же образе Docker более эффективной может стать автоматизация шагов с использованием инструмента управления конфигурацией, например Ansible или Chef. Дополнительную информацию об управлении конфигурацией см. в руководствах Введение в управление конфигурацией и Автоматизированная настройка сервера с помощью Ansible: набор материалов для тренинга DigitalOcean.

Вместо создания такого же образа на каждом хосте вы можете также упорядочить развертывание с помощью реестра образов, такого как Docker Hub, который централизованно строит, хранит и распределяет образы Docker на несколько серверов. Наряду с реестром образов непрерывный конвейер интеграции и развертывания может помочь вам строить, тестировать и развертывать образы на ваших серверах приложения. Дополнительную информацию о CI/CD см. в руководстве Введение в передовой опыт CI/CD.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Featured on Community

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel