Feature Flags в продакшене: управление релизами с Laravel, Redis и CI/CD без даунтайма
Введение: Feature Flags в 2026 году
Feature flags (или feature toggles) — это механизм, позволяющий включать и отключать функциональность приложения без изменения кода и без повторного деплоя. В 2026 году эта практика стала стандартом для команд, использующих trunk-based development: все разработчики работают в одной ветке, а незавершённый или рискованный код скрывается за флагами.
Связь с CI/CD очевидна: чем чаще вы деплоите, тем выше риск сломать продакшен. Feature flags разрывают это противоречие — код попадает в продакшен, но остаётся неактивным до явного включения. Это даёт постепенный релиз, безопасный откат и гибкое A/B тестирование без операций с БД или кодовой базой.
В этой статье мы построим полноценную систему управления флагами на Laravel и Redis, интегрируем её с GitHub Actions и разберём реальные сценарии использования.
Архитектура решения
Наша система состоит из трёх уровней:
- Laravel — бизнес-логика, сервис-провайдер, middleware, Blade-директивы.
- Redis — быстрое in-memory хранилище флагов с поддержкой TTL и горячего обновления без деплоя.
- CI/CD (GitHub Actions) — автоматическое включение флагов после успешного деплоя.
Принцип работы прост: флаг — это ключ в Redis со значением, описывающим состояние (включён/выключен, процент пользователей, список групп). Laravel читает этот ключ при каждом запросе (с кешированием на уровне приложения) и принимает решение о показе функции. CI/CD-пайплайн управляет флагами через Redis CLI или HTTP API после успешного прохождения тестов.
Реализация Feature Flag сервиса на Laravel
Контракт и реализация
Начнём с интерфейса, чтобы сервис был легко тестируем и заменяем:
<?php
namespace App\Services\FeatureFlags;
interface FeatureFlagInterface
{
public function isEnabled(string $flag, ?int $userId = null): bool;
public function enable(string $flag): void;
public function disable(string $flag): void;
public function setRolloutPercentage(string $flag, int $percentage): void;
}
Теперь реализация на основе Redis:
<?php
namespace App\Services\FeatureFlags;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Cache;
class RedisFeatureFlagService implements FeatureFlagInterface
{
private const PREFIX = 'feature_flag:';
private const CACHE_TTL = 30; // секунды
public function isEnabled(string $flag, ?int $userId = null): bool
{
$data = $this->getFlagData($flag);
if (empty($data) || $data['status'] === 'disabled') {
return false;
}
if ($data['status'] === 'enabled') {
return true;
}
// Canary: процентное включение по userId
if ($data['status'] === 'canary' && $userId !== null) {
$percentage = (int) ($data['percentage'] ?? 0);
return ($userId % 100) < $percentage;
}
// Whitelist: включён только для конкретных пользователей
if ($data['status'] === 'whitelist' && $userId !== null) {
$list = json_decode($data['users'] ?? '[]', true);
return in_array($userId, $list, true);
}
return false;
}
public function enable(string $flag): void
{
Redis::hset(self::PREFIX . $flag, 'status', 'enabled');
$this->invalidateCache($flag);
}
public function disable(string $flag): void
{
Redis::hset(self::PREFIX . $flag, 'status', 'disabled');
$this->invalidateCache($flag);
}
public function setRolloutPercentage(string $flag, int $percentage): void
{
Redis::hset(self::PREFIX . $flag, [
'status' => 'canary',
'percentage' => max(0, min(100, $percentage)),
]);
$this->invalidateCache($flag);
}
public function setWhitelist(string $flag, array $userIds): void
{
Redis::hset(self::PREFIX . $flag, [
'status' => 'whitelist',
'users' => json_encode($userIds),
]);
$this->invalidateCache($flag);
}
private function getFlagData(string $flag): array
{
return Cache::remember(
'ff:' . $flag,
self::CACHE_TTL,
fn () => Redis::hgetall(self::PREFIX . $flag) ?: []
);
}
private function invalidateCache(string $flag): void
{
Cache::forget('ff:' . $flag);
}
}
Сервис-провайдер
<?php
namespace App\Providers;
use App\Services\FeatureFlags\FeatureFlagInterface;
use App\Services\FeatureFlags\RedisFeatureFlagService;
use Illuminate\Support\ServiceProvider;
class FeatureFlagServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(
FeatureFlagInterface::class,
RedisFeatureFlagService::class
);
}
public function boot(): void
{
// Регистрируем Blade-директивы
$ff = $this->app->make(FeatureFlagInterface::class);
\Blade::if('feature', function (string $flag) use ($ff) {
$userId = auth()->id();
return $ff->isEnabled($flag, $userId);
});
}
}
Регистрируем провайдер в bootstrap/providers.php (Laravel 11+) или в config/app.php.
Middleware
Для защиты маршрутов за флагом:
<?php
namespace App\Http\Middleware;
use App\Services\FeatureFlags\FeatureFlagInterface;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckFeatureFlag
{
public function __construct(private FeatureFlagInterface $flags) {}
public function handle(Request $request, Closure $next, string $flag): Response
{
if (!$this->flags->isEnabled($flag, auth()->id())) {
abort(404);
}
return $next($request);
}
}
Использование в маршрутах:
Route::get('/new-dashboard', NewDashboardController::class)
->middleware('feature:new_dashboard');
Blade-директивы
После регистрации провайдера в шаблонах доступно:
@feature('new_checkout')
<x-new-checkout-form />
@else
<x-legacy-checkout-form />
@endfeature
Хранение флагов в Redis
Структура данных
Мы используем Redis Hash для каждого флага. Это позволяет атомарно обновлять отдельные поля и читать всё одной командой HGETALL:
# Включить флаг полностью
REDIS-CLI HSET feature_flag:new_checkout status enabled
# Canary: включить для 10% пользователей
REDIS-CLI HSET feature_flag:new_checkout status canary percentage 10
# Whitelist: только для конкретных userId
REDIS-CLI HSET feature_flag:new_checkout status whitelist users '[1,2,42,100]'
# Посмотреть состояние флага
REDIS-CLI HGETALL feature_flag:new_checkout
TTL и автоматическое отключение
Для временных экспериментов устанавливайте TTL прямо на ключ:
# Флаг активен 7 дней (604800 секунд)
REDIS-CLI EXPIRE feature_flag:ab_test_header 604800
В Laravel-сервисе можно добавить метод:
public function enableWithTtl(string $flag, int $ttlSeconds): void
{
Redis::hset(self::PREFIX . $flag, 'status', 'enabled');
Redis::expire(self::PREFIX . $flag, $ttlSeconds);
$this->invalidateCache($flag);
}
Горячее обновление без деплоя
Вся мощь подхода — возможность изменить поведение системы в секунды. Достаточно выполнить одну команду в Redis, и через CACHE_TTL (30 секунд в нашем примере) все инстансы приложения подхватят изменение. Никакого деплоя, никакого даунтайма.
Интеграция с CI/CD
Автоматическое включение флагов через GitHub Actions
Типичный сценарий: фича готова, деплой прошёл успешно — GitHub Actions активирует флаг через Redis API. Вот пример воркфлоу:
name: Deploy & Activate Feature Flags
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: php artisan test --parallel
- name: Deploy to production
run: ./deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
- name: Activate feature flag via API
if: success()
run: |
curl -X POST https://api.yourapp.com/internal/feature-flags/new_checkout/enable \
-H "Authorization: Bearer ${{ secrets.INTERNAL_API_TOKEN }}" \
-H "Content-Type: application/json"
- name: Start canary rollout (10%)
if: success()
run: |
curl -X POST https://api.yourapp.com/internal/feature-flags/new_checkout/rollout \
-H "Authorization: Bearer ${{ secrets.INTERNAL_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"percentage": 10}'
Internal API для управления флагами
<?php
namespace App\Http\Controllers\Internal;
use App\Services\FeatureFlags\FeatureFlagInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class FeatureFlagController
{
public function __construct(private FeatureFlagInterface $flags) {}
public function enable(string $flag): JsonResponse
{
$this->flags->enable($flag);
return response()->json(['status' => 'enabled', 'flag' => $flag]);
}
public function disable(string $flag): JsonResponse
{
$this->flags->disable($flag);
return response()->json(['status' => 'disabled', 'flag' => $flag]);
}
public function rollout(string $flag, Request $request): JsonResponse
{
$percentage = $request->integer('percentage');
$this->flags->setRolloutPercentage($flag, $percentage);
return response()->json([
'status' => 'canary',
'flag' => $flag,
'percentage' => $percentage,
]);
}
}
Маршруты защищаем токеном через middleware auth:sanctum или собственный InternalApiAuth.
Сценарии использования
A/B тестирование
Разделяем пользователей на группы по ID:
// Чётные userId видят вариант B
public function isAbVariantB(int $userId): bool
{
return $this->flags->isEnabled('ab_new_onboarding', $userId)
&& ($userId % 2 === 0);
}
Флаг ab_new_onboarding в режиме canary с процентом 50 автоматически покроет половину аудитории.
Canary Release
Постепенный релиз для Laravel позволяет снизить риски. Алгоритм прост: после деплоя активируем флаг для 5%, смотрим на метрики 30 минут, затем 25%, потом 100%:
# Через GitHub Actions или вручную
curl -X POST .../rollout -d '{"percentage": 5}'
# Ждём 30 минут, проверяем Sentry и Grafana
curl -X POST .../rollout -d '{"percentage": 25}'
# Ещё 1 час
curl -X POST .../enable # 100%
Kill Switch — аварийный откат
Самый ценный сценарий. Новая фича вызывает деградацию — одна команда в Redis восстанавливает прежнее поведение без деплоя:
redis-cli HSET feature_flag:new_payment_processor status disabled
Или через API:
curl -X POST https://api.yourapp.com/internal/feature-flags/new_payment_processor/disable \
-H "Authorization: Bearer $TOKEN"
Через 30 секунд (время жизни кеша) все серверы вернутся к старому коду.
Мониторинг состояния флагов
Логирование событий
Расширим сервис логированием через Laravel Events:
public function enable(string $flag): void
{
Redis::hset(self::PREFIX . $flag, 'status', 'enabled');
$this->invalidateCache($flag);
logger()->info('Feature flag enabled', [
'flag' => $flag,
'user_id' => auth()->id(),
'ip' => request()->ip(),
]);
event(new FeatureFlagChanged($flag, 'enabled'));
}
Метрики и алерты
Интегрируйте с Prometheus через spatie/laravel-prometheus или просто инкрементируйте счётчик в Redis:
public function isEnabled(string $flag, ?int $userId = null): bool
{
$result = $this->resolveFlag($flag, $userId);
// Счётчик проверок флага для Grafana
Redis::incr('ff_check:' . $flag . ':' . ($result ? 'true' : 'false'));
return $result;
}
Ключи ff_check:* можно экспортировать в Prometheus через redis_exporter и строить дашборды в Grafana. Алерт на резкий рост ошибок после включения флага — стандартная практика для canary release в Laravel.
Сравнение с готовыми решениями
LaunchDarkly
LaunchDarkly — лидер рынка: богатый UI, таргетинг по атрибутам, аудиты, SDK для 30+ языков. Минусы: цена от $10 за пользователя/месяц, внешняя зависимость в критическом пути, данные уходят за пределы инфраструктуры.
Unleash
Open-source альтернатива с self-hosted вариантом. Есть официальный PHP SDK, поддержка стратегий, webhook-и. Требует отдельного сервера и поддержки. Хорошо подходит для средних и крупных команд.
Самописное решение на Laravel + Redis
Оптимально, когда:
- Команда небольшая (до 10 разработчиков).
- Требования к флагам стандартные: on/off, canary, whitelist.
- Важна минимальная латентность (Redis в той же сети — микросекунды).
- Нет желания платить за SaaS или поддерживать отдельный сервис.
Используйте LaunchDarkly или Unleash, если нужны: сложный таргетинг по произвольным атрибутам, аудит всех изменений, роль-based доступ к флагам или у вас 50+ флагов в активном использовании.
Лучшие практики и типичные ошибки
Лучшие практики
- Называйте флаги описательно:
new_checkout_v2лучше, чемflag_42. - Удаляйте флаги после раскатки: технический долг — флаги, которые уже включены для 100% пользователей, но не убраны из кода. Ставьте напоминание в тикет-трекере.
- Тестируйте оба пути: пишите тесты для включённого и выключенного состояния флага.
- Документируйте флаги: храните описание, дату создания и владельца прямо в Redis Hash — поле
description. - Используйте короткий TTL кеша (30–60 секунд) для быстрого применения изменений.
Типичные ошибки
- Проверка флага на каждый запрос без кеширования — Redis быстрый, но лишние round-trip нагружают сеть. Всегда используйте application-level кеш.
- Флаги в базе данных без репликации — при росте трафика MySQL/PostgreSQL становятся узким местом для feature flags.
- Отсутствие дефолтного значения — если ключ не существует в Redis, сервис должен вернуть
false, а не бросить исключение. - Слишком много вложенных флагов — логика вида «если флаг A и флаг B, но не флаг C» — запах плохой архитектуры. Упрощайте.
- Забытые kill switch — keep switch должны быть всегда доступны команде, не только DevOps. Добавьте простой UI в Laravel Nova или Filament.
Заключение
Feature flags — это не просто инструмент деплоя. Это философия разработки, при которой код в продакшене всегда стабилен, а риски контролируются на уровне конфигурации, а не кода. Связка Laravel + Redis + CI/CD позволяет реализовать полноценную систему управления релизами за несколько часов работы.
Что вы получаете в итоге:
- Постепенный релиз (canary) без инфраструктурных сложностей Kubernetes Canary Deployment.
- Мгновенный откат через kill switch без деплоя.
- A/B тестирование прямо в коде без сторонних сервисов.
- Полный контроль данных и инфраструктуры.
Начните с одного флага для следующей рискованной фичи — и этот подход станет стандартом для всей команды. В 2026 году trunk-based development и feature flags — это не тренд, а необходимость для любой серьёзной PHP-команды.
Технологии
Теги
Руслан Исмаилов
Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →