Функциональные флаги в CI/CD: что, зачем и как

Узнайте, что такое функциональные флаги и как они ускоряют CI/CD. Виды флагов, инструменты (Flagsmith, LaunchDarkly), интеграция с pytest и лучшие практики внедрения.

Обложка: Функциональные флаги в CI/CD: что, зачем и как

Собрали небольшой гайд по функциональным флагам: какими они бывают, чем полезны, зачем нужны в CI/CD и как их правильно использовать.

Функциональные флаги (feature flags, feature toggles) позволяют разработчикам включать и отключать функции приложения, но при этом не требуют повторно собирать и развёртывать код. Благодаря флагам в CI/CD можно чётко отделить друг от друга этапы «развёртывания» (доставки результатов разработки и сборки) и «релиза» (запуска новой функциональности для пользователей). Для чего и как именно это делается — разберём в этой статье.

Что такое функциональный флаг

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

Ниже приведён пример JSON, который получает приложение. Оно проверяет геотег пользователя, и если пользователь из Москвы, то значение флага возвращает true и в итоге отображается виджет погоды. Для пользователей из других городов значение флага принимает default_variant: "disabled", и виджет для них не показывается.

			{
  "feature_flags": {
	"enable_local_weather_widget": {
  	"is_active": true,
  	"description": "Показывает виджет погоды только для пользователей из Москвы",
  	"targeting_rules": [
    	{
      	"attribute": "geo.city",
      	"operator": "equals",
      	"value": "Москва"
    	}
  	],
  	"variants": {
    	"enabled": true,
    	"disabled": false
  	},
  	"default_variant": "disabled"
	}
  }
}
		

Функциональные флаги могут использоваться в разных сценариях — приведём основные с парой примеров:

  • Повышение надёжности системы: отключение рекомендаций при высокой нагрузке, переключение на резервный платёжный шлюз при сбоях в основном.
  • Обучение через продуктовые эксперименты и A/B-тестирование: сравнение двух вариантов страницы регистрации, тестирование двух видов кнопки «Купить».
  • Управление доступом по тарифам и подпискам: экспорт отчётов только для пользователей Pro, расширенные настройки только для корпоративных клиентов.
  • Сезонные и временные функции: промокоды на «чёрную пятницу», приём работ для конкурса.
  • Аварийные выключатели (kill switches): отключение медиаплеера из-за ошибок, удаление неверно настроенного способа оплаты.

Далее рассмотрим основные типы флагов.

Типы флагов

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

По механизму работы: статические и динамические флаги

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

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

Статические флаги задаются через переменные окружения (.env), конфиг-файлы (config.json) или внешние сервисы конфигурации (Consul).

Пример в .env:

			# .env
APP_FEATURE_NEW_UI=true
APP_FEATURE_DARK_MODE=false
APP_FEATURE_BETA_SEARCH=true
APP_MAINTENANCE_MODE=false
		

Пример в виде конфиг-файла:

			{
  "feature_flags": {
	"new_ui": {
  	"enabled": true,
  	"description": "Новый дизайн главной страницы"
	},
	"dark_mode": {
  	"enabled": false,
  	"description": "Тёмная тема оформления"
	},
	"beta_search": {
  	"enabled": true,
  	"description": "Улучшенный поиск с AI-подсказками"
	}
  }
}
		

Использование в коде:

			// Чтение из переменных окружения.
const isNewUIEnabled = process.env.APP_FEATURE_NEW_UI === 'true';
if (isNewUIEnabled) {
  renderNewUI();
} else {
  renderOldUI();
}
		

Плюсы: простота, нулевые сетевые задержки, предсказуемость, безопасность (нет внешних вызовов в runtime).

Минусы: невозможность изменить поведение «на лету» — требуется пересборка/перезапуск.

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

Такие флаги управляются через внешние системы: API, БД, специализированные сервисы.

В примере ниже — два флага. Первый проверяет, попадает ли пользователь в сегмент для тестирования нового способа оформления заказа: берутся только 25 % пользователей сервиса; также они должны входить в группы "beta_testers", "premium_users" и быть из России, Беларуси или Казахстана. Второй флаг показывает, что ИИ-рекомендации при заказах пока отключены для всех пользователей, но в нужный момент это можно изменить даже во время работы приложения.

