Открытый канал ID 70941834150135

Махровый fullstack

А у нас в канале:

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

Постится как душе просится.

Освежает разум. Возможны побочные эффекты.

Запаски:
- https://vk.ru/ordo_code
- https://t.me/ordo_code

Каналов в рубрике
1 736
Место этого канала
№ 1 236
Открыть рубрику →
Каналов в рубрике
649
Место этого канала
№ 471
Открыть рубрику →

Публикации за период

По реакциям

Поиск и вся лента — после бесплатной регистрации. Свои даты — по подписке; владелец канала видит их бесплатно. Сортировка по реакциям — по подписке. Зарегистрироваться

Загрузка…

Тысяча и одна ошибка

— Не будь как Шазерезада - экономь квоту —

TL;DR
Одна ошибка может случиться тысячу раз, но в Bugsnag достаточно сообщить о ней один раз за 15 минут.
Кто о чём, а вшивый о багах.

Подключил я намедни проект Laravel к Bugsnag для сбора ошибок в коде, дабы не проспать чего апокалиптического. Но апокалипсис устроил сам... 😈

Проект ворочает довольно приличными объёмами данных и в случае катаклизма Bugsnag быстро бы оброс репортами.

Сказано - сделано!

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

Чтобы решить подобную проблему на будущее было решено немного притормозить отправку репортов.

Основные требования к решению:
— Единый механизм дросселирования для любых типов исключений;
— Легкое добавление нового типа репорта без изменения существующей логики;
— Возможность самостоятельно определять, какие ошибки считать одинаковыми;
— Единая точка настройки ограничений, без размазывания логики по всему проекту;
— Максимально использовать штатные возможности Laravel и избнжать вендорлок сервиса мониторинга;
— Ограничение частоты репортов не должно влиять на выполнение бизнес-логики или обработку самих исключений;

1. Готовим максимально простой интерфейс
interface ThrottledReport
{
public const int THROTTLE_MINUTES = 15;
public function throttleKey(): string;
public function throttleMinutes(): int;
}

Основная идея - это throttleKey. Именно он определяет, какие ошибки считать одинаковыми. А throttleMinutes - определяет на сколько притормозить отправку повторяющихся репортов.

2. Добавляем trait с реализацией таймера по умолчанию
trait HasReportThrottle
{
public function throttleMinutes(): int
{
return static::THROTTLE_MINUTES;
}
}


3. Далее подключаем обработку эксепшена в bootstrap
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->throttle(function (Throwable $e) {
if ($e instanceof ThrottledReport) {
return Limit::perMinutes($e->throttleMinutes(), 1)
->by($e::class . ':' . $e->throttleKey());
}

return null; // остальные исключения без ограничений
});
})

Тут есть важный нюанс: троттлинг работает на уровне Laravel Exception Handler, поэтому подавленные репорты не попадут не только в Bugsnag, но и в стандартные логи приложения. Но для моего решения — это приемлемо.

И ещё момент — важно учесть, что лимит хранится в кэше, поэтому важно иметь настроенный механизм кэширования. Например, redis.

4. Создаём своё исключение
final class DoNotSpamException extends RuntimeException implements ThrottledReport
{
use HasReportThrottle;

public const int THROTTLE_MINUTES = 60;

public function throttleKey(): string
{
return 'my-unique-key-for-error';
}
}


5. И используем
report(new DoNotSpamException('Something went wrong'));

‼️ Но тут важно помнить — это решение уменьшает частоту сообщений об ошибках, а не частоту возникновения самих ошибок. О количестве и качестве ошибок заботимся сами! ☕️

#php #laravel #exception #throttling #backend #web #разработка
Просмотры: 3
Перейти в пост Открыть в MAX Сообщить о неточности
Территориальный конфликт: война за 172.19.0.0/16

— Когда контейнер ищет внешний сервер у себя под носом —

TL;DR

Разбираемся, куда пропадают пакеты, при чём тут маршрутизация и как закончить войну за IP-адреса.

Кто‑то скажет — сделал из мухи слона! Да у меня каждый день такие приключения! Но для меня получился интересный кейс, и я лучше зафиксирую его для потомков.

Случилось страшное – из docker-контейнера недоступен удалённый хост. При этом с хост-машины никаких проблем с доступность нет – что надо пингуется, что пингуется, то роутится. Хост для определённости - 172.19.10.50.

WTF?!

Диагностика. Раунд первый

Для начала проверяем подсети, которые создал docker:


docker network inspect $(docker network ls -q) \
--format '{{.Name}}: {{range .IPAM.Config}}{{.Subnet}} {{end}}'

В моём случае обнаружились:


bridge: 172.17.0.0/16
traefik: 172.19.0.0/16
app-backend: 172.23.0.0/16
app-notifications: 172.29.0.0/16

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

Для пущей убедительности проверяем маршрут в сетевом пространстве docker:


docker run --rm --network host \
nicolaka/netshoot \
ip route get 172.19.10.50

Бинго!


172.19.10.50 dev br-caf585119744 src 172.19.0.1

Cреда docker считает, что внешний сервер находится непосредственно в локальной сети.

Первое лечение. Неудачное

Перенёс сеть traefik в другой диапазон – 10.250.10.0/24, предварительно убедившись, что подсеть не используется где-то ещё.


networks:
traefik:
name: traefik
driver: bridge
ipam:
config:
- subnet: 10.250.10.0/24

Тут надо сказать, что app-backend и app-notifications используют общие external-сети. Поэтому сети создавались отдельно, а не средствами docker compose.



docker network create app-backend
docker network create app-notifications

Остановил использующие сеть проекты, удалил старые сети, поднял сети заново.
И… внешний сервер снова недоступен.

WTF?!

Диагностика. Раунд второй

Смотрим подсети ещё раз:


traefik: 10.250.10.0/24
app-backend: 172.18.0.0/16
app-notifications: 172.19.0.0/16

Постоянству docker можно позавидовать – не отпускает подсеть 172.19.0.0/16.

Лечение. Теперь окончательное

После повторного конфликта стало понятно: доверять docker автоматическое распределение адресов конкретно в этом окружении — идея так себе.
И поэтому сети создаём с явным указанием подсетей:


docker network create \
--driver bridge \
--subnet 10.250.11.0/24 \
app-backend

docker network create \
--driver bridge \
--subnet 10.250.12.0/24 \
app-notifications

Проверяем итоговую адресацию:


docker network inspect traefik app-backend app-notifications \
--format '{{.Name}}: {{range .IPAM.Config}}{{.Subnet}}{{end}}'

Получаем:


traefik: 10.250.10.0/24
app-backend: 10.250.11.0/24
app-notifications: 10.250.12.0/24


Проверяем доступность внешнего сервера:


docker run --rm \
--network container:notifications-php \
nicolaka/netshoot \
ping -c 3 172.19.10.50

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

Полезное открытие: nicolaka/netshoot

У минималистичных docker-образов, таких как alpine, debian:slim часто отсутствуют даже базовые сетевые утилиты: ping, curl, nslookup, ip. Когда возникает необходимость разобраться с сетевыми проблемами, то удобнее использовать образ nicolaka/netshoot с целым арсеналом сетевых инструментов.

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


docker run --rm -it \
  --network container:notifications-php \
  nicolaka/netshoot

Теперь мы находимся в отдельном диагностическом контейнере, но используем сетевые интерфейсы, IP-адреса и маршруты notifications-php.
При этом исходный контейнер не пересобирается, а после выхода диагностический контейнер автоматически удалится благодаря --rm.

Отличная DevOps-выручалочка. Крайне рекомендую.

#docker #dockercompose #networking #devops
Просмотры: 8
Перейти в пост Открыть в MAX Сообщить о неточности
Великий исход из Bitbucket
– 347 репозиториев. Нормальный человек бы удалил –

tl;dr

Как перенести 347 репозиториев и не сойти с ума. Чудеса автоматизации с go, api и git –mirror

А помните в году так в 2023 пронеслась новость - Bitbucket заблокировал репозитории для разработчиков из России? Ох и испугался же я - ибо все существующие репозитории были на нём. Но делать нечего - взял и скачал все репозитории одним архивом на скорую руку в надежде в будущем перезалить куда-нибудь да и забросил его на антресоль.

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

Но тут что-то торкнуло заглянуть в аккаунт Atlassian.

А, Батюшки-святы! Аккаунт живой! Репозитории на месте! Дальше судьбу решил не искушать и таки перенести всё богатство.

Вооружившись Go, документацией к API и некоторым количеством терпения был создан скрипт, переносящий репозитории из Bitbucket -> GitFlic. Но получился небольшой миграционный конвейер.

Bitbucket API → git clone --mirror → GitFlic API → git push --mirror

Работает он в нескольких режимах: скачивание репозиториев и режим миграции.

По API Bitbucket скрипт получает полный список репозиториев с поддержкой постраничной загрузки и после чего каждый репозиторий клонируется локально.

Из интересного - git clone выполняется с флагом --mirror: создаётся bare-репозиторий, но без рабочей копии, но с полной историей, ветками, тегами и остальными git-особенностями, что заодно уменьшает объём промежуточных данных на диске.

Далее проверяется наличие проекта в GitFlic и если он не найден - создаётся через API GitFlic, после чего локальное зеркало отправляется командой git push --mirror.

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

И так несколько сотен раз.

На этом можно было бы и закончить, но тут появился Андрей (Андрей, привет! 🍺), заставивший пошебуршать по проектам, а меня дополнительно обуяло любопытство - что же у меня скопилось за почти 20 лет работы.

Скопилось 347 уникальных репозиториев — разной степени паршивости и полезности, конечно же, но всё же.

Содержат 29 133 уникальных коммитов, из которых 12 199 моих.

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

По языкам получился такой расклад:

PHP -- 395 293
JavaScript -- 141 132
CSS / SCSS -- 70 766
HTML -- 14 387
Dart -- 6 037
TypeScript -- 5 731
Swift -- 2 945
Ruby -- 2 666
Kotlin -- 1 847
Vue -- 1 545
Python -- 1 540


Также были найдены археологические слои: C/C++, Objective-C, голый SQL с триггерами и функциями, Java и 112 файлов на Go.

С типами проектов всё довольно стандартно, как подытожил ИИ: технологический профиль портфеля преимущественно веб-ориентированный. А то мы не знали ;)

В итоге, несколько часов программирования и 20Гб архивов плавно переехали на новое, не подверженное политическим веяниям, безопасное хранилище, а на SSD освободилось 40Гб. Почему 40? А у меня, оказывается, два архива было. Для надёжности, наверняка, а не благодаря Альцгеймеру ;)

#git #bitbucket #gitflic #golang #автоматизация #миграция
Просмотры: 16
Перейти в пост Открыть в MAX Сообщить о неточности
Загружаем изображение…
Не помню IP. И больше не собираюсь!
– Как высвободить немного своей памяти –

tl;dr:

Давайте серверам имена. Перекладываем IP из своей памяти в ~/.ssh/config.

Я устал!

Спустя несколько лет вечных

ssh master@22.22.22.22
ssh master@33.33.33.33
ssh master@44.44.44.44

и ещё пяток других - я устал помнить IP, я устал открывать Obsidian со списком серверов, я устал копипастить.

Но, преодолев свой склероз, вспомнил, что можно ж и в алиасы!


ssh master@22.22.22.22 => ssh server-1
ssh master@33.33.33.33 => ssh server-2

И т.д.

Если вы спросите как, то я с радостью поделюсь секретом!

Можно в ~/.ssh/config просто добавить список серверов:


Host server-1
HostName 22.22.22.22
User master

Host server-2
HostName 33.33.33.33
User master

