Простой сервер

Пишешь свою игру с поддержкой сети? Планируешь свой сайт или веб-сервис? Бот для телеграмма или навык Яндекс Алисы?

Всему этому нужен backend. А backend’у нужна инфраструктура.

Поднимаем свой сервер со своим доменом в интернете на Ubuntu Linux, настраиваем стек Nginx + Gunicorn + Flask, защищаем все соединения при помощи SSL.

Смотреть на YouTube

Дисклеймер (Disclaimer)

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

Знатоков того самого единственного правильного рецепта я попрошу сегодня не беспокоится.

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

Давайте попробуем разбить это руководство на некие условные этапы, но четкой градации, как мне кажется, достичь просто невозможно, в том числе из-за сильной интеграции сервисов и процессов друг с другом.

Что мы в итоге получим

Работающий на сервер.

На входе мы установим веб-сервер Nginx, который и будет принимать входящие запросы по http (80 порт) и https (443 порт) и передавать (проксировать) их серверу приложений gunicorn. Который в свою очередь запустит экземпляр python приложения, написанного с использованием микро-фреймворка flask, и передаст запрос туда. И именно там, в python коде и произойдет обработка входящего запроса, ответ на который заберет gunicorn и передаст в nginx, который уже вернет результаты пользователю.

Предполагаю, что основой запросов и ответов будут данные сериализованные в json формат для обработки запросов от внешних сервисов, и доступная по умолчанию генерация html.

Все это будет защищено сертификатом от Let`s Encrypt и встроенным файрволом. А управление, настройка и обновление - через подключение по SSH .

Где.

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

Регистрация доменного имени. Я для этой цели воспользовался сервисом регистрации от AWS - Route 53. Просто потому, что я давно им пользуюсь, и это не первый домен который я у них регистрирую, вы же, повторюсь, можете воспользоваться услугами абсолютно любого регистратора.

Да, возможно кто то скажет - ну зачем я буду тратить лишние 10-20 долларов на доменное имя, если можно ходить на сервер просто по IP? И да, и нет. Современные правила работы предполагают использование зашифрованного соединения с нашим сервером, а сервисы предоставляющие сертификаты требуют именно зарегистрированное доменное имя, а не IP адрес. Без использования шифрования пользователь, в лучшем случае будет получать постоянные сообщения браузера об угрозе безопасности, а в худшем вообще не сможет подключится.

Итак, я только зарегистрировал имя blueoctopus.cc, и на текущий момент на нем ни одной записи.

Саму виртуальную машину мы сделаем в Yandex Cloud. (Да, я все понимаю, но прошу не осуждать меня).

Ключи доступа.

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

Итак, откроем консоль кликнув на соответствующую иконку, либо нажав комбинацию клавиш Ctrl + Alt + T (Command (⌘) + T) и перейдем в каталог назначенный по умолчанию для хранения ключей.

cd .ssh

Если такой папки нет, то ее необходимо создать

mkdir .ssh

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

Если при попытке выполнить указанные выше действия вы столкнулись с ошибками, то вероятно вы используете ОС Wndows на своем ПК. Насколько мне известно, на Windows 10 c 2018 позволяет использовать SSH, только его необходимо отдельно установить. Официальный сайт Microsoft говорит, что:

OpenSSH можно использовать для подключения устройств с Windows 10 (версия 1809 и более поздние) Чтобы установить компоненты OpenSSH, сделайте следующее:

  1. Откройте приложение Параметры, выберите элементы Приложения > Приложения и возможности, щелкните Дополнительные возможности.
  2. Просмотрите этот список и определите, установлено ли средство OpenSSH. Если нет, выберите пункт Добавить компонент в верхней части страницы и сделайте следующее:
    • Найдите Клиент OpenSSH и щелкните Установить.

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

Продолжим. Напомню, у нас открыт терминал и мы находимся в папке .ssh

Напечатаем простую команду:

.ssh$  ssh-keygen

Генератор предложит вам указать уникальное название ключа, защитить его дополнительно паролем, и вроде бы будет еще пара вопросов, которые никто не читает.

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

В итоге у вас будет два файла: server_key - это ваш приватный ключ, его необходимо оставить в папке .ssh вашей домашней директории, и никому никогда не передавать. И server_key.pub - это, соответственно, публичная часть, содержимое которого необходимо загрузить при создании конфигурации нашей виртуальной машины.

Выведем содержимое публичного ключа и скопируем его в буфер обмена.

cat server_key.pub

Для удобства, большой точности и безопасности копирования в буфер можно воспользоваться утилитами xclip для Linux и pbcopy для MacOS.

Виртуальная машина в Yandex Cloud

Теперь откроем консоль Yandex Cloud. Если у вас нет аккаунта, то его придется создать, думаю, что с регистрацией на Яндекс проблем возникнуть не должно.

Далее в меню слева выбираем раздел Compute Cloud и нажимаем кнопку Создать ВМ. Я специально решил делать этот пример на сервисах яндекса, поскольку у них есть подробное описание каждого пункта меню на русском языке.

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

Давайте быстро пройдемся по пунктам:

Имя и описание - все понятно, уникальное название и некоторые подробности, что бы вы сами не забыли что это за виртуалка крутится у вас в облаке.

Зона доступности - мне яндекс предложил ru-central-b, так и оставлю.

Операционная система - Ubuntu 20.04

Диск - HDD на 13 Gb

Платформа - Intel Ice Lake

vCPU - 2 (Количество ядер процессора, наверное хватило бы и одного, но яндекс так уже не предлагает)

Гарантированная доля vCPU - 20% (Минимальная гарантированная доля производительности. По моему опыту, при настройке загрузка ЦП не превышает 1-2%, так что этой доли нам более чем достаточно не только для настройки сервера, но и для небольших сервисов развернутых на нем.)

RAM - Очевидно оперативная память. Выбирайте минимум 2 GB

Прерываемая - самый спорный пункт. Его выбор делает большую скидку на ВМ, однако это дает право яндексу остановить ее в любой момент. Сервер будет остановлен корректно, так что за сохранность данных переживать не стоит, но включать его обратно прийдется “вручную”. Для этого урока я этот пункт включу, но если настраиваемый вами сервер планируется в дальнейшем использовать в рабочем режиме, то выбирать этот пункт точно не стоит.

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

Думаю здесь необходимо небольшое пояснение - в автоматическом режиме яндекс, конечно, предоставит публичный адрес, но проблема в том что при каждом перезапуске сервера он будет новым, что неприемлемо для настройки доменного имени. И да, это стоит отдельных денег, это нормально. Постоянно зарезервированный адрес стоит немного дороже, чем автоматический, и еще дороже будет стоить зарезервированный, но неиспользуемый адрес. (На момент написания этой статьи это около 150 рублей в месяц.)

В любом случае, когда вы выберите пункт “список”, то скорее всего получите сообщение о том, что в вашем облаке нет свободных статических IP-адресов. В конце этого сообщения будет ссылка с названием “список адресов”, смело нажимаем на нее и попадаем в раздел Virtual Private Cloud.

Выбираем в меню слева пункт “IP-адреса”, и потом жмем на кнопку “Зарезервировать адрес”, тут главное проследить что бы зона доступности, где резервируется адрес была той же, где создается сервер. Если все правильно - нажимаем “Зарезервировать” и возвращаемся к конфигурированию нашей ВМ.

Теперь при настройке публичного адреса в пункте список будет доступен наш зарезервированный IP. Выберем его и перейдем к настройке доступа.

Сервисный аккаунт - нужен если вы планируете получать доступ с виртуальной машины к другим ресурсам облака. (Например к серверу базы данных). В данном случае в нем нет необходимости.

Логин - имя пользователя в на сервере. Тут все просто, главное не выбирайте root или www-data. Я укажу просто max.

SSH-ключ - содержимое того самого server_key.pub, которое мы скопировали в буфер обмена. Просто вставьте его сюда.

Доступ к серийной консоли не включаем. Считайте это аварийным режимом, при штатной работе сервера он должен быть отключен.

Ну вот, теперь внимательно смотрим на правую колонку, и если цена нас устраивает - жмем на кнопку Создать ВМ внизу страницы.

Подключение к серверу

Наша виртуальна машина готова. Поскольку мы еще не строили доменное имя, то давайте подключимся по IP

ssh max@178.154.219.150

Тут все просто: login@ip-adress

Login - Логин, который мы указали при конфигурации.

ip-adress - Зарезервированный за ВМ статический адрес.

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

The authenticity of host 'example.com (178.154.219.150)' can't be established.
ECDSA key fingerprint is SHA256:7Q4nIqjuo/lSXWFkt9RaJYVHrT6LUAc6KWrdQ4/DDeA.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

Все в порядке, так и должно быть, это сервер присылает отпечаток ключа для проверки. Просто напечатаем yes и нажмем enter еще раз.

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

Установка утилит

Давайте для начала обновим данные о репозиториях:

sudo apt update

И, при необходимости, обновим сами пакеты:

sudo apt upgrade

Жмем (Y)es и немного подождем.

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

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

sudo apt install mc git curl wget rsync zip unzip

Опять же жмем (Y)es и немного подождем.

И что бы не терять время пройдемся по утилитам:

mc - Midnight Commander, консольный двух-панельный файловый менеджер. Большинство операций, я выполняю через него.

git - тот самый гит, очень пригодится, если захотим что нибудь клонировать с гитхаб или битбакет. Ну или наоборот отправить туда.

curl - Мне кажется не существует сетевого протокола, по которому эта утилита не могла бы послать запрос прямо из командной строки.

wget - качает файлы по ссылке. Можно конечно качать и с помощью curl, но эта мне как то привычнее.

rsync - удобная синхронизация файлов и каталогов, можно запускать по расписанию для создания резервных копий.

zip и unzip - очевидно архиваторы zip. Родные архиваторы bzip2 и gzip как правило есть всегда, а эти бывает, что и не установлены на старте.

Сервер SSH

Следующим шагом пройдемся по параметрам сервера SSH, который отвечает за обработку нашего подключения.

Для этого откроем файл с настройками параметров.

Это можно сделать напечатав в командной строке:

sudo nano /etc/ssh/sshd_config

Либо запустив midnight commander, найти, выделить файл и нажать F4. При первом запуске МС попросить выбрать предпочитаемый редактор, рекомендую выбрать nano

sudo mc

И да, большую часть команд и правок мы будем вносить используя sudo, (Substitute User and do, дословно «подменить пользователя и выполнить»). Это необходимо для повышения наших полномочий до уровня root

Давайте взглянем на файл конфигурации. Большинство строк с параметрами комментированы символом #, предполагается, что значения указанные в них - будут использованы по умолчанию. Опираясь на один из принципов программирования “явное лучше чем неявное” - пройдемся по параметрам, которые я считаю важным указать явным способом.

Пройдемся по файлу сверху вниз, и первый параметр который нам нужен выглядит так:

#PermitRootLogin prohibit-password

Очевидно из названия - разрешить руту подключатся удаленно с использованием непарольных методов аутентификации. Например сканер отпечатка, смарт-карта и т.д.

Раскомментируем строку и ставим значение в no:

PermitRootLogin No

Никаких удаленных авторизаций и входов для root

Следующий параметр отвечает за возможность авторизации по SSH ключу. Мы именно таким способом и зашли на наш сервер.

PubkeyAuthentication yes

Укажем это явно, оставляем yes по умолчанию, просто удалим знак комментария.

Дальше будет длинная строка с параметрами отделенными табуляцией:

#AuthorizedKeysFile     .ssh/authorized_keys .ssh/authorized_keys2

Это указание на то где искать файл с публичными частями ключей для авторизации пользователей. Оставляем как есть, просто раскомментируем.

И последний обязательный пункт:

#PasswordAuthentication yes

Удаляем символ комментария и устанавливаем параметр в no. Никаких парольных идентификаций, только файлы ключей.

PasswordAuthentication no

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

X11Forwarding yes

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

Все. Теперь нажимаем Ctrl + X и подтверждаем запись файла.

Если вы использовали midnight commander то нажмите Ctrl + O, это скроет панели, отобразив терминал, но не закроет сам MC, так же как и не прервет сессию sudo. Повторное нажатие вернет панели обратно.

Перезапустим наш сервис SSH, что бы применить внесенные изменения.

sudo systemctl restart ssh

Можно отключится напечатав exit, и подключится опять, для проверки работоспособности, он это необязательно.

Устанавливаем Nginx

Именно он будет отвечать за прием входящих запросов и проксированием их в глубь нашего сервера.

Никаких дополнительных модулей устанавливать не планируется, поэтому просто ставим из стандартного репозитория:

sudo apt install nginx

И стразу же проверим его статус:

sudo systemctl status nginx

Все.

Работает. Да, мы еще не настроили доменное имя, но если мы откроем браузер и введем там внешний IP нашей виртуальной машины, то мы увидим стандартную страницу приветствия Nginx. Так же можно воспользоваться утилитой curl, не зря же мы ее ставили.

curl 178.154.219.150

178.154.219.150 - вам следует заменить на свой.

Стандартный HTML лежит в папке /var/www/html, при желании можете его заменить на любой другой. Мы в рамках данной инструкции делать этого не будем, поскольку использование данной папки в дальнейшем не планируется, а стандартная конфигурация домена будет отключена.

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

Что бы не возвращаться к этому в дальнейшем - давайте сразу расскажу про основные команды которые необходимо знать при работе с Nginx:

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

sudo nginx -t

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

Кстати о перезагрузке. Почему то многие советуют использовать системный метод:

sudo systemctl restart nginx

Эта команда целиком остановит и запустит заново все службу, разорвав все текущие соединения, что не всегда хорошо. Точнее всегда не хорошо.

Вы можете сказать, а какая разница, ну нажмет пользователь обновить страницу, а многие может и вообще не заметят этих секунд перезагрузки. Опять же верно, но отчасти. А представьте, что у нас на базе этого сервера работает приложение, которое пишет игровую статистику вашей же разработанной игры? Например получает json файл с новым пользовательским рекордом, и готовится записать его в базу данных. А тут раз, и все потухло. Рекорд не записан, игра напрасно ждет ответа от сервера о статусе операции, пользователь страдает. Или у нас работает бек-энд бота для телеграма или навыка Яндекс Алисы, а там вообще время ожидания 2 и 3 секунды. Получится, что пользователь спросил что то у колонки, а в ответ тишина, Яндекс ждет 2 секунды и отключает сеанс с навыком. Так с пользователями приличные разработчики не поступают.

В общем, используйте этот метод только в том случае, когда точно понимаете зачем это делать.

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

sudo systemctl reload nginx

Либо воспользоваться внутренней командой самого Nginx:

(Да и официальная документация рекомендует именно ее).

sudo nginx -s reload

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

Ну и давайте убедимся, что наш веб-сервер будет запущен при старте операционной системы:

sudo systemctl is-enabled nginx

Если все в порядке - двигаемся дальше, если нет, то добавим его в автозагрузку:

sudo systemctl enable nginx

Некий промежуточный итог

Наш сервер работает.

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

Например, нет необходимости активировать файрвол, если планируется использование специальных групп безопасности (security groups) работающих на уровне инстанса ВМ. Либо нет смысла настраивать защищенное соединение по 443 порту, если сервер будет работать где то во внутреннем кластере, получая трафик только вышестоящего nginx, выполняющего роль балансировщика нагрузки.

Но давайте продолжим двигаться к нашей цели, а именно сервер для Flask приложения.

Настройка UFW

Не смотря на свое название Uncomplicated FireWall, это все таки не файрвол, а оболочка для стандартного для Linux брандмауэра iptables. А вот то, что он действительно Uncomplicated (несложный, незапутанный) это правда.

Начнем сначала. Проверим наличие ufw:

sudo uwf status

Если получили сведения о состоянии, например неактивен, то все в порядке, двигаемся дальше. Если нет - то его необходимо установить:

sudo apt install ufw

Тут мне кажется необходимо сделать важную оговорку - не торопитесь его активировать, поскольку вы потеряете возможность удаленного доступа к серверу. Сначала крайне важно убедится в корректности настроек правил фильтрации.

Поскольку в планах включить поддержку IPV6 на нашем веб-сервере, то нам нужно убедится, что в нашем файрволе она включена:

Для этого откроем файл с конфигурацией:

sudo nano /etc/default/ufw

Нас интересует первый же параметр. Убедитесь, что и у вас он имеет значение yes:

IPV6=yes

Сохраняем и закрываем файл. Если вы как и я используете редактор nano, то просто нажмите Ctrl+x

Итак, фильтрация.

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

DEFAULT_INPUT_POLICY="DROP"

Нас это не устраивает, поскольку необходимо разрешить как минимум доступ по SSH, HTTP и HTTPS.

Не торопитесь открывать файл конфигурации, он более нам не нужен. Мы сделаем это по другому.

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

Список доступных приложений можно посмотреть так:

sudo ufw app list

Если вы двигались вместе со мной, то вы скорее всего увидите записи для nginx и openssh, которые созданы автоматически при их обнаружении в системе.

Available applications:
 
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
  OpenSSH

Сами файлы лежат в отдельном каталоге, давайте посмотрим его содержимое:

ls -la /etc/ufw/applications.d

(Либо просто откройте его через MC)

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

Просмотрев любой из них, мы поймем, что синтаксис предельно прост:

[Nginx HTTP] - Название раздела. Это то, что нам покажет команда sudo ufw app list title= - Заголовок description= - Описание раздела ports= - Порты, которые необходимо открыть для сервиса

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

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

Итак, создадим пустой файл:

sudo nano /etc/ufw/applications.d/flask

И наполним его таким содержимым:

[Flask]
title=Flask server
description=Flask development server, do not use it on prodaction
ports=5000/tcp

Сохраняем, выходим. И давайте еще раз посмотрим на список приложений:

sudo ufw app list

И мы видим, что наше приложение Flask было добавлено в список доступных для фильтрации.

Available applications:
  Flask
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
  OpenSSH

Но следует помнить, что добавление приложения в эти файла, не включает доступ к нему автоматически, это делается отдельной командой:

sudo ufw allow 'название сервиса'

Так что давайте включим некоторые правила на нашем сервере:

Первое и самое важное, повторюсь - без него, при активации UFW мы потерям возможность удаленного доступа.

sudo ufw allow OpenSSH

Далее разрешим порты веб-сервера:

sudo ufw allow 'Nginx Full'

И наш тестовый Flask

sudo ufw allow Flask

Обратите внимание, там где в названиях присутствовали пробелы, я использовал одинарные кавычки.

Вот теперь мы можем включить наш файрволл:

sudo ufw enable

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

Удаляется правило аналогично:

sudo ufw delete allow 'название сервиса'

Либо можно получить нумерованный список правил:

sudo ufw status numbered

И удалить его по номеру:

sudo ufw delete номер

Целиком UFW можно отключить:

sudo ufw disable

В качестве бонуса.

Сброс настроек:

sudo ufw reset

Если не уверены в команде, добавьте добавьте этот ключ.

–dry-run

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

A - Запись

Или самая главная главная запись.

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

Тут все просто, открываете панель управления у вашего доменного регистратора, выбираете домен и жмете что то типа “добавить запись”, тип записи - А, значение - публичный IP. Убедитесь, что сделали его постоянным, иначе при следующем подключении вам его сменят.

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

Flask

И pyhon

Для тех кто не в курсе немного википедии:

Python - Высокоуровневый язык программирования общего назначения с динамической строгой типизацией и автоматическим управлением памятью… и т.д.

Flask - Фреймворк для создания веб-приложений на языке программирования Python, использующий набор инструментов Werkzeug, а также шаблонизатор Jinja2. Относится к категории так называемых микрофреймворков - минималистичных каркасов веб-приложений, сознательно предоставляющих лишь самые базовые возможности.

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

Давайте для начала проверим, вдруг на вашей ВМ не установлен python:

(А так вообще бывает?)

python3 -V

Если ответ будет: Python 3.x.xx - все в порядке, иначе его нужно установить:

sudo apt install python3

Так же нам понадобится менеджер пакетов:

sudo apt install python3-pip

Работает он по принципу apt, которым пользуемся для установки приложений на этом сервере, только внутри python. Синтаксис так же аналогичен. Не волнуйтесь, дальше все покажу.

Теперь нужно установить несколько инструментов для разработки:

(Точнее я уверен, что они уже стоят, но проверить это необходимо. Как я уже писал ранее, если пакет уже есть в системе - apt просто пропустит этот шаг.)

sudo apt install build-essential libssl-dev libffi-dev python3-dev python3-setuptools

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

build-essential - информационный список пакетов необходимых для dpkg

libssl-dev - пакет для работы с SSL и TLS

libffi-dev - интерфейс чужеродных функций. Позволит вызывать функции написанные на других языках.

python-dev - содержит все необходимое для компиляции модулей расширения

setuptools – стандартный способ создавать пакеты в Python.

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

sudo apt install python3-venv

Проект будет располагаться в домашней директории. (/home/’имя пользователя’), поэтому переймем туда и создадим папку для него:

(Если используете МС - просто нажмите F7.)

mkdir blueoctopus

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

Зайдем в созданный каталог

cd blueoctopus

И создадим виртуальное окружение:

python3 -m venv octopusenv

Через ключ -m мы вызвали модуль venv, который создал для нас изолированное окружение с именем octopusenv

И при помощи системной команды source загрузим его переменные. (Активируем виртуальное окружение)

source octopusenv/bin/activate

Обратите внимание на то как изменилось приглашение командной строки. Это означает, что мы “внутри” окружения для нашего проекта. Дальнейшую работу продолжим в нем.

На случай, если вам понадобится прерваться - отключить окружение можно командой:

deactivate

Только не забудьте его снова включить для продолжения настройки.

Для начала установим средство распаковки пакетов python:

pip install wheel

Дальше ставим Flask, и еще один элемент, без которого нам не обойтись:

pip install flask gunicorn

Обращаю внимание, что для двух предыдущих команд я воспользовался пакетным менеджером pip.

И так, gunicorn -

WSGI (расшифровывается как Web Server Gateway Interface — интерфейс шлюза Web-сервера) — это простой и универсальный интерфейс взаимодействия между Web-сервером и Web-приложением, впервые описанный в PEP-333

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

В итоге, цепочка прохождения запроса будет выглядеть как то так:

Интернет -> Nginx -> Gunicorn -> Flask + Python, и обратно.

Почти все необходимые элементы системы установлены, поэтому давайте попробуем настроить эту цепочку. Начнем, пожалуй с конца, а именно с простого Flask приложения.

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

nano main.py

И пишем следующий код:

# Импортируем класс Flask из пакета flask
from flask import Flask, render_template

# Импортруем модуль для работы с json
import json

# Создадим объект Flask
# и передадим ему имя пакета в качестве аргумента 
app = Flask(__name__)


# Воспользуемся поставляемыми flask декораторами route 
# для создания двух адресов на нашем сервере
#
# Первый, это просто индексная страница, которая будет
# передана пользователю, когда он введет адрес нашего сервера
# в адресной строке. Мы ему вернем произвольный index.html
# и код 200 "OK"
@app.route('/', methods=['GET', 'HEAD',])
def hello_world():
    return render_template("index.html"), 200


# Второй маршрут будет отвечать за POST запрос по адресу:
# blueoctopus.cc/api2 и возвращать сформированный json 
# Метод GET и 'Content-Type' я добавил для обработки
# пустых запросов из браузера
@app.route('/api2', methods=['POST', 'GET',])
def api_json():
    json_response = {"server_status": 201, "description": "Hello from python",}
    return json.dumps(json_response, indent=2), 201, {'Content-Type':'application/json'}

# Это условие гарантирует, что указанный после ного код
# будет выполнентолько при запуске как основная программа.
# При вызове метода run при импорте он вызван не будет.
# Это важно, потому что gunicorn будет импортировать его.
if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0') # 5000 port

Почти готово, остался index.html, которая должна быть показана пользователю при обращении на главную страницу сайта. По умолчанию Flask будет искать его в папке templates своей директории.

Создадим ее:

mkdir templates

И средствами SSH можно скопировать в нее любой статичный index.html

Если уж совсем никак не найти вам такой, то:

nano templates/index.html

И вставьте в него код ниже, должно сработать.

<!doctype html>
<html>
  <head>
    <title>Our Funky HTML Page</title>
    <meta name="description" content="Our first page">
    <meta name="keywords" content="html tutorial template">
  </head>
 
  <body>
    <div>
      Hello from Flask server    
    </div>
  </body>
</html>

По умолчаниюFlask использует, доступ к которому мы с вами уже открыли при настройке UFW, поэтому просто запустим его:

python main.py

И если мы видим что то похожее на это:

 * Serving Flask app 'main' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 186-312-395

То мы все сделали правильно и наше приложение работает. Если сейчас открыть главную страницу нашего сайта, то мы увидим наш index.html, а перейдя в раздел /api2 - красиво отрисованный json ответ.

Думаю стоит сделать некоторое уточнение. Я тут при записи видео столкнулся с тем, что точно не помню как передать порт в адресной строке :)

  1. Приложение запущено в режиме отладки на 5000 порту, и соответственно обратится к серверу мы должны с его указанием. Если явно не указать порт, то мы получит ответ по порту 80, который “слушает” наш Nginx

  2. У нас нет еще настроенного сертификата шифрования, и поэтому во многих браузерах это нужно указать явно.

Запросы в адресной строке в моем случае буду выглядеть так:

http://blueoctopus.cc:5000 - Индексная страница, с генерацией стандартного HTML ответа

http://blueoctopus.cc:5000/api2 - Тест ответа JSON

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

Gunicorn

По сути, это еще один веб сервер.

Где то выше я уже писал о предполагаемой схеме работы:

Когда Nginx решит, что запрос необходимо передать нашему Python приложению, то он его как раз направит на Gunicorn, который в свою очередь переведет его в понятный нашему приложению WSGI формат, запустит код нашего приложения, передаст этот запрос туда, потом заберет результаты, и вернет их обратно Nginx. Который, в свою очередь передаст результаты пользователю.

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

Если вы вышли из виртуального окружения нашего сервиса, то необходимо его опять активировать:

source octopusenv/bin/activate

Сейчас нам необходимо создать точку для входа или, если хотите, запуска нашего приложения для Gunicorn. Это будет еще один маленький файл на языке Python:

nano wsgi.py
# Импортируем наше приложение
from main import app

# И создадим параметр для запуска
# О значении данного условия я говорил выше.
if __name__ == "__main__":
    app.run()
# Обратите внимание, здесь команда без параметров,
# их мы передадим через командную строку

И конечно давайте запустим наш сервер приложения:

gunicorn --bind 0.0.0.0:5000 wsgi:app

Именно здесь мы передали хост и порт, которые должен “слушать” Guicorn. Мы ему как бы сказали: При поступлении запроса с любого интерфейса на 5000 порт запустить приложение app и передать его туда.

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

Все в порядке, остановим сервер - ctrl+x и закроем виртуальное окружение.

deactivate

Gunicorn установлен и работает, и сейчас мне бы хотелось просто сказать - а давайте “добавим его в автозагрузку”, но все не так просто. Думаю, что правильнее будет - теперь необходимо дать возможность системным службам управлять его состоянием, в том числе и запускать при старте системы.

Инициализацией и управлением службами на нашем сервере занимается давно ставшая стандартом подсистема systemd, именно для нее мы писали команды начинающиеся на systemctl.

Systemd запускает сервисы, которые описаны в его файлах конфигурации, которые называют юниты, расположены на в разных местах системы, в зависимости от метода установки. Нас интересует папка /etc/systemd/system/, куда помещают файлы созданные пользователем. (Ну хорошо, системным администратором).

Создадим такой файл:

sudo nano /etc/systemd/system/blueoctopus.service

Думаю, вас уже не удивит, если я скажу, что файл имеет такой синтаксис:

[Название секции]
имя_переменной = значение

Стандартный юнит должен содержать минимум три секции: [Unit], [Service], [Install], поэтому давайте их и опишем для нашего сервиса:

# Опишем что за сервис
[Unit]
# Описание
Description=Blueoctopus gunicorn instance
# Здесь мы говорим systemd запускать наш сервер только после загрузки сетевых служб. 
After network.target

# Уже описывает наш юнит для системы.
[Service]
# От чьего имени запускать сервис
# Это я
User=max
# Группа созданная для Nginx
Group=www-data
# Где его запускать
WorkingDirectory=/home/trash/blueoctopus
# Путь к виртуальному окружению, в котором его запускать
Envirovement="PATH=/home/max/blueoctopus/octopusenv/bin"
# Команда запуска сервиса с параметрами, о ней ниже
ExecStart=/home/max/blueoctopus/octopusenv/bin/gunicorn --workers 2 --bind unix:gunicorn.sock -m 007 wsgi:app

# Здесь опишем на каком уровне стартует наш сервис
[Install]
# Он будет соостветсвовать стандартному серверному (Runlevel 3)
# Многопользовательский режим с поддержкой сети, но без графического интерфейса.
WantedBy=multi-user.target

Команда запуска:

/home/max/blueoctopus/octopusenv/bin/gunicorn - Полный путь к исполняемому файлу.

–workers 2 - Количество процессов запускаемых сервером, я поставил два, по количеству ядер процессора на нашем сервере.

–bind unix:flaskproject.sock - поскольку все в Linux представляется как файлы, то и точку взаимодействия между Nginx и gunicorn мы опишем как специальный тип файла .sock (Вообще через этот файл кто угодно может взаимодействовать с сервером приложений, но в нашем случает это будет именно Nginx).

-m 007 - маска прав доступа к создаваемому файлу написанная по стандарту umask. В данном случае дает полный доступ указанному пользователю и группе, и запрещает всем остальным (u=rwx,g=rwx,o=)

wsgi:app - ну и наконец код, который должен запустить gunicorn. Это приложение app импортированное в файле wsgi.py

Надо бы еще написать команду для перезапуска сервиса, но возможно я это сделаю в следующей версии данной инструкции.

Команды управления стандартные для systemd:

Запустить:

sudo systemctl start blueoctopus

Посмотреть статус:

sudo systemctl status blueoctopus

И вот теперь мы добавим его в автозагрузку:

sudo systemctl enable blueoctopus

Nginx proxy config

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

Напомню, что мы создали unit - файл для gunicorn, в котором указали некой точкой связи с nginx служебный файл gunicorn.sock. Теперь он при старте системы автоматически загружается и ожидает появления в нем входящих запросов.

Теперь нам необходимо сконфигурировать nginx, что бы он так же передавал в этот системный файл (сокет, если хотите) входящие запросы. Давайте взглянем на его основной конфигурационный файл:

nano /etc/nginx/nginx.conf

Либо, традиционно открыть расположение используя Midnight Commander (mc)

sudo можно не использовать, поскольку пока никаких правок вносить не планируется. Давайте просто убедимся, что в конце модуля http есть такие строки:

		##
		# Virtual Host Configs
		##

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;

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

На месте

include /etc/nginx/sites-enabled/*;

будут подставленный файлы из папки sites-available, на которые ссылаются ярлыки из папки sites-enabled. Но поскольку эта часть предполагает настройку нескольких доменов на одном сервере, чего не предполагает этот проект - мы ее использовать не будем, и даже удалим единственный ярлык @default из sites-enabled.

Нас интересует часть со строкой:

include /etc/nginx/conf.d/*.conf;

Это, так называемые drop-in файлы, которые, как мы помним из части про юниты, не только расширяют конфигурацию, но и переопределят параметры, указанные в основном файле. То есть, нам вообще не надо вносить никаких изменений в оригинальном файле, а достаточно еще раз указать параметр с требуемым значением здесь, и он будет учтен системой.

В этом есть еще один плюс - что бы вернуть систему в дефолтное состояние достаточно будет просто удалить файл из conf.d директории.

Этот вариант в данном случае, нас устраивает, поэтому давайте создадим новый файл конфигурации:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    # server_name your_domain www.your_domain;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/trash/flaskproject/flaskproject.sock;
    }
}

Сайт будет один, потому функционал с симлинками использовать не будем.

Симлинк в sites-enabled для default домена также удалим.

И немного магии: надо в nginx.conf раскомментировать строку

server_names_hash_bucket_size 64;

UFW

Теперь не забыть убрать проброшеный 5000 порт в файрволе

sudo ufw status
sudo ufw delete allow 5000

letsencript

Теперь займемся защитой. (Не обязательный пункт)

Ставим сертификат от letsencript. Рекомендуется ставить с помощью специального бота:

ссылка

https://certbot.eff.org/

Раньше бота можно было ставить напрямую из репозитория

sudo apt install certbot certbot-python-nginx

Но сейчас так не работает, поскольку все переехало на снап.

(Хорошо это или плохо, тема отдельного холливара)

https://snapcraft.io/ - это для убунту

https://flathub.org/home - для гнома

В общем ставим

Проверим наличие Snap:

sudo snap

Если нет - ставим.

https://snapcraft.io/docs/installing-snapd

sudo apt install snapd

Установим самого бота:

sudo snap install --classic certbot

И пробросим символьную ссылку:

sudo ln -s /snap/bin/certbot /usr/bin/certbot

Автоматический режим установки сертификатов может не помочь, будем ставить вручную.

(Это удобно, может у вас еще откуда есть сертификаты.)

sudo certbot certonly --nginx

ключ certonly говорит только загрузить сертификаты.

По умолчанию сертификаты залиты в:

/etc/letsencrypt/

Для проверки возможности обновления сущеуствует ключ

(Без самого обновления)

sudo certbot renew --dry-run

Сами сертификаты лежат в ../live/host-name

Nginx и https

Допишем секцию server в нашем конфиге, теперь он выглядит так:

server {
    listen 80;
    listen [::]:80;

    # server_name your_domain www.your_domain;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/trash/flaskproject/flaskproject.sock;
    }

}

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name flaskserver.deadend.xyz;

    #ssl on;
    ssl_certificate /etc/letsencrypt/live/flaskserver.deadend.xyz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/flaskserver.deadend.xyz/privkey.pem;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/trash/flaskproject/flaskproject.sock;
    }

}

Далее опять запускаем запускаем тест конфигурации:

sudo nginx -t

И если нет ошибок:

sudo systemctl nginx reload

теперь откроем в браузере и увидим, что работает все на https

далее надо решить с 80 портом. Закрывать не вариант, так что делаем переадресацию

Опять идем в кофиг сайта на nginx

теперь он должен выглядеть так

server {
    listen 80;
    listen [::]:80;
    # server_name your_domain www.your_domain;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/trash/flaskproject/flaskproject.sock;
    }

}

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name flaskserver.deadend.xyz;

    #ssl on;
    ssl_certificate /etc/letsencrypt/live/flaskserver.deadend.xyz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/flaskserver.deadend.xyz/privkey.pem;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/trash/flaskproject/flaskproject.sock;
    }

}

Далее опять запускаем запускаем тест конфигурации

sudo nginx -t

И если нет ошибок:

sudo systemctl nginx reload

После чего в браузере открываем ссылку http и видим переадресацию на https

Автоматическое обновление сертификата

Осталось настроить автоматическое обновление сертификатов

В “ручную” обновляется командой:

certbot renew

для автоматизации воспользуемся cron`ом.

sudo crontab -e

Стоит уточнить, что крону нужны полные пути.

30 2 15 * * /usr/bin/certbot renew >> /var/log/renew-ssl.log

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