Пример конфигурации:

			{
  "flags": {
	"new_checkout_flow": {
  	"enabled": true,
  	"rollout_percentage": 25,
  	"targeting": {
    	"user_segments": ["beta_testers", "premium_users"],
    	"countries": ["RU", "BY", "KZ"]
  	}
	},
	"ai_recommendations": {
  	"enabled": false,
  	"rollout_percentage": 0,
  	"reason": "Отключено из-за высокой нагрузки на ML-сервис"
	}
  }
}
		

Использование в коде:

			// Периодический опрос сервиса флагов (например, раз в 30 секунд).
const flags = await fetchFlagsFromService();
 
if (flags.new_checkout_flow.enabled) {
  // Проверяем, попадает ли пользователь в rollout.
  if (user.id % 100 < flags.new_checkout_flow.rollout_percentage) {
	renderNewCheckout();
  } else {
	renderLegacyCheckout();
  }
}
		

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

Минусы: усложнение логики системы (flag debt), дополнительная задержка при запросе состояния флага и зависимость от внешних систем.

Сведём сравнение типов флагов в небольшую таблицу:

Функциональные флаги в CI/CD: что, зачем и как_33

По цели применения: релизные, экспериментальные, операционные и разрешающие флаги

Релизные флаги (release flags) позволяют отделить развёртывание от релиза, то есть экспериментальная/незаконченная фича вливается в основную ветку и развёртывается вместе с остальным кодом, но остаётся выключенной до официального релиза. Если после релиза что-то пойдёт не так, флаг можно сразу же отключить, не делая новый релиз и не откатывая приложение. Когда новая функциональность начинает работать стабильно, релизный флаг удаляют.

Например, код нового платёжного шлюза уже в продакшене, но флаг пока enabled: false. Флаг включат 1 ноября, и фича станет доступной всем. Если с платежами возникнут проблемы, флаг сразу же отключат без отката деплоя.

			{
  "feature_flags": {
	"new_payment_gateway": {
  	"enabled": false,
  	"description": "Новый платёжный шлюз",
  	"release_date": "2026-11-01",
  	"rollback_ready": true
	}
  }
}
		

Экспериментальные флаги (experiment flags) — это инструмент для A/B-тестов и проверки гипотез. Они позволяют показать разные новые варианты новой функции разным пользователям (или показать новую функциональность только ограниченной лояльной группе пользователей), чтобы изучить их реакцию и на её основе сделать вывод о новой фиче. По окончании A/B-теста экспериментальный код и флаг удаляют.

В примере ниже команда интернет-магазина проводит тест, чтобы выбрать подходящий цвет для кнопки «Оформить заказ» в корзине. Выборка из 30 % пользователей распределяется случайным образом: 50 % из них видят зелёную кнопку, 25 % — красную, 25 % — оранжевую. Система собирает метрики: конверсию и отказ от корзины. Через две недели становится ясно, что оранжевая кнопка даёт +15 % к конверсии, поэтому её делают основной, а флаг и экспериментальный код удаляют.

			{
  "feature_flags": {
	"checkout_button_color": {
  	"enabled": true,
  	"experiment_type": "ab_test",
  	"variants": {
    	"control": {
      	"color": "green",
      	"weight": 50
    	},
    	"treatment_a": {
      	"color": "red",
      	"weight": 25
    	},
    	"treatment_b": {
      	"color": "orange",
      	"weight": 25
    	}
  	},
  	"targeting": {
    	"user_percentage": 30
  	},
  	"metrics": ["conversion_rate", "cart_abandonment"]
	}
  }
}
		

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

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

Рассмотрим следующий пример. В «чёрную пятницу» нагрузка в приложении интернет магазина растёт, утилизация CPU превышает 85 %. Из-за этого отключаются ИИ-рекомендации (на ресурсоёмкой ML-модели) и включаются обычные статические. После того как нагрузка снижается, флаг возвращается в исходное состояние.