Но чтобы не захламлять конфиг, можно сделать круче!

Создаём в ~/.ssh/ папку config.d и там складываем конфиги серверов, сгруппированные хоть по именам, хоть по IP, хоть по натальной карте!


# ~/.ssh/config.d/22-servers.conf
# ~/.ssh/config.d/44-servers.conf

И инклудим всё это добро в основном конфиге:


Include ~/.ssh/config.d/*.conf

Маленькие грабли: Include лучше размещать до первого Host в основном конфиге. Потому что пустая строка блок Host не заканчивает.

Я, конечно же, узнал об этом исключительно из документации. Никаких «какого хрена» не было. ☺

А для отлова граблей - используем


ssh -G server-1 | grep -E '^(hostname|user|port) '

И никаких тебе больше мучительных воспоминаний и лишних пасов руками.

Ну красота же!

P.S.: Вишенка на торте - алиасы работают и с scp, rsync, sftp и прочими SSH-радостями.
P.P.S.: Про шаблоны и Match в конфигах поговорим как-нибудь потом ;)

#ssh #sshconfig #devops #linux #администрирование
Просмотры: 43
Перейти в пост Открыть в MAX Сообщить о неточности
Загружаем изображение…
Не долбимся как кролики с RabbitMQ и yii2-queue

– retry, delay, exponential backoff и немного jitter без лишнего насилия над очередями –

tl;dr

yii2-queue с amqp_interop может повторно отправлять упавшую задачу практически сразу. Добавляем DeliveryDelay - и получаем retry с паузой.

Ну что же, у нас классика на столе - yii2 и система нотификаций, реализованная через yii2-queue и rabbitmq в качестве брокера очередей.

У очередей настроены попытки - пусть 5 для определённости.

Как будто-бы всё хорошо: джоба падает → yii2-queue делает retry → жизнь продолжается.

Но есть нюанс.

При использовании amqp_interop повторная отправка сообщения идёт через redeliver(). И идёт она сразу.
То есть внешний сервис прилёг, API отвечает ошибкой - а мы такие:

— Не получилось?
— Не получилось.
— А сейчас?
— Нет.
— А сейчас?
— Всё ещё нет.
— А СЕЙЧАС?!


Не retry, а тревожный разработчик какой-то перед дедлайном. 😃

Улучшаем - добавляем паузу

Сам yii\queue\amqp_interop\Queue почти всё уже умеет. Но немножечко надо допилить метод redeliver и добавить отправку сообщений с задержкой.



protected function redeliver(AmqpMessage $message): void
{
$attempt = (int)$message->getProperty(self::ATTEMPT, 1);
$delay = $attempt * 60;

$newMessage = $this->context->createMessage(
$message->getBody(),
$message->getProperties(),
$message->getHeaders()
);
$newMessage->setDeliveryMode($message->getDeliveryMode());
$newMessage->setProperty(self::ATTEMPT, $attempt + 1);
$newMessage->setProperty(self::DELAY, $delay);

$producer = $this->context->createProducer();
$producer->setDeliveryDelay($delay * 1000);
$producer->send($this->context->createQueue($this->queueName), $newMessage);
}

Теперь между попытками появляется простенький linear backoff:
1 retry → 60 секунд
2 retry → 120 секунд
3 retry → 180 секунд
...


Если не нравится линейный вариант, то можно сделать exponential backoff:


$delay = 60 * (2 ** ($attempt - 1));

И тогда будет
attempt 1 → 60 сек
attempt 2 → 120 сек
attempt 3 → 240 сек
attempt 4 → 480 сек
...


Нет предела совершенству - добавляем jitter

Представим, что в очереди 10 000 сообщений. Все получили ошибку отправки уведомления через внешний сервис примерно одновременно и происходит retry для всей очереди.

Йеее - словили thundering herd problem, он же «эффект громового стада»! 😆

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

Full jitter - retry распределится по всему диапазону от 0 до $delay.


$jitter = random_int(0, $delay);

Equal Jitter - гарантированно ждём хотя бы половину задержки, а остальное отдаём на волю рандома.


$half = (int) ($delay / 2);
$jitter = $half + random_int(0, $half);

Decorrelated Jitter - cледующая задержка зависит от предыдущей (оставим для самостоятельного изучения 😄).

🚨 !!! ВАЖНО !!!

Уход на retry с новой попыткой срабатывает только тогда, когда джоба выбрасывает Exception.

Не возвращает false, null или что-нибудь ещё в духе "ну, не шмогла".

А именно падает:


throw new RuntimeException('Notification sending failed');

Иначе для yii2-queue джоба успешно завершилась и повторять там, собственно, нечего.

#php #yii2 #rabbitmq #yii2queue #queues #retry #amqp #backend #web #разработка
Просмотры: 28
Перейти в пост Открыть в MAX Сообщить о неточности
Загружаем изображение…
Неуникальная уникальность, и при чем тут дни рождения

– Ведь в мире их еще миллиарда четыре! А быть может и пять, как знать? –

tl;dr

Почему 2^32 вариантов — не так много, как кажется?
Немного математики о коллизиях и обманчивой уникальности.

Наткнулся на днях на интересное понятие - Парадокс дней рождений (birthday paradox). Это ситуация, когда интуиция нас подводит в оценке вероятностей.

В чём суть

Классический пример - кажется, что, раз в году 365 дней, то чтобы с вероятностью 50% у кого‑то в группе совпали дни рождения, то понадобится (на глазок) что-то около половины от 365 - то есть под две сотни человек. Но математика говорит другое: достаточно всего 23 человека, чтобы вероятность совпадения хотя бы у двоих была примерно 50,7%.

Так происходит потому, что сравнивается не “один человек со всеми датами”, а все пары людей между собой.

Для 23 человек пар будет: 23 * 22 / 2 = 253 пары

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

Зачастую мы генерируем всякие уникальные значения: UUID, slug, реферальный код и прочие идентификаторы каким-нибудь эдаким способом:

substr(sha1($orderKeyString), 0, 8)

Разберём

Один hex-символ хранит 4 бита. Значит 8 hex-символов - это 32 бита. А это, на минуточку, 2^32, примерно 4,29 млрд возможных значений. Дофига, не правда ли? На первый взгляд много, но из-за парадокса коллизии становятся вероятными намного раньше.

Вероятность хотя бы одной коллизии можно приблизительно оценить по формуле: p ≈ 1 - exp(-n(n - 1) / (2 × 2^32)).

Оценочно:
10 000 значений -> ~1.2% шанс хотя бы одной коллизии
50 000 значений -> ~25%
77 000 значений -> ~50%
100 000 значений -> ~69%


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

Как исправить

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

Например, 16 hex-символов — это уже 64 бита. Для 100 000 записей вероятность коллизии составляет примерно 0,000000027%.

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

—

Дурацкая математика! Вечно портит красивые решения!

#php #коллизии #хеширование #birthdayparadox #backend #web #разработка
Просмотры: 37
Перейти в пост Открыть в MAX Сообщить о неточности
Загружаем изображение…
Даже в Laravel не обойтись без политики

— Как проверить права доступа к модели через Policy —

Сегодня хочу поговорить о политике.
Чего я хуже остальных, что ли... ✊

tl;dr

Проверку доступа к модели удобно выносить в Policy и подключать в карте маршрутов.

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

Самый простой и правильный способ сделать это в Laravel - использовать Policy.

Policy - специальные классы в Laravel, позволяющие определить логику авторизации для конкретной модели или ресурса.

Преимущества подхода с политиками:

Централизация логики доступа
Поведение задаётся в одном месте - в классе политики


public function delete(User $user, Attachment $attachment)
{
return $attachment->user_id === $user->id;
}

Переиспользование
Policy можно использовать в разных частях приложения
-- в контроллере:


$this->authorize('delete', $attachment);

-- в шаблонах:


@can('delete', $attachment)
<button>Delete</button>
@endcan

-- в маршрутах:


->can('delete', 'attachment')

Улучшение читаемости


$this->authorize('delete', $attachment);

более очевидно считывается, чем


if ($attachment->user_id !== auth()->id())

Масштабируемость
Логика доступа со временем может усложняться (роли, админы, shared-ресурсы и т.д.), но благодаря централизованному описанию политики изменения вносятся в одном месте.

Расширяемость
Можно добавлять собственные методы политик, а не ограничиваться только стандартными.

Тестируемость
Отдельный метод политики легко покрывается тестами.

Чёткое разделение ответственности
— Controller - обрабатывает запрос;
— Policy - определяет доступ;
— Model - определяет данные;

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


app/
├─ Models/
│ └─ Attachment.php
└─ Policies/
└─ AttachmentPolicy.php

Laravel автоматически сопоставит:


Attachment → AttachmentPolicy

Никакой регистрации не требуется.
(Но при необходимости политику можно зарегистрировать вручную в AppServiceProvider.)
—————

Политики создаются командой:


php artisan make:policy AttachmentPolicy --model=Attachment

А дальше происходит магия соглашения именования и политика применяется к модели. ✨

Для своего случая я выбрал вариант проверки доступности на уровне обработки маршрута:


Route::post('/upload/{attachment}/delete', [UploadController::class, 'delete'])
->can('delete', 'attachment')
->name('upload.remove');

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

Если файл принадлежит пользователю - удаляем, а нет - извините, политика. 📄

#laravel #policy #authorize #backend #web #разработка
Просмотры: 42
Перейти в пост Открыть в MAX Сообщить о неточности
Загружаем изображение…
Мутанты на проводе или Всевидящий MutationObserver

– Когда мир перестаёт быть реактивным, остаётся только наблюдать –

tl;dr

Иногда не нужно менять архитектуру. Достаточно наблюдать и DOM сам расскажет, что произошло.

Давненько у нас не было фронтовых баек, а пожалуй что вот оно!

Уютный мир: ламповый laravel, приятные vue-компоненты, встроенные в шаблоны. Всё живёт в безмятежном неведении надвигающейся катастрофы.

Но тут появляется внешний JS-скрипт, который начинает управлять состоянием vue-компонента извне. В частности - открывает попап, а vue-форма внутри должна менять своё поведение.

Должна, но не может!

Классические приемы не работают:
- Пропсы не пробросить - компонент уже смонтирован;
- События не проэмитить - нет родителя;
- Пересоздавать компонент с переносом логики - слишком жирно. Да и внешний скрипт менять нельзя.

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

Да, MutationObserver - наше спасение. Всевидящее Око Саурона в мире DOM.

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


const component = el.parentElement as HTMLDivElement | null;
let observer: MutationObserver | null = null;

onMounted(() => {
observer = new MutationObserver(() => {
if (component.classList.contains('popup_active')) {
// наша логика
}
})

observer.observe(component, {
attributes: true,
attributeFilter: ['class'],
});
});

Но что бы наблюдатель был не слишком наблюдателен, ограничим его кругозор через фильтр: attributeFilter и следим строго за классом.

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


onUnmounted(() => { 
observer?.disconnect(); 
observer = null;
});

Если состояние меняется вне зоны реактивности компонента - это не приговор.

Мутанты среди нас, и они всегда помогут! 

#vue #mutationobserver #javascript #frontend #web #разработка
Просмотры: 38
Перейти в пост Открыть в MAX Сообщить о неточности
Загружаем изображение…
Мамкин DevOps или CI/CD на коленке

Для всех, кто знает боль ручного деплоя.
Тем, кто делал git pull на проде в пятницу вечером.
Всем кто помнит адреса серверов наизусть.
Всем беспризорным душам посвящается.


tl;dr

GitLab webhook + простой серверный обработчик = автоматизированный git pull. Дёшево, сердито и без регистраций и смс.

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

Речь пойдёт о максимально простом варианте CI/CD. Без GitLab Runner, Kubernetes, Jenkins и прочих инструментов взрослых и серьёзных дядек. Сегодня нам понадобятся только GitLab, webhook и небольшой серверный обработчик.

Что такое webhook
Если отбросить умные определения, webhook — это обычный HTTP-запрос по событию. Произошёл push в репозиторий — GitLab отправил запрос на указанный URL. Для наших целей webhook будет выполнять одну простую задачу — запускать обновление проекта.

Как это работает
-- На сервере создаём endpoint для webhook.
-- В GitLab настраиваем webhook на этот URL.
-- GitLab получает push.
-- GitLab отправляет POST-запрос.
-- Сервер проверяет токен.
-- Сервер выполняет деплой.

Всё - один сервер вежливо сообщает другому, что пора обновляться.

Настраиваем GitLab
В проекте открываем: Settings → Webhooks
Далее указываем:
-- URL обработчика;
-- Secret Token;
-- событие Push Events;
-- при необходимости ограничиваем веткой main.

После сохранения GitLab начнёт отправлять запросы при каждом пуше в целевую ветку.

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

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

Для небольших проектов, внутренних стендов и пет-проектов этого часто хватает с головой. А главное — больше не придётся вспоминать, обновил ли ты этот сервер вчера или только собирался.

А главное — больше не придётся вспоминать, обновил ли ты этот сервер вчера или только собирался.

#cicd #devops #gitlab #webhook #deploy #разработка #backend #automation
Просмотры: 49
Перейти в пост Открыть в MAX Сообщить о неточности
Загружаем изображение…
Кладбище миграций в Yii2

- Миграции...миграции никогда не меняются -

tl;dr

Если папка migrations разрослась до размеров энциклопедии -
разнесите миграции по подпапкам и добавьте их в migrationPath.

В начале проекта всё аккуратно, красиво, лакончно:

m240101_create_user_table.php
m240102_add_index_to_user.php


Через пару месяцев проблема нарастает:

m240101_create_user_table.php
m240102_add_index_to_user.php
m240203_fix_index.php
m240205_fix_index_again.php
m240206_final_fix_index_really.php
m240207_why_it_does_not_work.php


А через год папка migrations выглядит как кладбище древней цивилизации. ☠

Но в Yii2 предусмотрели способ навести порядок - можно разнести миграции по папкам.

Создаём миграцию сразу в нужной директории:
php yii migrate/create --migrationPath=@app/migrations/campaign create_campaign_table

И немного подсказываем фреймворку, где теперь их искать:
'controllerMap' => [
'migrate' => [
'class' => MigrateController::class,
'migrationPath' => [
'@app/migrations',
'@app/migrations/attachment',
'@app/migrations/campaign',
'@app/migrations/company',
'@app/migrations/contacts',
'@app/migrations/geobase',
'@app/migrations/invoice',
]
],
]


После этого yii migrate спокойно пройдётся по всем папкам и применит всё как обычно.

А структура проекта станет намного приятнее:
migrations/
├ attachments/
├ campaign/
├ company/
├ contacts/
└ invoice/


Yii2 всё поймёт - даже на кладбище файлов.

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

#php #backend #yii2 #migration #миграции #web #разработка
Просмотры: 76
Перейти в пост Открыть в MAX Сообщить о неточности
Вся лента — после бесплатной регистрации
ВКонтакте Телеграм Одноклассники
Избранное в datool.ru

Следите за каналом в личном кабинете

Зарегистрируйтесь в datool.ru, чтобы добавить канал «Махровый fullstack» в избранное и отслеживать его показатели.

Зарегистрироваться
Для владельцев каналов

Подтвердите свой канал

Войдите или зарегистрируйтесь в datool.ru, затем добавьте нашего бота в канал по инструкции в личном кабинете. После проверки владения замок исчезнет и вам станет доступно управление каналом.

Войти или зарегистрироваться

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти