Недавно выдалось свободное время и я сделал простой проект про specialty-кофейни на Кипре: сайт и телеграм-бот по всем канонам "большой" разработки. Люблю хороший кофе ????
Делюсь своим процессом разработки и рекомендациями как сделать всё задуманное без потери времени.
Цели проекта изначально простые:
карта кофеен на сайте
просмотр подробностей о кофейне
переход в большую карту или приложение Goolge Maps для маршрута, отзывов и т.д.
список кофеен в боте
поиск кофейни по названию в боте
поиск ближайшей кофейни в боте
случайная кофейня в боте
всё это в минимальном и понятном стиле
Кроме того - не уходить в перфекционизм и бесконечную разработку.
Вот это, на самом деле, очень сложно, потому что хочется сделать лучше, чем у других, обвеситься бэйджиками всевозможных линтеров и выразить в коде все свои таланты. Хотя достаточно, чтобы сервис просто работал, метрики считались, а ошибки логгировались. Это стоит тех самых 20% усилий, которые принесут 80% результата.
Поэтому декомпозируем всё на мелкие задачи, выкидываем всё длиннее нескольких часов и то, что невозможно оценить.
Честно говоря, я срывался несколько раз: первый раз зацепился за идею запустить сервер Caddy без конфига из консоли примерно caddy php_fastcgi 9000
, но увы, так можно запускать только обратный прокси и/или файл-сервер; потом ещё добавились хитрые редиректы; в общем, убил на это 2 дня.
Ещё день пришлось потратить на неудачно выбранную библиотеку для Telegram-бота.
В целом всё удалось, код проекта открыт, велкам в пул-реквесты. Адрес сайта - в конце статьи, чтобы меньше походило на рекламу.
Для достижения целей было решено реализовать REST API микросервис, фронтэнд-сайт (подробнее во второй части) и бэкэнд для бота (подробнее в третьей части). Деплой - на что-нибудь современное управляемое с free tier, а не VPS или shared-хостинг. Бот вообще хорошо ложится на идеологию Serverless/FaaS.
Первым шагом я зарегистрировал домен: 20 евро из собственного кармана - хорошая мотивация не потратить их впустую реализовать задуманное.
К слову про регистрацию: сложно предугадать, как сложится судьба проекта: возможно, получится выгодно продать, а, возможно, захочется избавиться от него без следов. Поэтому лучше регистрировать все внешние сервисы на отдельные аккаунты: почта, домен, хостинг, аналитика, мониторинг и т.д. Дополнительно получится использовать free tier'ы и триалы.
REST API микросервис
У меня есть хороший опыт работы с Laravel и Symfony, поэтому для быстрой реализации я выбрал знакомую и лёгкую технологию. Потом обязательно перепишу на Go. А использование свежей версии PHP 8.1 позволило написать чуть меньше кода и получить чуть выше производительность. Promoted properties, readonly и строгая типизация сильно облегчают разработку.
Для бОльшего облегчения из Laravel удалены неиспользуемые пакеты и сервисы: получился почти Lumen.
В composer.json можно отметить пакеты как "установленные" и они не будут устанавливаться на самом деле. Очень удобно для выпиливания избыточных полифилов, например, так:
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-intl-grapheme": "*",
"symfony/polyfill-intl-idn": "*",
"symfony/polyfill-mbstring": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"dragonmantank/cron-expression": "*",
"egulias/email-validator": "*",
"league/commonmark": "*",
"league/flysystem": "*",
"symfony/mime": "*",
"symfony/var-dumper": "*",
"tijsverkoyen/css-to-inline-styles": "*"
}
Ещё можно отключить platform-check, чтобы не проверять версию PHP при каждом запросе, и ограничиться проверкой при установке пакетов. Также полезно включить classmap-authoritative, чтобы классы загружались только из созданной composer'ом карты, а не из каждого use, но это будет мешать разработке, поэтому достаточно включить при деплое.
Итоговый composer.json и config/app.php. На подобную оптимизацию ушло менее часа, поэтому ok. Но более глубокая оптимизация потребует гораздо больше времени, так что не сейчас.
Архитектура
Сервис построен на single-action контролерах, которые выбирают данные из моделей. Репозитория нет, поскольку считаю его избыточным для простейших запросов без дополнительной логики.
Входные данные валидируются отдельными Request'ами, выходные - заворачиваются в GeoJson Resourse'ы. Один класс - одна ответственность.
На момент разработки фронтэнда был всего один endpoint /cafes со списком всех кофеен: это позволило быстро запустить API и не мокать его для других частей проекта. Во время разработки бота я добавил ещё несколько endpoint'ов.
БД
В качестве БД для начала используется SQLite - это позволило не тратить время на развёртывание классических MySQL/PostgreSQL. Более того, я уверен, что при нагрузке в 100 посещений в день и нескольких десятках или сотнях записей в таблицах, SQLite - отличное решение для микропродакшена.
Сидирование данными производится из обычного массива в database/seeders/CafeSeeder.php в процессе деплоя. В дальнейшем планирую сделать 1-2 консольные команды для редактирования данных, потому что гораздо быстрее, чем любая визуальная админка.
Поиск
API умеет в полнотекстовый поиск благодаря Scout c драйвером "collection": он позволяет искать по каждому полю модели обычным "LIKE %smth%" запросом и не требует полнотекстовых индексов в БД. Подключение заняло 15 минут, поэтому ok.
Статика
В сервисе есть немного обязательных статических файлов:
robots.txt с запретом индексации
favicon.ico, который любят многие сервисы
humans.txt
и т.д.
Тесты
В качестве отдельных Feature-тестов используются http-request'ы в PHPStorm по всем endpoint'ам с правильными и ошибочными данными. Проверка - глазами, но в CI не положишь ¯_(ツ)_/¯
Написание нормальных тестов - первоочередная задача и я планирую использовать для этого Pest https://pestphp.com/.
Конфигурация
Laravel, в отличии от Symfony, не читает конфигрурацию из файла .env.local чтобы переопределить/дополнить конфигурацию из .env, да и вообще не рекомендует хранить .env в репозитарии. Это хороший подход, но он не очень удобен, когда параметров конфигурации много.
Можно сделать чуть иначе: записать локальные параметры в .env (и не добавлять его в репозиторий), а все боевые параметры и названия параметров-секретов - в .env.production и добавить его в репозиторий. Параметр APP_ENV=production, а также сами секреты необходимо записать средствами хостинга и/или деплоя.
В таком случае .env.local заменит (не дополнит!) конфигурацию из .env.production, а перечисление всех используемых параметров (даже без значений) в .env.production упростит понимание проекта. Бесполезный, в данном случае, .env.example удаляем.
Мониторинг
По окончании первого этапа разработки добавляем в проект Sentry: в .env.production достаточно указать пустое значение SENTRY_LARAVEL_DSN (для наглядности), а фактическое значение записать в секрет.
Деплой
Для размещения сервера используется платформа Fly.io с управляемыми microVM Firecracker. В отличии от популярного Heroku, никогда не спит, имеет хороший free tier и позволяет разместить как статику, так и любой сервер приложений. Кроме того, есть различные стратегии деплоя и отката изменений, health check'и и география размещения на выбор.
Настроить среду выполнения можно автоматически командой flyctl launch из каталога приложения - сервис сам определелит необходимые компоненты и соберёт конфиг fly.toml и Dockerfile. Либо можно написать свои конфиг и Dockerfile.
У меня уже был Dockerfile для подобных проектов, поэтому использовал его. Бонусом получилось запустить все сервисы под непривилегированным пользователем.
В относительно свежих версиях Docker реализовано кеширование слоёв, поэтому нет смысла писать все инструкции в одной команде RUN. Наоборот, лучше размещать "тонкие" слои RUN и COPY в порядке увеличения частоты изменений данных в них.
Например, дистрибутивы и пакеты ОС меняются редко, поэтому слой RUN apk add ...
может быть в самом начале Dockerfile.
Пакеты composer обновляются чаще, но не так часто как исходный код проекта, поэтому слои COPY composer.* .
и RUN composer install --no-autoloader --no-dev --no-interaction --no-scripts
могут быть указаны в середине Dockerfile и смогут браться из кеша.
Ну а COPY --chown=www-data:www-data . .
, RUN composer dump-autoload --classmap-authoritative --no-interaction
и возможные другие команды, затрагивающие исходный код проекта могут быть в самом конце и будут выполняться, только если изменится сам код проекта, а не пакеты ОС или зависимости composer.
Итоговый Dockerfile.
Внутри контейнера находится классический PHP-FPM на Alpine и проксирующий сервер Caddy. Он чуть легче и проще в настройке, чем привычный Nginx и состоит из одного бинарника и одного необязательного конфига.
{
admin off
auto_https off
log
skip_install_trust
}
{$APP_URL_INSECURE}:8080 {
root * /var/www/html/public
php_fastcgi 127.0.0.1:9000
file_server
encode gzip
header -Server
}
:8080 {
respond 404
header -Server
}
Тут "file_server" отвечает за раздачу статики.
Платформа Fly.io сама терминирует https и управляет сертификатами, поэтому приложению в контейнере достаточно обрабатывать обычный http-трафик.
Главный минус при размещении PHP-приложений - необходимость использования прокси-сервера или балансера, их совместный запуск и работа. В данном случае пришлось использовать достаточно тяжёлый supervisor, который тянет за собой Python. Но это быстрое и работающее решение, позволяющее не застревать в разработке и настройке.
Итоговый supervisord.conf
CI/CD
Тут всё просто: Github Action из одного workflow и тот же самый flyctl.
Благодарности
Спасибо @kvasilenkoза код-ревью проекта.
На этом этапе микросервис API работает, размещён в production-окружении и доступен всем пользователям. План-минимум выполнен :-)
Репозиторий API, сайт https://specialtycoffee.cy
Во второй части расскажу про создание фронтэнда и в третьей - про телеграм-бота.
amarao
А на выходе пустая страница, если ничего не подгрузилось с гугла. :-/
mvs Автор
Вроде всё ok, написал в личку