Второй флаг позволяет переключиться на режим обслуживания БД, а третий — на резервный платёжный сервис, если есть сбои в основном.

			{
  "feature_flags": {
	"enable_ai_recommendations": {
  	"enabled": true,
  	"description": "ИИ-рекомендации товаров на главной",
  	"degradation_mode": {
    	"enabled": false,
    	"trigger": "cpu_usage > 85%",
    	"fallback": "static_recommendations"
  	}
	},
	"maintenance_mode": {
  	"enabled": false,
  	"description": "Режим обслуживания БД"
	},
	"use_backup_payment_provider": {
  	"enabled": false,
  	"description": "Переключение на резервный платёжный сервис при сбоях основного"
	}
  }
}
		

Разрешающие флаги (permission flags) — это одна из составляющих настройки доступа к специфическим функциям приложения, например к ролям в админ-панели для разработчиков и менеджеров или к платным функциям для клиентов. Такие флаги могут быть очень динамичными и менять состояние при каждом запросе.

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

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

			{
  "feature_flags": {
	"admin_panel_access": {
  	"enabled": true,
  	"type": "permission",
  	"rules": [
    	{
      	"condition": "user.role == 'admin'",
      	"access": "full"
    	},
    	{
      	"condition": "user.role == 'manager'",
      	"access": "limited"
    	},
    	{
      	"condition": "user.subscription == 'premium'",
      	"access": "analytics_dashboard"
    	}
  	]
	},
	"export_to_excel": {
  	"enabled": true,
  	"type": "permission",
  	"requires": "subscription.premium || user.role == 'admin'"
	}
  }
}
		

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

Системы управления флагами

Глобально все решения для управления флагами делятся на три категории: проприетарные, Open Source и собственные разработки.

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

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

  • LaunchDarkly позволяет включать функционал для конкретных сегментов аудитории с высокой гранулярностью, то есть подходит для детального таргетинга пользователей и маркетинговых экспериментов. Платформа даёт enterprise-уровень надёжности и позволяет гибко управлять жизненным циклом флагов.
  • ConfigCat обеспечивает единую точку управления, если инфраструктура состоит из разнородных технологий (веб, мобильные приложения, бэкенд на разных языках).
  • Harness интегрирует управление флагами прямо в CI/CD-процессы. Предоставляет командам детальную аналитику о влиянии флагов на производительность и стабильность системы прямо в пайплайне развёртывания.

Open Source-решения дают полный контроль над кодом и данными и позволяют развёртывать систему на своих серверах (on-premise), что критично для организаций со строгими требованиями к безопасности.

Вот пара примеров Open Source-платформ для управления флагами:

  • Flagsmith позволяет сохранить данные внутри периметра компании, хорошо поддаётся конфигурации и кастомизации через удобный интерфейс.
  • Unleash хорошо подходит компаниям, которым важно разграничивать права управления флагами между разными командами, менеджерами и разработчиками.

Собственная разработка тоже возможна, но требует значительных ресурсов. На ранних этапах разработки продукта или при тестировании гипотез внедрения флагов можно использовать переменные окружения, JSON-файлы или простой API-сервис. Но по мере роста продукта поддерживать собственное решение становится всё сложнее: нужно обеспечивать отказоустойчивость, безопасность и разработку SDK для всех языков программирования в проекте. Так что переход на специализированный инструмент (Open Source или SaaS) — обычно вопрос времени.

Независимо от того, какое решение вы выберете, важно убедиться, что оно легко интегрируется с вашей CI/CD-системой и не создаёт узких мест в разработке. Кроме того, внедрять полноценную систему управления флагами на ранних этапах разработки может быть избыточным, поэтому нужно всегда спрашивать себя, стоят ли затраченные усилия желаемого результата.

Теперь посмотрим, какую роль функциональные флаги играют в CI/CD.

Функциональные флаги в CI/CD

Флаги фундаментально меняют подход к управлению исходным кодом, особенно если используется модель ветвления Trunk Based Development (TBD).

При TBD в основную ветку (main/trunk) часто вливаются изменения. Функциональные флаги позволяют разработчикам интегрировать и развёртывать фичи, которые ещё не готовы к релизу, и при этом не рисковать стабильностью всего приложения. Благодаря флагам быстрее внедряются изменения и упрощается слияние, так как весь код (и основной, и разрабатываемый) остаётся в одной ветке. Это предотвращает конфликты слияния (merge hell), которые часто случаются при долгоживущих ветках.

Флаги улучшают управление рисками, а именно:

  • дают возможность выключить функциональность сразу после релиза, если она работает неправильно, — благодаря этому система быстрее восстанавливается (MTTR);
  • позволяют включать фичу поэтапно, делать canary/rollout, быстро откатывать поведение через переключатель и не ждать новой сборки для исправления проблемы.

На сам CI/CD-пайплайн функциональные флаги никак не влияют. Основная задача пайплайна — запуск автотестов, сохранение результатов их работы и передача далее для дальнейшего анализа. Проверка функциональности при включённом и выключенном флаге должна быть частью автотестов.

Чтобы эффективно протестировать оба состояния флага, можно использовать параметризованные тесты (переключаться между состояниями внутри одного набора тестов) либо поддерживать отдельные наборы тестов для включённого и выключенного флага.

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

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

Допустим, мы готовим новую версию приложения и до того, как выпустить её в продакшен, тестируем релиз-кандидата в окружении staging. CI/CD-пайплайн собрал образ release-candidate и развернул его в staging. При запуске приложения запрашивается набор флагов из централизованной системы управления флагами. Далее при запуске автотестов подбирается набор тест-кейсов для заданного окружения. Также подбирается набор флагов для конкретного пользователя после того, как его логин и user id считаются из cookie во время запроса.

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

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

Сценарий тестирования релиз-кандидата с применением функциональных флагов

Сценарий состоит из следующих шагов:

  1. CI/CD-пайплайн собирает образ release-candidate и деплоит в staging.
  2. При запуске приложения запрашиваются флаги из Flagsmith с параметрами environment=staging, user_id, role.
  3. Система флагов возвращает конфигурацию: какие флаги включены и какие тест-кейсы нужно выполнить.
  4. Автотесты динамически фильтруют тесты на основе полученного ответа. Тесты, которые не относятся к активным функциям текущего окружения, не запускаются.

Все флаги хранятся в системе управления флагами Flagsmith в формате JSON.

			{
  "feature_flags": {
	"new_payment_flow": {
  	"enabled": true,
  	"environments": {
    	"staging": { "enabled": true, "rollout_percentage": 100 },
    	"production": { "enabled": false }
  	},
  	"test_cases": {
    	"staging": ["test_payment_success", "test_payment_failure", "test_payment_timeout"],
    	"production": ["test_payment_success"]
  	},
  	"config": { "timeout_ms": 5000 }
	}
  }
}
		

Здесь описан флаг new_payment_flow — он управляет новой версией процесса оплаты.

В нашем примере функция включена в staging и выключена в production.

Помимо самого состояния флага, конфигурация содержит список тестов для каждого окружения. Для staging предусмотрены три проверки:

  • успешная оплата;
  • обработка ошибки;
  • обработка тайм-аута.

Для production пока оставлен только тест успешной оплаты.

В config находятся параметры, которые тесты также могут получить из системы флагов. В нашем случае это timeout_ms 5000. Значение тайм-аута не зашито непосредственно в тесте, и его можно изменить через конфигурацию флага.

Автотесты при запуске запрашивают флаги из внешней системы с помощью следующего запроса:

			POST /api/v1/flags
{
	"environment": "staging",
	"user": { "user_id": "user_123", "role": "qa_team" },
	"context": { "release_candidate": "rc-2026.07.20", "source": "autotest_runner" }
}
		

Здесь тестовый раннер сообщает системе флагов контекст, в котором выполняется проверка. В параметре context указаны идентификатор release candidate и источник запроса — autotest_runner.

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

Автотесты используют функциональные флаги, чтобы динамически сформировать определённый тестовый сценарий. Для этого процесс сбора тестов перехватывается через специальную функцию (conftest.py):

			# conftest.py
import pytest
from flagsmith import Flagsmith
 
