Что такое Release Engineering
Release engineering — дисциплина, которая обеспечивает надёжную, повторяемую и безопасную доставку кода в production. Цель: сделать релизы скучными и рутинными, а не страшными событиями.
Принципы Release Engineering
| Принцип | Описание |
|---|---|
| Reproducibility | Каждый билд воспроизводим из одних и тех же inputs |
| Automation | Минимум ручных шагов в процессе релиза |
| Hermetic builds | Билд не зависит от окружения машины |
| Incremental | Маленькие, частые релизы вместо больших |
| Rollback-ready | Любой релиз можно откатить за минуты |
CI/CD Pipeline
Continuous Integration
CI — практика частой интеграции кода (несколько раз в день) с автоматическими проверками.
Push → Build → Lint → Test (unit) → Test (integration) → Artifact → Ready
Continuous Delivery vs Continuous Deployment
| Аспект | Continuous Delivery | Continuous Deployment |
|---|---|---|
| Деплой | Ручное подтверждение | Автоматически |
| Когда | Код всегда готов к деплою | Каждый commit в production |
| Для кого | Большинство компаний | Зрелые DevOps-организации |
| Риск | Контролируемый | Требует отличного тестирования |
GitHub Actions
<?php
// .github/workflows/ci.yml structure (shown as PHP config concept)
// Actual YAML workflow is in the repository root
/**
* CI Pipeline stages for a Symfony application.
* This class represents the pipeline logic, not the YAML config.
*/
declare(strict_types=1);
namespace App\Deploy;
final readonly class CiPipeline
{
/**
* Define pipeline stages and their checks.
*
* @return array<string, array{command: string, failFast: bool}>
*/
public function getStages(): array
{
return [
'install' => [
'command' => 'composer install --no-interaction --prefer-dist',
'failFast' => true,
],
'lint' => [
'command' => 'vendor/bin/php-cs-fixer fix --dry-run --diff',
'failFast' => true,
],
'static-analysis' => [
'command' => 'vendor/bin/phpstan analyse -l 9',
'failFast' => true,
],
'unit-tests' => [
'command' => 'vendor/bin/phpunit --testsuite=unit',
'failFast' => true,
],
'integration-tests' => [
'command' => 'vendor/bin/phpunit --testsuite=integration',
'failFast' => true,
],
'security-check' => [
'command' => 'composer audit',
'failFast' => false,
],
];
}
}
Сравнение стратегий
| Стратегия | Downtime | Rollback | Риск | Сложность |
|---|---|---|---|---|
| Big Bang | Да | Сложный | Высокий | Низкая |
| Rolling | Минимальный | Средний | Средний | Средняя |
| Blue-Green | Нет | Мгновенный | Низкий | Высокая |
| Canary | Нет | Быстрый | Низкий | Высокая |
| Shadow | Нет | Не нужен | Минимальный | Очень высокая |
Blue-Green Deployment
Два идентичных окружения: Blue (текущий production) и Green (новая версия).
Load Balancer
/ \
[Blue - v1.0] [Green - v1.1]
(active) (staging)
Шаги:
1. Задеплоить v1.1 на Green
2. Протестировать Green
3. Переключить LB на Green
4. Green становится active
5. Blue = rollback target
Canary Release
Постепенное направление трафика на новую версию.
Время: 0 мин 5 мин 15 мин 30 мин 60 мин
v1.0: 100% → 95% → 90% → 50% → 0%
v1.1: 0% → 5% → 10% → 50% → 100%
На каждом шаге: проверка метрик → если OK → увеличить %
→ если NOT OK → rollback
Реализация Canary Router
<?php
declare(strict_types=1);
namespace App\Deploy;
final readonly class CanaryRouter
{
/**
* @param int $canaryPercentage Percentage of traffic to route to canary (0-100)
* @param array<string> $allowedUsers Users always routed to canary (beta testers)
*/
public function __construct(
private int $canaryPercentage = 0,
private array $allowedUsers = [],
) {}
/**
* Determine if a request should be routed to canary version.
*/
public function shouldRouteToCanary(string $userId, string $requestId): RoutingDecision
{
// Always route allowed users to canary
if (in_array($userId, $this->allowedUsers, true)) {
return new RoutingDecision(
target: DeployTarget::Canary,
reason: 'User in canary allowlist',
);
}
// Use consistent hashing for sticky sessions
$hash = crc32($userId);
$bucket = abs($hash) % 100;
if ($bucket < $this->canaryPercentage) {
return new RoutingDecision(
target: DeployTarget::Canary,
reason: sprintf('Bucket %d < %d%% canary threshold', $bucket, $this->canaryPercentage),
);
}
return new RoutingDecision(
target: DeployTarget::Stable,
reason: sprintf('Bucket %d >= %d%% canary threshold', $bucket, $this->canaryPercentage),
);
}
}
enum DeployTarget: string
{
case Stable = 'stable';
case Canary = 'canary';
}
final readonly class RoutingDecision
{
public function __construct(
public DeployTarget $target,
public string $reason,
) {}
}
Feature flags (feature toggles) позволяют включать/отключать функциональность без деплоя.
Типы Feature Flags
| Тип | Срок жизни | Пример |
|---|---|---|
| Release | Дни-недели | Новая фича за флагом |
| Experiment | Недели-месяцы | A/B тест |
| Ops | Постоянно | Kill switch, degraded mode |
| Permission | Постоянно | Premium features |
Система Feature Flags
<?php
declare(strict_types=1);
namespace App\Feature;
enum FlagStatus: string
{
case Enabled = 'enabled';
case Disabled = 'disabled';
case Percentage = 'percentage';
case UserList = 'user_list';
}
final readonly class FeatureFlag
{
public function __construct(
public string $name,
public FlagStatus $status,
public int $percentage = 0,
public array $allowedUsers = [],
public ?\DateTimeImmutable $expiresAt = null,
) {}
}
final class FeatureFlagManager
{
/** @var array<string, FeatureFlag> */
private array $flags = [];
public function __construct(
private readonly \Redis $redis,
private readonly string $prefix = 'feature_flag:',
) {
$this->loadFlags();
}
/**
* Check if a feature is enabled for a given context.
*/
public function isEnabled(string $flagName, ?string $userId = null): bool
{
$flag = $this->flags[$flagName] ?? null;
if ($flag === null) {
return false;
}
// Check expiration
if ($flag->expiresAt !== null && $flag->expiresAt < new \DateTimeImmutable()) {
return false;
}
return match ($flag->status) {
FlagStatus::Enabled => true,
FlagStatus::Disabled => false,
FlagStatus::Percentage => $this->checkPercentage($flagName, $userId, $flag->percentage),
FlagStatus::UserList => $userId !== null && in_array($userId, $flag->allowedUsers, true),
};
}
/**
* Toggle a flag on/off.
*/
public function toggle(string $flagName, FlagStatus $status): void
{
$flag = $this->flags[$flagName] ?? null;
if ($flag === null) {
return;
}
$updated = new FeatureFlag(
name: $flag->name,
status: $status,
percentage: $flag->percentage,
allowedUsers: $flag->allowedUsers,
expiresAt: $flag->expiresAt,
);
$this->flags[$flagName] = $updated;
$this->redis->hSet($this->prefix . 'flags', $flagName, serialize($updated));
}
/**
* Get all flags for debugging.
*
* @return array<string, array{status: string, percentage: int}>
*/
public function getAllFlags(): array
{
$result = [];
foreach ($this->flags as $name => $flag) {
$result[$name] = [
'status' => $flag->status->value,
'percentage' => $flag->percentage,
];
}
return $result;
}
private function checkPercentage(string $flagName, ?string $userId, int $percentage): bool
{
// Consistent: same user always gets the same result
$seed = $userId ?? uniqid('anon_', true);
$hash = crc32($flagName . ':' . $seed);
$bucket = abs($hash) % 100;
return $bucket < $percentage;
}
private function loadFlags(): void
{
$data = $this->redis->hGetAll($this->prefix . 'flags');
foreach ($data as $name => $serialized) {
$this->flags[$name] = unserialize($serialized);
}
}
}
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Feature\FeatureFlagManager;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
final class OrderController
{
public function __construct(
private readonly FeatureFlagManager $features,
private readonly OrderService $orderService,
private readonly NewOrderService $newOrderService,
) {}
#[Route('/api/orders', methods: ['POST'])]
public function create(CreateOrderRequest $request): JsonResponse
{
$userId = $request->getUserId();
// Gradual rollout of new order processing
if ($this->features->isEnabled('new_order_processing', $userId)) {
$order = $this->newOrderService->create($request->toDto());
} else {
$order = $this->orderService->create($request->toDto());
}
return new JsonResponse($order, 201);
}
}
Deployer — инструмент для автоматизации деплоя PHP-приложений.
<?php
// deploy.php — Deployer configuration concept
// Demonstrates the deployment recipe structure
declare(strict_types=1);
namespace App\Deploy;
/**
* Deployment configuration for Symfony application.
* This represents the Deployer recipe logic.
*/
final readonly class DeploymentConfig
{
/** @return array<string, mixed> */
public function getConfig(): array
{
return [
'repository' => '[email protected]:company/app.git',
'branch' => 'main',
'shared_dirs' => ['var/log', 'var/sessions'],
'shared_files' => ['.env.local'],
'writable_dirs' => ['var'],
'keep_releases' => 5,
];
}
/** @return array<string, array<string>> */
public function getTasks(): array
{
return [
'deploy:prepare' => [
'deploy:info',
'deploy:setup',
'deploy:lock',
'deploy:release',
'deploy:update_code',
'deploy:shared',
],
'deploy:build' => [
'composer install --no-dev --optimize-autoloader',
'bin/console cache:clear --env=prod',
'bin/console cache:warmup --env=prod',
'bin/console assets:install',
],
'deploy:publish' => [
'deploy:symlink', // Atomic switch (blue-green via symlink)
'deploy:unlock',
'deploy:cleanup',
],
'rollback' => [
'deploy:rollback', // Revert symlink to previous release
],
];
}
}
| Стратегия | Скорость | Сложность |
|---|---|---|
| Symlink switch | Секунды | Deployer style |
| Container swap | Секунды | Blue-green, Docker |
| Git revert + deploy | Минуты | Полный pipeline |
| Database rollback | Минуты-часы | Обратная миграция |
| Feature flag off | Мгновенно | Без деплоя |
Правило: Всегда имейте план rollback до деплоя. Если rollback невозможен (например, необратимая миграция БД) — используйте canary и feature flags.
Checklist релиза
| Шаг | Проверка |
|---|---|
| Pre-deploy | CI зелёный, тесты проходят |
| Database | Миграции обратимы или совместимы |
| Config | Переменные окружения настроены |
| Feature flags | Новые фичи за флагами |
| Monitoring | Dashboards и алерты готовы |
| Rollback | План отката задокументирован |
| Communication | Команда уведомлена о деплое |
| Post-deploy | Smoke tests, метрики в норме |
Итоги
| Концепция | Суть |
|---|---|
| CI/CD | Автоматизация от commit до production |
| Blue-Green | Два окружения, мгновенное переключение |
| Canary | Постепенное увеличение трафика на новую версию |
| Feature Flags | Включение/отключение фич без деплоя |
| Rollback | Всегда иметь план, всегда тестировать |
| Release Checklist | Системный подход к каждому релизу |