Я люблю начинать разные сторонние проекты, считаю, что это один из лучших способов узнавать что-то новое и по-настоящему стоящее. И у меня есть один серьезный недостаток — я почти никогда не довожу дела до конца. Речь, конечно же, не идет о проектах по учебе, за которые мне поставят оценку или задачам, поставленным работодателем. Я про собственные идеи, которыми загораюсь вне постоянной работы или учебы. Каждый раз, когда я осваиваю какой-нибудь совершенно новый навык, который считаю востребованным и не вижу перспективы еще что-нибудь такое изучить — я забиваю на проект совсем. Но в этот раз решил исправиться — начать проект, завершить его и поведать о пути, который я прошел.
Совсем немного о себе
У меня профильное образование, бакалавриат на математическом факультете, магистратура в IT. На удивление, довольно много чего, что я освоил в универе, мне пригодилось, особенно магистратура. Во всяком случае, мне удалось настроить
Пока учился, я был фанат C++ и гейм-дева. Разработал тогда самописный движок и писал на нем игры, пытался. Диплом у меня был на Си и связан с использованием графических процессоров в вычислениях. Потом заинтересовался Java и разработкой под Android. Выполнил на этом языке несколько учебных проектов, а затем в рамках студенческого проекта, при поддержке одной международной корпорации, писал консольную утилиту для анализа производительности программ. Все это, затем, выросло в приложение под Android с возможностью протестировать свой телефон с точки зрения производительности и сравниться с другими.
На горизонте меня ждал мир python. Неожиданным образом, с подачи моего доброго друга, заинтересовался Data science и python, это было больше четырех лет назад. Так совпало, что мне пришлось поменять работу и мои навыки оценили подходящими, чтобы начать заниматься бэкендами для внутренних аналитических сервисов. Так, через анализ данных я пришел к веб-разработке на этом языке.
По долгу службы приходилось быть и fullstack-разработчиком, когда настоящие frontend-разработчики были чрезвычайно заняты или когда стандарты качества UI не на первом месте.
Это описание пути, который пришлось пройти для создания собственного полноценного веб-приложения без полноценного погружения в технические детали. По ходу будут скидываться ссылки на коммиты из моего репозитория. Коротко об этапах этого путешествия:
- Определиться с тематикой приложения и стеком технологий
- Сверстать первоначальный UI
- Разработать бэкенд
- Реализовать управление пользователями и разделение прав
- Провести рефакторинг, реализацию недостающих базовых функций, CI
- Сформировать настройки для production-окружения
- Развернуть в production-окружении
- Зарегистрировать домен и настроить сертификат для доступа по https
Получилось полноценное веб-приложение на платформе AWS. Кому все еще интересно, прошу за мной.
Определиться с тематикой приложения и стеком технологий
Выбор тематики приложения — отдельная история, окончательный выбор пал на отслеживание привычек. На главной странице должен был располагаться набор кнопок с отслеживаемыми привычками. Сделали действие — нажали на кнопку — и так каждый день. Данные должны сохраняться и на отдельной странице отображаться в виде таблицы, привычки в строках, а календарные дни в столбцах, закрашенная ячейка говорит о том, что требуемое действие в этот день было выполнено. Как в простейшем бумажном трекере привычек.
Технологический стек для меня был очевиден: react, django, postgres, nginx, uwsgi.
Сверстать первоначальный UI
Начать решил я с пользовательского интерфейса и установил nodejs.org/en/download/package-manager, github.com/facebook/create-react-app, затем создал проект:
npx create-react-app easytrack
И приступил к верстке. С самого начала не придумал ничего проще, чем захардкодить список объектов бизнес-логики прямо в программе и выводить их в виде списка в тэге ul. Этими объектами стали: тематическая группа, отслеживаемый элемент, сами записи об отслеживании конкретного элемента в конкретный день.
На первой странице у меня были тематические группы, а по клику на любую из них открывался список элементов, которые можно отслеживать.
Сверстал и другую страницу, которая содержала в себе простейшую таблицу со статистикой за последние дни.
Разработать бэкенд
На данном этапе возникла потребность в бэкенде. Он нужен, чтобы сохранять объекты в базу данных, управлять пользователями, разграничивать права. Django уже приходилось использовать для собственных проектов (не доведенных до логического завершения, конечно же), но была одна сложность — нужно было задействовать Django Rest Framework, с которым я дел никогда не имел вообще. Все спасибо книге Building Django 2.0 Web-applications [1]. Я прошел ее от корки до корки, кроме последней главы, где создавали API на DRF, ее я бегло глазами пробежал. По ходу повествования я еще не раз к ней обращусь.
Деваться некуда, открыл документацию www.django-rest-framework.org и начал ее курить, а также обратился к упомянутой книге.
Установил и активировал виртуальное окружение, установил Django и в корне проекта создал django-проект:
virtualenv venv
. ./venv/bin/activate
pip install django
django createproject config
Главную папку переименую в django, чтобы по названию сразу было понятно все про ее содержимое. Внутри нее будет python-модуль под названием config, что тоже весьма удобно. Код фронтенда завернул в папку react. Так сложилась общая структура папок, которая сохранилась и до сих пор (посмотреть на Github).
В корне проекта создал главное приложение:
django createapp core
Создал классы моделей бизнес-логики ровно в том же формате, в каком я захардкодил их во фронте, сериализаторы и представления (views) (ссылка на коммит в Github).
С базой данных не заморачиваемся и используем предлагаемый по-умолчанию SQLite. Через админку залил туда парочку тестовых образцов данных, которые я захардкодил ранее во фронте. Пробуем подключить к фронтенду. python3 manage.py runserver в одной вкладке, yarn start в другой и погнали.
Разработческий сервер React крутится на 3000 порту, а Django на 8000. Нет ничего проще, чем писать во фронте запрос fetch('http://localhost:8000/...'). Но так не сработало из-за cors-origin — специальной защиты, которая абы какому сайту не дает делать автоматические запросы на абы какой сервер. Поэтому, недолго думая, встроил в бэкенд django-cors-headers), настроил его — все заработало. Лишь потом догадался в package.json добавить секцию proxy и указать на бэкенд, тогда fetch('/api/v1/...') начали нормально работать и никаких лишних настроек больше не понадобилось.
По первости это было страшно наивно, потому что асинхронные запросы я делал в конструкторе — у меня все работало и ладно. Лишь потом я узнал про методы жизненного цикла, где можно, а где не стоит делать такую работу. Сейчас элементы отображались, новые элементы можно было создавать.
Реализовать управление пользователями и разделение прав
На данном этапе не хватало только разделения прав на элементы данных: все они создавались от имени анонимного незалогиненного пользователя. Мне надо было не нарушая экосистему django как-то встроиться в нее.
Для начала я на фронтенде захардкодил логин/пароль и формировал заголовок Authorization со значением 'Basic ' + base64.encode(username + ":" + password). Потом думал генерировать строку, что в формате base64 и сохранять на клиенте при вводе логина/пароля. Но по поводу этого решения были большие сомнения в плане безопасности, хотелось попробовать что-нибудь другое.
Недолго порылся в интернете и узнал о технологии JWT и модуле django-rest-framework-simplejw, который предоставлял authentication classes для DRF и представления (views) для получения пары токенов и для обновления access-токена.
Со стороны фронтенда достаточно было в localStorage сохранять пару токенов и передавать заголовок Authorization со значением «Bearer access-token». В завершение, сверстал страницы для логина и закрыл все доступы для неавторизованных пользователей на сайт с помощью permission classes (ссылка на коммит Github). На фронте, если не было refresh-токена, перенаправлял на страницу логина (ссылка на коммит Github).
Затем, сделал возможность регистрации на сайте, создал на бэкенде представление, сверстал страницу, настроил посылку запросов. В дальнейшем моей мечтой было написать активацию аккаунта по почте.
Провести рефакторинг, реализацию недостающих базовых функций, CI
В этот момент получилось минимально юзабельное приложение и мне захотелось собрать обратную связь. Показал приложение жене и попросил попользоваться без единой подсказки с моей стороны. Регистрация прошла вполне успешно, но дальше все пошло не совсем так, как я думал. Тут я понял, что тематические папки не должны быть во главе угла, а должны быть вспомогательным инструментом, а также нужно добавить подсказок, что мы создаем набор кнопок, чтобы их раз в день нажимать. Она ожидала, что это будет похоже на обычный бумажный трекер привычек с таблицей, где надо закрашивать ячейки. Когда я это понял, я приступил к рефакторингу. И кроме этого было, что рефакторить. Я перебрал структуру модулей почти с нуля.
К этому моменту я решил наводить красоту, css стили и адаптивную верстку. В этом я не особо силен и уповал на CSS-фреймворки. Выбор пал на Bulma, опираясь на количество звезд, скачиваний с npmjs.com, при том, что брать Bootstrap мне не хотелось. Худо-бедно я с этой задачей справился.
Параллельно улучшал функции бекенда. Сделал полноценный CRUD. Сбылась и мечта по подтверждению регистрации через почту. Я разобрался с функциями отправки писем, обрел возможность отлаживать все через отладочный мейл-сервер.
python -m smtpd -n -c DebuggingServer localhost:1025
Для стейджинга завел мусорный гугл-аккаунт и сумел наладить отправку писем через гугл-почту.
Что касается тестового покрытия для бекенда — все получилось вполне себе неплохо, а для фронтенда все совсем вяло до сих пор. Но я не отчаиваюсь, вдруг потом получится наладить.
Следующим этапом надо было наладить CI, я завел GithubActions для запуска тестов, воспользовался конструкторами конфигурации для запуска, немного ее подправил и готово.
Сформировать настройки для production-окружения
После недолгого приведения кода и мелких деталей логики в порядок мне надо было начать формировать production-конфигурацию. Для этого вдохновился все той же книгой [1]. Файлы django-конфигурации, python-зависимости я разбил на 3 части (ссылка на коммит Github):
- common — все самое необходимое для любой среды
- dev — отладочные инструменты и настройки
- prod — здесь отключена любая отладка и большая часть параметров передается через переменные окружения
Далее надо было задуматься над тем, как это все будет запускаться.
Я локально запустил uwsgi, nginx и правильно их сконфигурировал, чтобы написать dockerfile, в котором все это будет запускаться и перенести конфигурацию в контейнер.
В книге [1] не придерживались концепции «1 процесс — 1 контейнер», вот и я не стал, воспользовался тем, что там уже было. В качестве базового образа я выбрал phusion/baseimage, который представляет собой видоизмененный образ Ubuntu, в котором правильно запускаются нужные службы и можно безопасно стартовать несколько процессов.
Наконец, это заработало локально, я поднял локально базу данных postgres, достаточно мучительно настроил в нее доступ с контейнера и после всего поднял контейнер. Ура (ссылка на коммит Github).
Развернуть в production-окружении
Я никогда не работал с публичными облаками, но я прочитал книгу [1], в которой приводилось описание развертывания приложения в AWS, до этого я даже много раз слышал об этом. Для меня это был страшный темный лес, какие-то сервисы. Действия, которые я тогда совершал, совершались бездумно, просто копипаста команд в надежде, что потом осознание придет. А еще неведомый Free Tier, который позволял пользоваться сервисами бесплатно, который мне был очень сильно нужен, но как он работает? Все это предстояло узнать.
Оказывается, такая большая экосистема AWS разбита на множество довольно-таки небольших сервисов, которые могут весьма слаженно работать. Но не нужно знать их все, достаточно выделить для себя главные и приступить к их освоению. Мое приложение задействует некоторые из них, и я начал с ECS.
Elastic Container Service
Я увидел, что конструктор Github Actions позволяет создать конфигурацию для задачи непрерывного развертывания контейнеров на AWS ECS. Допустим, я начал вникать в этот сервис и понял, что в тамошней консоли нужно создать кластер, создать определение задачи и описать контейнер, предварительно сохранив его образ в еще одном сервисе AWS ECR, который по своей функции является Dockerhub. В консоли было предложено 2 типа кластеров: Fargate и EC2. Первая технология полностью бессерверная, подразумевается, что мы просто запускаем контейнер и обо всем заботится среда исполнения. Контейнеры в кластере второго типа крутятся на собственном экземпляре виртуальной машины в облаке. Не желая долго погружаться в это, создал кластер на основе Fargate. Но я столкнулся с тем, у меня не получалось передать секретные значения в контейнер, из-за этого задача постоянно падала.
Пока я пытался привести контейнер в работоспособное состояния, кластер работал уже несколько часов и мне во вкладке с оплатой услуг накапали деньги. Сама по себе технология оказалась платная и на нее не распространяется Free Tier. Я погасил кластер и с оплатой решил разобраться чуть позже. Прочитал в документации, что за использование ECS поверх EC2 не надо доплачивать, кроме использования ресурсов EC2, но на EC2 распространяется Free Tier и я решил попробовать этот путь.
Поддержка AWS
В итоге кто-то мне соврал и за эту конфигурацию у меня сняли еще немного денег. Суммарно $0.32, пусть немного, но все равно обидно.Тут я написал в поддержку, сказал, что у меня были сложности с настройкой и все время думал, что укладываюсь во Free Tier, но так ничего у меня и не получилось, а денег сняли. Попросил помочь. В ответ на мой призыв радостно откликнулись, дали ваучер на $1, который покрыл все неожиданные расходы. Приятно.
В итоге пришлось забыть про этот контейнерный сервис и осваивать EC2 в чистом виде.
Elastic Computing Cloud
EC2 — сервис для создания виртуальных машин в облаке. Free Tier позволяет использовать 750 часов работы этих машин в месяц. Этого с лихвой хватает, чтобы одна, пусть и довольно-таки слабая, виртуальная машина работала круглые сутки.
В книге [1] описывается работа docker-machine, через который можно создавать виртуальные машины от разных поставщиков. Например, для него есть драйвер EC2. После создания машины она записывается в окружение терминала и все операции docker будут выполняться на удаленной машине.
Таким образом, с помощью команды dockre-machine create… мы создаем экземпляр EC2. А docker-compose up -d позволяет запустить контейнер на этом экземпляре. Остается не забыть разрешить входящие подключения к порту 80 с любого адреса. С этого момента, если зайти по адресу указанном в поле «Public DNS» экземпляра контейнера, то можно было попадать на сайт.
Relational Database Service
База данных тоже живет на серверах AWS и задействует сервис RDS. По сути это управляемый сервер базы данных. Его достаточно создать, указав тип базы данных (у меня postgres) и то, что собираюсь использовать Free Tier, при этом можно почти не трогать больше никакие настройки. А затем использовать имя хоста, пользователя, пароль и имя базы данных для выполнения подключений.
Simple Email Service
Пришлось столкнуться еще и с тем, что письма не отправлялись. Дело в том, что все почтовые сервисы банят айпишники, которые соответствуют экземплярам EC2 в целях противодействия спаму. Нужно было непременно пользоваться SES. Еще один сервис AWS.
Его я тоже настроил, указал почтовые ящики, с которых собираюсь слать письма, все хорошо. Однако до сих пор не все так радужно. Новые аккаунты по-умолчанию в режиме песочницы, который не позволяет слать письма на неподтвержденные почтовые ящики. Пришлось подтвердить ящики для моих бета-тестеров.
Зарегистрировать домен и настроить сертификат для доступа по https
Чтобы полноценно тестировать приложение требовалось было настроить соединение https, а это оказалось невозможным без регистрации домена. Не хотелось подвергать риску почтовые адреса, пароли и токены, передавая их через незащищенное соединение. За этим делом я обшарил весь интернет. Какие-то регистраторы не устраивали, потому что дорого, а где-то прям очень дорого, к тому же, у многих просто ужаснейшие отзывы. Пока искал, рекламная сеть яндекса поняла чего я хочу и тут же предложила мне фирму где можно зарегистрировать домен за 39 рублей. Об этом сервисе все-таки отзывов было побольше, поэтому я создал аккаунт, выбрал свободный домен и мне его зарегистрировали.
Затем, чтобы доступаться на мой экземпляр EC2 по этому домену, создал в сервисе AWS Route 53 зону
Для создания сертификата я использовал сервис Let's Encrypt. Остановился на такой конфигурации: локальный nginx делает proxy_pass на адрес контейнера, контейнер крутится и доступен через порт 8080. Не возникло сложности воспользоваться консольной утилитой certbot, сгенерировать сертификат и автоматически настроить https. Наконец, разрешить доступ на виртуальную машину через порт 443.
Разработка проекта подошла к логическому завершению
Сама разработка еще не закончена и много чего придется доработать, чтобы пустить туда настоящий трафик. Например, хотя бы, вывести SES аккаунт из песочницы, чтобы получать письма мог любой. Однако, сделать этого мне не дали. Я послал заявку, расписал, что это сервис, условно, для своих, но мне ответили, что то, как я собираюсь использовать сервис, может плохо повлиять на функционирование сервиса и создать риск прерывания в обслуживании для других клиентов. Ну ладно, буду развивать сервис, потом получится. Что еще:
- Настроить мониторинги
- Реализовать настройки в профиле пользователя, чтобы можно было менять пароль, почту и т. д.
- Провести нагрузочное тестирование
- Подготовиться к различного вида атакам
- Предусмотреть возможность сбоев и отработать аварийно-спасательные действия
- Лучше позаботиться об инфраструктуре, чтобы она могла держать трафик
- Придумать правила использования и легализовать прием и обработку персональных данных пользователей
- Более тщательно подумать о ценности программного продукта
- Наконец, что-то сделать с фирменным стилем
В качестве итога — я довел проект до завершения, хотя добился образовательных целей гораздо раньше, чем можно было говорить о завершенности. Я изучил React и даже сумел подхватить пару срочных тасочек на основной работе, я познакомился и стал глубже понимать AWS, благодаря этому осознал важность мониторингов, например, а также стал лучше понимать реальную инфраструктуру нашего рабочего проекта.
Но главное осознание — один в поле совсем не воин. Я работал над этим проектом больше двух месяцев помногу часов в свободное от работы время… И знаете что? Я так страшно еще не зашивался. Становится заметно, что чем старше становишься, тем меньше желания учить что-то «просто так», меньше желания много работать над какими-то вещами, которые «просто интересны». Но при этом замечаешь, что появляется больше доступных возможностей делать что-то во благо, на пользу. Начинаешь ценить сфокусированные усилия, работу в команде, а также отдых, время, проведенное с близким окружением.
Ссылки
Автор: Evgene Petrenko