def pytest_collection_modifyitems(config, items):
	"""Фильтрация тестов на основе конфигурации из сервиса флагов"""
	env = config.getoption("--environment")
	flagsmith_key = config.getoption("--flagsmith-key")
	
	fs = Flagsmith(environment_key=flagsmith_key)
	flags = fs.get_environment_flags()
	
	# Собираем whitelist тест-кейсов из активных флагов.
	allowed_test_cases = set()
	for flag in flags.all_flags():
    	if flag.enabled and isinstance(flag.value, dict):
        	allowed_test_cases.update(flag.value.get("test_cases", {}).get(env, []))
        	
	# Фильтруем pytest items.
	filtered_items = []
	for item in items:
    	if env in item.keywords and any(tc in item.name for tc in allowed_test_cases):
        	filtered_items.append(item)
        	
	items[:] = filtered_items
 
def pytest_addoption(parser):
	parser.addoption("--environment", action="store", default="staging")
	parser.addoption("--flagsmith-key", action="store")
		

Тут pytest находит все тесты, какие есть. Код узнаёт, в каком окружении запускаются тесты, подключается к Flagsmith и получает активные флаги.

Из флагов берётся список тестов, которые нужно запустить для конкретного окружения. Из всех найденных тестов остаются только те, которые:

  • относятся к нужному окружению;
  • указаны в конфигурации активного флага.

В результате pytest запустит только оставшиеся тесты.

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

			# test_payment.py
import pytest
 
class TestPaymentFlow:
	@pytest.mark.staging
	def test_payment_success(self, flagsmith_client, payment_service):
    	# Получаем конфиг напрямую из флага.
    	flag_config = flagsmith_client.get_feature_value("new_payment_flow")
    	timeout = flag_config.get("timeout_ms", 3000)
    	
    	response = payment_service.process_payment(amount=1000, timeout_ms=timeout)
    	assert response.status == "success"
		

Здесь из конфигурации флага new_payment_flow извлекается параметр timeout_ms. В нашей конфигурации его значение было задано — 5000. Если бы оно не было указано, использовалось бы значение по умолчанию — 3000.

Таким образом, CI/CD-пайплайн остаётся универсальным и не содержит отдельной логики вроде «если это такой-то релиз, запусти такие-то тесты». Он запускает один и тот же механизм, а конкретное поведение определяется конфигурацией функциональных флагов.

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

Риски и рекомендации по использованию флагов

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

Основные риски при работе с флагами

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

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

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

Архитектурные рекомендации

Прежде всего, флаги нужно вовремя удалять: можно настроить в CI/CD автоматические уведомления на флаги, которые живут дольше установленного времени, например больше 30 дней. А релизные и экспериментальные флаги лучше удалять сразу после того, как стабилизировалась фича или закончился A/B-тест. У каждого флага должны быть владелец, дата создания и план удаления.

Как мы уже писали выше, флаги — это критическая зависимость, поэтому стоит обезопасить себя от сбоев в системе управления флагами: использовать на стороне приложения fallback-значения (default variants), внедрить локальное кеширование (чтобы пережить кратковременные падения сети), использовать автоотключение (Circuit Breaker) запросов к сервису флагов при множественных таймаутах.

Обезопасить себя при работе с флагами нужно и с других сторон: нельзя хранить в их конфигурации секреты или персональные данные, необходимо логировать все изменения (кто, когда и что изменил) и внедрить обязательный code review и approval-процесс для критичных флагов.

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

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

Резюмируем ключевые принципы:

  • Временность. Для флагов должен быть чёткий план удаления.
  • Прозрачность. У каждого флага должны быть владелец, назначение и срок жизни.
  • Отказоустойчивость. Приложение должно работать даже при недоступности сервиса управления флагами.
  • Мониторинг. Состояние и использование флагов нужно постоянно отслеживать.

Итак, функциональные флаги — это важный элемент культуры непрерывной поставки. Они повышают гибкость, безопасность и скорость доставки ПО, позволяют командам выпускать код чаще. Но важно помнить, что флаги — это не панацея: они дают максимум пользы только при грамотном внедрении и регулярной очистке, а без должной дисциплины могут усложнить кодовую базу и тестирование.