Стандарты CI/CD пайплайна
Обзор
CI/CD пайплайн платформы IWM обеспечивает качество кода, безопасность и надёжные деплои для финансовой системы, обрабатывающей MLM комиссии, платежи и чувствительные данные пользователей.
Цели пайплайна
| Цель | Реализация |
|---|---|
| Корректность | Комплексные наборы тестов для финансовых расчётов |
| Безопасность | Многоуровневое сканирование (зависимости, секреты, контейнеры) |
| Надёжность | Деплои без простоя с возможностью отката |
| Скорость | Параллельные jobs, кэширование Docker слоёв, умное определение изменений |
| Аудируемость | Отслеживание версий, логи деплоев, документирование изменений |
Архитектура пайплайна
┌─────────────────────────────────────────────────────────────────────────┐
│ PR VALIDATION │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Build │ │ Lint │ │ Type │ │ Secret │ │
│ │ Check │ │ Format │ │ Check │ │ Scan │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │ │
│ └─────────────────┴────────┬────────┴─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ TEST SUITE │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ Unit │ │Integration│ │ E2E │ │ Contract │ │ │
│ │ │ (80%+) │ │ (DB/API) │ │ (Critical)│ │ (API) │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ SECURITY GATES │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ npm audit │ │ gitleaks │ │ SAST │ │ Trivy │ │ │
│ │ │ (high+) │ │ (secrets) │ │ (CodeQL) │ │ (container│ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ DATABASE VALIDATION │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ Migration dry-run │ │ Schema drift detection │ │ │
│ │ └─────────────────────┘ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
│ Merge to main
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ RELEASE PIPELINE │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Version │ │ Build │ │ Push │ │ Generate │ │
│ │ Bump │──▶│ Images │──▶│ Registry │──▶│ Changelog │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ DEPLOY TO STAGING │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ Run │ │ Deploy │ │ Health │ │ Smoke │ │ │
│ │ │Migrations │──▶│ (B/G) │──▶│ Check │──▶│ Tests │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ Manual Approval │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ DEPLOY TO PRODUCTION │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ Run │ │Blue-Green │ │ Health │ │ Smoke │ │ │
│ │ │Migrations │──▶│ Deploy │──▶│ Check │──▶│ + Notify │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘Workflow валидации PR
Каждый pull request должен пройти все проверки перед разрешением merge.
Этап 1: Сборка и статический анализ
| Проверка | Инструмент | Порог отказа |
|---|---|---|
| Компиляция TypeScript | tsc --noEmit | Любая ошибка |
| ESLint | eslint | Любая ошибка (warnings допустимы) |
| Prettier | prettier --check | Любая проблема форматирования |
| Stylelint (фронтенд) | stylelint | Любая ошибка |
Этап 2: Набор тестов
| Тип теста | Охват | Требование покрытия |
|---|---|---|
| Unit тесты | Доменная логика, утилиты, чистые функции | Минимум 80% |
| Интеграционные тесты | Операции с БД, API эндпоинты, Redis | Минимум 70% |
| E2E тесты | Критические пользовательские сценарии (см. ниже) | Должны проходить |
| Контрактные тесты | Валидация API схемы (см. ниже) | Должны проходить |
Спецификация контрактных тестов
Контрактные тесты проверяют, что реализация API соответствует спецификации и что изменения не ломают потребителей.
Что валидируем:
| Аспект | Инструмент | Описание |
|---|---|---|
| OpenAPI соответствие | @apidevtools/swagger-cli validate | Схема - валидный OpenAPI 3.1 |
| Форма ответа | Кастомные Jest matchers | Ответы соответствуют объявленным схемам |
| Критические изменения | openapi-diff | Обнаружение удалённых эндпоинтов, изменённых типов |
| Валидация запросов | Class-validator + Zod | Входные DTO соответствуют OpenAPI параметрам |
Реализация контрактных тестов:
// test/contract/api-contract.spec.ts
import { OpenAPIValidator } from 'express-openapi-validator';
import spec from '../openapi.json';
describe('API Contract Tests', () => {
const validator = new OpenAPIValidator({ spec });
describe('POST /api/v1/auth/register', () => {
it('should match request schema', async () => {
const validRequest = {
email: 'test@example.com',
password: 'SecurePass123',
referralCode: 'ABC123'
};
expect(() => validator.validateRequest({
path: '/api/v1/auth/register',
method: 'post',
body: validRequest
})).not.toThrow();
});
it('should match response schema', async () => {
const response = await request(app)
.post('/api/v1/auth/register')
.send(validRequest);
expect(() => validator.validateResponse({
path: '/api/v1/auth/register',
method: 'post',
statusCode: response.status,
body: response.body
})).not.toThrow();
});
});
});Обнаружение критических изменений в CI:
- name: Check for Breaking API Changes
run: |
# Сравнение текущей спецификации с main веткой
git show origin/main:openapi.json > openapi-main.json
npx openapi-diff openapi-main.json openapi.json --fail-on-incompatible
# Несовместимые изменения (вызовут отказ):
# - Удаление эндпоинтов
# - Удаление обязательных полей ответа
# - Добавление обязательных полей запроса
# - Изменение типов полей
# Совместимые изменения (разрешены):
# - Добавление новых эндпоинтов
# - Добавление опциональных полей запроса
# - Добавление полей ответаКритические E2E сценарии (всегда должны тестироваться):
1. Поток аутентификации
- Регистрация с реферальным кодом
- Вход с 2FA
- Сброс пароля
- Управление сессиями
2. Поток заказа и оплаты
- Добавить в корзину → Оформление → Оплата → Подтверждение
- Переходы статуса заказа (полный state machine)
- Обработка платёжных webhook
3. Поток расчёта комиссий
- Завершение заказа → Генерация комиссий
- Многоуровневое распределение (до 10 уровней)
- Утверждение комиссии → Выплата
4. Операции с MLM-деревом
- Регистрация партнёра под спонсором
- Запросы обхода дерева
- Проверка квалификации рангаЭтап 3: Проверки безопасности
| Сканирование | Инструмент | Действие при отказе |
|---|---|---|
| Уязвимости зависимостей | npm audit --audit-level=high | Блокировать merge |
| Обнаружение секретов | gitleaks | Блокировать merge |
| SAST | CodeQL / SonarQube | Блокировать при high/critical |
| Уязвимости контейнеров | Trivy | Блокировать при critical |
Этап 4: Валидация базы данных
# Dry-run миграций на тестовой базе данных
- name: Validate Migrations
run: |
# Создание временной базы данных
createdb iwm_migration_test
# Запуск всех миграций
npx prisma migrate deploy --preview-feature
# Проверка соответствия схемы Prisma схеме
npx prisma db pull --force
npx prisma validate
# Очистка
dropdb iwm_migration_testWorkflow релиза
Запускается при merge в ветку main.
Управление версиями
Семантическое версионирование: MAJOR.MINOR.PATCH
Правила автоматического повышения:
- Merge в main → PATCH инкремент (1.2.3 → 1.2.4)
- Ручное изменение MINOR → Пропустить auto-bump, использовать ручную версию
- Ручное изменение MAJOR → Пропустить auto-bump, использовать ручную версию
Версия хранится в: файл VERSION (корень проекта)Этап сборки
| Шаг | Описание |
|---|---|
| Чтение VERSION | Получить текущую или повышенную версию |
| Сборка Docker образов | Backend + Frontend отдельно |
| Тегирование образов | v{VERSION} + latest |
| Push в registry | GitHub Container Registry (ghcr.io) |
| Кэширование слоёв | type=gha,mode=max для быстрой пересборки |
Кэширование Docker слоёв
# Каждая инструкция Dockerfile создаёт слой с SHA256 хэшем
# Неизменённые слои переиспользуются из кэша
FROM node:20-alpine # Layer sha256:a1b2... (кэшируется если без изменений)
COPY package*.json ./ # Layer sha256:c3d4... (кэшируется если package.json тот же)
RUN npm ci # Layer sha256:e5f6... (кэшируется если зависимости те же)
COPY . . # Layer sha256:g7h8... (меняется при изменении кода)
RUN npm run build # Layer sha256:i9j0... (пересобирается при изменении кода)Конфигурация кэша:
cache-from: type=gha # Pull из кэша GitHub Actions
cache-to: type=gha,mode=max # Push ВСЕ слои (не только финальный)Детали проверок безопасности
Сканирование зависимостей
# Запуск на каждом PR и еженедельно по расписанию
- name: Dependency Audit
run: npm audit --audit-level=high
- name: Snyk Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}Сканирование секретов
- name: Gitleaks Scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Защищаемые паттерны:
| Паттерн | Пример |
|---|---|
| API ключи | sk_live_*, pk_live_* |
| URL баз данных | postgresql://*:*@* |
| JWT секреты | JWT_SECRET=* |
| Ключи шифрования | ENCRYPTION_MASTER_KEY=* |
| Webhook секреты | *_WEBHOOK_SECRET=* |
Сканирование контейнеров
- name: Trivy Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE }}
severity: 'CRITICAL,HIGH'
exit-code: '1' # Отказ при critical/highСтратегия миграций базы данных
Валидация миграций (PR)
1. Dry-run на пустой базе данных
2. Dry-run на клоне production (staging)
3. Проверка на деструктивные операции (DROP, TRUNCATE)
4. Оценка продолжительности миграции
5. Флаг миграций, требующих maintenance windowВыполнение миграций (Deploy)
┌─────────────────────────────────────────────────────────────────┐
│ MIGRATION EXECUTION │
│ │
│ 1. Создание backup snapshot │
│ └─▶ pg_dump iwm_production > backup_$(date).sql │
│ │
│ 2. Запуск миграций │
│ └─▶ npx prisma migrate deploy │
│ │
│ 3. Валидация схемы │
│ └─▶ npx prisma validate │
│ │
│ 4. Health check │
│ └─▶ curl /health/ready │
│ │
│ 5. При ошибке: Восстановление из backup │
│ └─▶ psql < backup_$(date).sql │
└─────────────────────────────────────────────────────────────────┘Важно: Prisma
migrate deployзапускает каждый файл миграции в своей транзакции автоматически. НЕ оборачивайте это в BEGIN/COMMIT вручную.
Ограничения транзакций DDL
Некоторые DDL операции PostgreSQL не могут выполняться внутри транзакции. Они требуют специальной обработки:
| Операция | Поддержка транзакций | Обработка |
|---|---|---|
| CREATE INDEX CONCURRENTLY | Нет | Отдельный файл миграции, выполнять вручную |
| ALTER TYPE (enum add value) | Нет (PG < 12) | Отдельная миграция, требует downtime на старом PG |
| DROP INDEX CONCURRENTLY | Нет | Отдельный файл миграции |
| REINDEX CONCURRENTLY | Нет | Выполнять во время maintenance window |
Для нетранзакционных миграций:
-- migrations/20240115_add_index_concurrently.sql
-- @non-transactional
CREATE INDEX CONCURRENTLY idx_orders_created
ON product.orders(created_at);# Пайплайн обрабатывает нетранзакционные миграции отдельно
- name: Run Non-Transactional Migrations
run: |
for file in prisma/migrations/*_non_transactional.sql; do
psql $DATABASE_URL -f "$file" || exit 1
doneПолитика деструктивных миграций
| Операция | Требование |
|---|---|
| DROP TABLE | Требует ручного утверждения + проверки backup |
| DROP COLUMN | Должна предшествовать удалению кода в предыдущем релизе |
| TRUNCATE | Запрещено в production миграциях |
| ALTER TYPE | Требует maintenance window |
Тестирование верификации backup
Backup бесполезны если их нельзя восстановить. Регулярная верификация обеспечивает реальную возможность восстановления.
Расписание верификации:
| Окружение | Частота | Метод |
|---|---|---|
| Production | Еженедельно | Полное восстановление на изолированный инстанс |
| Staging | Ежемесячно | Тест полного восстановления |
| После миграции | Немедленно | Spot check критических таблиц |
Автоматизированная job верификации backup:
# .github/workflows/backup-verification.yml
name: Backup Verification
on:
schedule:
- cron: '0 4 * * 0' # Еженедельно воскресенье 4AM
workflow_dispatch:
jobs:
verify-backup:
runs-on: ubuntu-latest
steps:
- name: Create Fresh Backup
run: |
pg_dump $PRODUCTION_URL \
--format=custom \
--file=backup-$(date +%Y%m%d).dump
- name: Spin Up Isolated Instance
run: |
docker run -d \
--name pg-verify \
-e POSTGRES_PASSWORD=verify_test \
-p 5433:5432 \
postgres:15
sleep 10 # Ожидание запуска
- name: Restore Backup
run: |
pg_restore \
--host=localhost \
--port=5433 \
--username=postgres \
--dbname=postgres \
--clean \
--if-exists \
backup-$(date +%Y%m%d).dump
- name: Verify Data Integrity
run: |
PGPASSWORD=verify_test psql \
-h localhost -p 5433 -U postgres -d postgres \
-f scripts/verify-backup-integrity.sql
- name: Verify Row Counts
run: |
# Сравнение количества строк с production
node scripts/compare-backup-counts.js
- name: Cleanup
if: always()
run: docker rm -f pg-verify
- name: Report Results
run: |
if [ "${{ job.status }}" == "success" ]; then
curl -X POST $SLACK_WEBHOOK \
-d '{"text": ":white_check_mark: Backup verification passed"}'
else
curl -X POST $SLACK_WEBHOOK \
-d '{"text": ":x: Backup verification FAILED - investigate immediately"}'
fiСкрипт проверки целостности:
-- scripts/verify-backup-integrity.sql
-- Проверка существования критических таблиц и наличия данных
DO $$
DECLARE
v_count INT;
BEGIN
-- Таблица users
SELECT COUNT(*) INTO v_count FROM core.users;
IF v_count = 0 THEN
RAISE EXCEPTION 'CRITICAL: users table is empty';
END IF;
RAISE NOTICE 'users: % rows', v_count;
-- Таблица partners
SELECT COUNT(*) INTO v_count FROM mlm.partners;
RAISE NOTICE 'partners: % rows', v_count;
-- Commission transactions
SELECT COUNT(*) INTO v_count FROM mlm.commission_transactions;
RAISE NOTICE 'commission_transactions: % rows', v_count;
-- Таблица orders
SELECT COUNT(*) INTO v_count FROM product.orders;
RAISE NOTICE 'orders: % rows', v_count;
-- Проверка целостности внешних ключей
SELECT COUNT(*) INTO v_count
FROM mlm.partners p
LEFT JOIN core.users u ON p.user_id = u.id
WHERE u.id IS NULL;
IF v_count > 0 THEN
RAISE EXCEPTION 'CRITICAL: % orphaned partner records', v_count;
END IF;
-- Проверка целостности дерева
SELECT COUNT(*) INTO v_count
FROM mlm.partner_tree_paths ptp
LEFT JOIN mlm.partners p ON ptp.ancestor_id = p.id
WHERE p.id IS NULL;
IF v_count > 0 THEN
RAISE EXCEPTION 'CRITICAL: % orphaned tree path records', v_count;
END IF;
RAISE NOTICE 'All integrity checks passed';
END $$;Отслеживание времени восстановления:
| Метрика | Цель | Оповещение если |
|---|---|---|
| Время создания backup | < 30 мин | > 1 час |
| Время восстановления | < 1 час | > 2 часа |
| Время верификации | < 15 мин | > 30 мин |
| Общее RTO | < 2 часа | > 4 часа |
Стратегия окружений
Матрица окружений
| Окружение | Назначение | Триггер деплоя | Данные |
|---|---|---|---|
| Development | Локальная разработка | Ручной | Seed data |
| CI | Тестирование в пайплайне | Каждый PR | Свежие на каждый запуск |
| Staging | Pre-production валидация | Merge в main | Клон production (анонимизированный) |
| Production | Live система | Ручное утверждение | Реальные данные |
Анонимизация данных Staging
Данные production, клонированные в staging, должны быть анонимизированы для защиты приватности пользователей и соответствия GDPR/требованиям защиты данных.
Правила анонимизации по типам данных:
| Категория данных | Поле | Метод анонимизации |
|---|---|---|
| PII | faker.email() с сохранением оригинального домена | |
| PII | phone | +7900${random7digits} |
| PII | first_name, last_name | faker.name() |
| PII | address fields | faker.address() |
| Финансовые | bank_account | ****${last4} (маскирование) |
| Финансовые | card_number | Полностью удаляется |
| Финансовые | balance amounts | Сохраняются (не PII) |
| KYC | passport_number | XX${random8digits} |
| KYC | tax_id | ${random12digits} |
| KYC | document_urls | Заменяются placeholder изображениями |
| Auth | password_hash | Устанавливается известный тестовый хэш пароля |
| Auth | session tokens | Удаляются |
| Auth | 2fa_secrets | Удаляются |
| Audit | ip_address | 192.168.x.x (приватный диапазон) |
| Audit | user_agent | Сохраняется (не PII) |
Скрипт анонимизации:
-- scripts/anonymize-staging.sql
-- Запускать после клонирования production в staging
BEGIN;
-- Таблица users
UPDATE core.users SET
email = 'user_' || id::text || '@staging.iwm.local',
phone = '+7900' || LPAD(FLOOR(RANDOM() * 10000000)::TEXT, 7, '0'),
password_hash = '$2b$10$staging.password.hash.for.testing'; -- Пароль: "staging123"
-- Профили пользователей
UPDATE core.user_profiles SET
first_name = 'Test',
last_name = 'User_' || SUBSTRING(user_id::text, 1, 8),
middle_name = NULL;
-- Адреса
UPDATE product.addresses SET
first_name = 'Test',
last_name = 'User',
address_line1 = FLOOR(RANDOM() * 100)::TEXT || ' Test Street',
address_line2 = 'Apt ' || FLOOR(RANDOM() * 100)::TEXT,
city = 'Test City',
phone = '+7900' || LPAD(FLOOR(RANDOM() * 10000000)::TEXT, 7, '0');
-- KYC документы
UPDATE core.kyc_verifications SET
document_number = 'XX' || LPAD(FLOOR(RANDOM() * 100000000)::TEXT, 8, '0');
UPDATE core.kyc_documents SET
file_url = 'https://staging-assets.iwm.local/placeholder-document.pdf',
file_name = 'anonymized_document.pdf';
-- Детали выплат (чувствительные банковские данные)
UPDATE mlm.payout_requests SET
payout_details = jsonb_set(
payout_details,
'{account_number}',
'"****' || RIGHT(payout_details->>'account_number', 4) || '"'
)
WHERE payout_details ? 'account_number';
-- Удаление чувствительных auth данных
DELETE FROM core.sessions;
DELETE FROM core.two_factor_auth;
-- Audit логи - анонимизация IP
UPDATE core.audit_log SET
ip_address = ('192.168.' || (RANDOM() * 255)::INT || '.' || (RANDOM() * 255)::INT)::INET;
-- Реферальные ссылки партнёров - обновление домена
UPDATE mlm.referral_links SET
full_url = REPLACE(full_url, 'iwm.com', 'staging.iwm.local');
COMMIT;
-- Проверка отсутствия утечки production данных
DO $$
BEGIN
-- Проверка отсутствия реальных email
IF EXISTS (SELECT 1 FROM core.users WHERE email NOT LIKE '%@staging.iwm.local') THEN
RAISE EXCEPTION 'Anonymization failed: real emails found';
END IF;
-- Проверка отсутствия реальных номеров телефонов
IF EXISTS (SELECT 1 FROM core.users WHERE phone NOT LIKE '+7900%') THEN
RAISE EXCEPTION 'Anonymization failed: real phone numbers found';
END IF;
END $$;Автоматизированное обновление staging:
# .github/workflows/staging-refresh.yml
name: Refresh Staging Data
on:
schedule:
- cron: '0 3 * * 0' # Еженедельно воскресенье 3AM
workflow_dispatch: # Ручной запуск
jobs:
refresh-staging:
runs-on: ubuntu-latest
steps:
- name: Create Production Snapshot
run: |
pg_dump $PRODUCTION_URL \
--no-owner \
--no-privileges \
> production-snapshot.sql
- name: Restore to Staging
run: |
psql $STAGING_URL -c "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;"
psql $STAGING_URL < production-snapshot.sql
- name: Run Anonymization
run: psql $STAGING_URL < scripts/anonymize-staging.sql
- name: Verify Anonymization
run: node scripts/verify-anonymization.js
- name: Notify Team
run: |
curl -X POST $SLACK_WEBHOOK \
-d '{"text": "Staging environment refreshed with anonymized production data"}'Контроль доступа к staging:
| Роль | Доступ к Staging | Что видит |
|---|---|---|
| Developer | Полный доступ | Только анонимизированные данные |
| QA | Полный доступ | Только анонимизированные данные |
| Support | Нет доступа | — |
| Внешний подрядчик | Нет доступа | — |
Паритет окружений
# Все окружения используют идентичные:
- Docker образы (тот же SHA)
- Схему базы данных (те же миграции)
- Структуру переменных окружения (разные значения)
- Конфигурацию инфраструктуры (масштабируется по-разному)Валидация переменных окружения
// Валидируется при запуске приложения с помощью Zod
// CI должен проверять определение всех обязательных переменных
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'staging', 'production']),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
ENCRYPTION_MASTER_KEY: z.string().min(32),
// ... все остальные обязательные переменные
});Стратегия деплоя
Blue-Green деплой
┌─────────────────────────────────────────────────────────────────┐
│ BLUE-GREEN DEPLOY │
│ │
│ Текущее состояние: │
│ ┌─────────────┐ │
│ │ BLUE │ ◀── Load Balancer ◀── Traffic │
│ │ (v1.2.3) │ │
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ GREEN │ (idle) │
│ │ (v1.2.3) │ │
│ └─────────────┘ │
│ │
│ Деплой v1.2.4: │
│ 1. Деплой в GREEN │
│ 2. Запуск health checks на GREEN │
│ 3. Запуск smoke tests на GREEN │
│ 4. Переключение трафика на GREEN │
│ 5. Мониторинг ошибок │
│ 6. При ошибках: Переключение обратно на BLUE (откат) │
│ 7. При стабильности: Обновление BLUE до v1.2.4 (синхронизация) │
└─────────────────────────────────────────────────────────────────┘Процедура отката
# Автоматические триггеры отката:
- Отказ health check (3 подряд)
- Процент ошибок > 5% (по сравнению с baseline)
- Время ответа > 2x baseline
# Шаги отката:
1. Переключить load balancer на предыдущую версию
2. Оповестить дежурного инженера
3. Сохранить логи для расследования
4. НЕ запускать обратные миграции автоматическиОкно отката
| Фаза | Длительность | Статус BLUE | Действие при проблемах |
|---|---|---|---|
| Немедленная | 0-15 мин | Работает, без трафика | Мгновенное переключение обратно |
| Краткосрочная | 15 мин - 2 часа | Работает, тёплый резерв | Быстрый откат (< 1 мин) |
| Среднесрочная | 2-24 часа | Остановлен, образ сохранён | Перезапуск BLUE, переключение трафика |
| Долгосрочная | > 24 часов | Терминирован | Редеплой предыдущей версии |
Учёт расхождения данных:
- Откат в течение 2 часов: Минимальное расхождение данных, откат безопасен
- Откат после 2 часов: Аудит новых созданных данных, может потребоваться ручная сверка
- Откат после 24 часов: Требуется план миграции данных, не автоматический
# Конфигурация окна отката
rollback:
instant_window: 15m # BLUE работает
warm_standby: 2h # BLUE остановлен но не терминирован
image_retention: 24h # Предыдущий образ сохраняется в registry
max_auto_rollback: 2h # После этого требуется ручное утверждениеУправление соединениями с БД во время деплоя
Во время blue-green деплоя обе версии могут работать одновременно. Это требует аккуратного управления пулом соединений.
┌─────────────────────────────────────────────────────────────────┐
│ CONNECTION POOL DURING DEPLOY │
│ │
│ Database Pool Limit: 100 connections │
│ │
│ Нормальная работа: │
│ ┌─────────────┐ │
│ │ BLUE │ ─── 50 connections ───▶ ┌──────────────┐ │
│ │ (active) │ │ │ │
│ └─────────────┘ │ PostgreSQL │ │
│ │ │ │
│ Во время деплоя (оба работают): │ Pool: 100 │ │
│ ┌─────────────┐ │ │ │
│ │ BLUE │ ─── 40 connections ───▶ │ │ │
│ │ (draining) │ │ │ │
│ └─────────────┘ │ │ │
│ ┌─────────────┐ │ │ │
│ │ GREEN │ ─── 40 connections ───▶ │ │ │
│ │ (starting) │ └──────────────┘ │
│ └─────────────┘ │
│ │
│ Reserve: 20 connections для миграций и админских операций │
└─────────────────────────────────────────────────────────────────┘Конфигурация пула соединений:
// config/database.ts
const poolConfig = {
// Нормальная работа
default: {
min: 5,
max: 50,
},
// Во время деплоя (определяется через DEPLOY_MODE env)
deployment: {
min: 2,
max: 40, // Уменьшено для допуска перекрытия
},
};Последовательность деплоя для предотвращения исчерпания соединений:
deploy_steps:
1. Установить GREEN pool в режим deployment (max: 40)
2. Запустить GREEN инстансы
3. Дождаться health check GREEN
4. Запустить миграции (используют зарезервированные соединения)
5. Постепенно переключать трафик (10% → 50% → 100%)
6. Установить BLUE в режим drain (прекратить приём новых соединений)
7. Дождаться закрытия соединений BLUE (max 30s)
8. Остановить BLUE инстансы
9. Установить GREEN pool в нормальный режим (max: 50)PgBouncer рекомендуется для production:
# pgbouncer.ini
[pgbouncer]
pool_mode = transaction
max_client_conn = 200
default_pool_size = 50
reserve_pool_size = 10
reserve_pool_timeout = 3Health Checks
// /health/live - Работает ли процесс?
// Возвращает 200 если процесс жив
// /health/ready - Может ли обрабатывать запросы?
// Проверки:
// - Соединение с базой данных
// - Соединение с Redis
// - Доступность необходимых сервисов
// /health/startup - Завершена ли инициализация?
// Используется Kubernetes чтобы знать когда отправлять трафикТребования к тестам
Пороги покрытия
{
"coverageThreshold": {
"global": {
"branches": 75,
"functions": 80,
"lines": 80,
"statements": 80
},
"src/modules/mlm/domain/**": {
"branches": 90,
"functions": 95,
"lines": 95
},
"src/modules/payment/domain/**": {
"branches": 90,
"functions": 95,
"lines": 95
}
}
}Наборы тестов для платформы
| Набор | Фокус | Триггер |
|---|---|---|
| Commission Tests | Многоуровневые расчёты, edge cases, округление | Каждый PR |
| Tree Operation Tests | INSERT, MOVE, лимиты глубины, предотвращение циклов | Каждый PR |
| Payment Integration | Обработка webhook, идемпотентность, возвраты | Каждый PR |
| State Machine Tests | Переходы заказов, предотвращение невалидных состояний | Каждый PR |
| Encryption Tests | Цикл encrypt/decrypt, ротация ключей | Каждый PR |
| Load Tests | Расчёт комиссий под нагрузкой | Еженедельно / Pre-release |
Спецификации нагрузочных тестов
Нагрузочные тесты проверяют производительность системы в реалистичных и стрессовых условиях.
Целевые метрики:
| Сценарий | Цель | Порог (отказ если) |
|---|---|---|
| Расчёт комиссий | 100 заказов/сек | < 50 заказов/сек |
| Конкурентные запросы выплат | 50 запросов/сек | < 25 запросов/сек |
| Обход дерева (10 уровней) | < 50ms p95 | > 200ms p95 |
| Время ответа API (p95) | < 200ms | > 500ms |
| Соединения с БД | Стабильно на max pool | Исчерпание пула |
| Использование памяти | < 512MB на инстанс | > 1GB |
Сценарии нагрузочных тестов:
// load-tests/k6/commission-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
// Sustained load: Нормальная работа
sustained: {
executor: 'constant-arrival-rate',
rate: 100, // 100 заказов в секунду
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
},
// Spike: Симуляция flash sale
spike: {
executor: 'ramping-arrival-rate',
startRate: 100,
timeUnit: '1s',
stages: [
{ duration: '1m', target: 100 }, // Нормальный
{ duration: '30s', target: 500 }, // Всплеск до 5x
{ duration: '2m', target: 500 }, // Поддержание всплеска
{ duration: '30s', target: 100 }, // Возврат к нормальному
],
preAllocatedVUs: 200,
},
// Stress: Поиск точки отказа
stress: {
executor: 'ramping-arrival-rate',
startRate: 50,
timeUnit: '1s',
stages: [
{ duration: '2m', target: 200 },
{ duration: '2m', target: 400 },
{ duration: '2m', target: 600 },
{ duration: '2m', target: 800 }, // Поиск где ломается
],
preAllocatedVUs: 300,
},
},
thresholds: {
http_req_duration: ['p(95)<500'], // 95% запросов < 500ms
http_req_failed: ['rate<0.01'], // Процент ошибок < 1%
'commission_calculated': ['rate>50'], // Минимум 50/s обработано
},
};
export default function () {
const orderPayload = {
userId: `user_${__VU}_${__ITER}`,
amount: Math.floor(Math.random() * 10000) + 1000,
referringPartnerId: 'partner_test_001',
};
const res = http.post(
`${__ENV.API_URL}/api/v1/orders`,
JSON.stringify(orderPayload),
{ headers: { 'Content-Type': 'application/json' } }
);
check(res, {
'order created': (r) => r.status === 201,
'commission triggered': (r) => r.json('commissionJobId') !== null,
});
sleep(0.1);
}Поведение соединений с БД под нагрузкой:
# Мониторинг во время нагрузочных тестов
metrics:
- pg_stat_activity.active_connections
- pg_stat_activity.waiting_connections
- pg_stat_user_tables.seq_scan (не должен резко расти)
- pg_stat_user_tables.idx_scan (должен справляться с нагрузкой)
- pg_locks.blocked_queries (должен быть ноль)Интеграция в CI:
- name: Run Load Tests (Pre-release)
if: github.event_name == 'release'
run: |
k6 run load-tests/k6/commission-load.js \
--env API_URL=${{ secrets.STAGING_URL }} \
--out json=load-test-results.json
# Отказ релиза если пороги не пройдены
node scripts/validate-load-test.js load-test-results.jsonМониторинг и уведомления
Уведомления о деплое
# Уведомлять о:
- Деплой начат (staging/production)
- Деплой успешен
- Деплой провален
- Откат инициирован
- Требуется ручное утверждение
# Каналы:
- Slack / Telegram
- Email (для провалов)
- PagerDuty (для production провалов)Post-Deploy верификация
# Smoke tests запускаются сразу после деплоя:
1. GET /health/ready → 200
2. POST /api/v1/auth/login (тестовый пользователь) → 200 + JWT
3. GET /api/v1/products → 200 + валидный ответ
4. GET /api/v1/mlm/ranks → 200 + валидный ответ
# При любом отказе → инициировать откатМетрики для мониторинга после деплоя
| Метрика | Сравнение с baseline | Порог оповещения |
|---|---|---|
| Процент ошибок | vs. предыдущий час | > 2x baseline |
| Время ответа (p95) | vs. предыдущий час | > 1.5x baseline |
| Время запросов к БД | vs. предыдущий час | > 2x baseline |
| Использование памяти | vs. предыдущий деплой | > 120% |
| Использование CPU | vs. предыдущий деплой | > 150% |
Процедура Hotfix
Когда production сломан и требуется быстрое исправление, процедура hotfix позволяет обойти нормальный поток сохраняя безопасность.
Когда использовать Hotfix
| Ситуация | Использовать Hotfix? | Нормальный деплой OK? |
|---|---|---|
| Production не работает / 500 ошибки | Да | Нет |
| Критическая уязвимость безопасности | Да | Нет |
| Обработка платежей сломана | Да | Нет |
| Расчёт комиссий неправильный | Да | Нет |
| Мелкий баг, пользователи не затронуты | Нет | Да |
| Деградация производительности (< 2x) | Нет | Да |
| Фича не работает как ожидалось | Нет | Да |
Поток Hotfix
┌─────────────────────────────────────────────────────────────────┐
│ HOTFIX PROCEDURE │
│ │
│ 1. ОЦЕНКА (5 мин max) │
│ └─▶ Подтвердить серьёзность, определить root cause │
│ └─▶ Решение: Hotfix vs Откат vs Ожидание │
│ │
│ 2. ВЕТКА │
│ └─▶ git checkout -b hotfix/ISSUE-ID main │
│ └─▶ НЕ от feature ветки │
│ │
│ 3. ИСПРАВЛЕНИЕ │
│ └─▶ Только минимальное изменение │
│ └─▶ Без рефакторинга │
│ └─▶ Без "раз уж мы тут" дополнений │
│ │
│ 4. ВАЛИДАЦИЯ (Сокращённая) │
│ └─▶ Unit тесты для изменённого кода │
│ └─▶ Type check │
│ └─▶ Ручной smoke test │
│ └─▶ Пропустить: Full E2E, Load tests, SAST │
│ │
│ 5. УТВЕРЖДЕНИЕ │
│ └─▶ Один reviewer (senior engineer) │
│ └─▶ PR не требуется (прямой push с утверждением) │
│ │
│ 6. ДЕПЛОЙ │
│ └─▶ Прямо в production (пропустить staging) │
│ └─▶ Наблюдать за метриками 15 минут │
│ │
│ 7. ПОСЛЕДУЮЩИЕ ДЕЙСТВИЯ (в течение 24 часов) │
│ └─▶ Создать правильный PR backport в main │
│ └─▶ Добавить регрессионный тест │
│ └─▶ Написать incident report │
└─────────────────────────────────────────────────────────────────┘Команды Hotfix
# 1. Создать hotfix ветку от production тега
git fetch --tags
git checkout -b hotfix/IWM-123-fix-commission-calc v1.2.3
# 2. Сделать исправление
# ... изменения кода ...
# 3. Запустить сокращённые тесты
npm run test:unit -- --testPathPattern="commission"
npm run type-check
# 4. Деплой напрямую (требует HOTFIX_APPROVED=true)
HOTFIX_APPROVED=true npm run deploy:production
# 5. Тегировать hotfix
git tag -a v1.2.3-hotfix.1 -m "Hotfix: Commission calculation overflow"
git push origin v1.2.3-hotfix.1
# 6. Backport в main
git checkout main
git cherry-pick <hotfix-commit-sha>
git push origin mainМатрица утверждения Hotfix
| Тип исправления | Требуемый утверждающий | Может утвердить сам |
|---|---|---|
| Логическое исправление (без БД) | 1 senior engineer | Нет |
| Изменение конфигурации | 1 engineer | Да (если дежурный) |
| Исправление БД | 2 engineers + DBA | Нет |
| Исправление безопасности | 1 security + 1 engineer | Нет |
| Откат к предыдущей версии | 1 engineer | Да (если дежурный) |
Дежурство и эскалация
Путь эскалации
┌─────────────────────────────────────────────────────────────────┐
│ ESCALATION LADDER │
│ │
│ Level 0: Автоматизация │
│ └─▶ Health check отказ → Авто-откат │
│ └─▶ Процент ошибок > 5% → Авто-откат │
│ └─▶ Оповещение в #alerts канал │
│ │
│ Level 1: Дежурный инженер (0-15 мин) │
│ └─▶ Получить PagerDuty alert │
│ └─▶ Acknowledge в течение 5 минут │
│ └─▶ Оценка: Можно исправить самому? Нужна эскалация? │
│ │
│ Level 2: Senior Engineer (15-30 мин) │
│ └─▶ Авто-эскалация если L1 не acknowledge │
│ └─▶ Присоединиться к incident call │
│ └─▶ Решение: Hotfix vs Откат vs Внешняя помощь │
│ │
│ Level 3: Engineering Lead + Команда (30+ мин) │
│ └─▶ Несколько инженеров на связи │
│ └─▶ Координация со стейкхолдерами │
│ └─▶ Коммуникация с клиентами если нужно │
│ │
│ Level 4: Executive (Major Incident) │
│ └─▶ Продолжительный outage (> 1 часа) │
│ └─▶ Утечка данных / Инцидент безопасности │
│ └─▶ Финансовое влияние (платежи затронуты) │
└─────────────────────────────────────────────────────────────────┘Конфигурация PagerDuty
# pagerduty-config.yml
services:
- name: IWM Production
escalation_policy: iwm-production
alert_creation: create_alerts_and_incidents
escalation_policies:
- name: iwm-production
rules:
- escalation_delay_minutes: 5
targets:
- type: schedule_reference
id: primary-oncall
- escalation_delay_minutes: 15
targets:
- type: schedule_reference
id: senior-oncall
- escalation_delay_minutes: 30
targets:
- type: user_reference
id: engineering-lead
- type: user_reference
id: cto
schedules:
- name: primary-oncall
rotation: weekly
users: [engineer_1, engineer_2, engineer_3, engineer_4]
- name: senior-oncall
rotation: weekly
users: [senior_1, senior_2]Уровни серьёзности оповещений
| Серьёзность | Время реагирования | Примеры |
|---|---|---|
| P1 - Критический | 5 мин | Production не работает, платежи не проходят, утечка данных |
| P2 - Высокий | 15 мин | Важная фича сломана, процент ошибок > 5% |
| P3 - Средний | 1 час | Деградация производительности, некритические ошибки |
| P4 - Низкий | Следующий рабочий день | Мелкие баги, оповещения мониторинга |
Коммуникация при инциденте
# Во время инцидента:
channels:
- "#incident-active" # Обновления в реальном времени (только инженеры)
- "#engineering" # Обновления статуса (каждые 30 мин)
- "#general" # Статус для клиентов (если нужно)
templates:
initial_alert: |
:rotating_light: **INCIDENT DETECTED**
Severity: {severity}
Service: {service}
Description: {description}
On-call: @{oncall_user}
Incident channel: #incident-{id}
status_update: |
**Incident Update** ({time} с начала)
Status: {investigating|identified|fixing|monitoring|resolved}
Impact: {impact_description}
Следующее обновление: {eta}
resolution: |
:white_check_mark: **INCIDENT RESOLVED**
Duration: {duration}
Root cause: {root_cause}
Fix applied: {fix_description}
Follow-up: {follow_up_ticket}Ротация секретов
Расписание ротации
| Секрет | Частота ротации | Авто-ротация | Требуется downtime |
|---|---|---|---|
| JWT_SECRET | 90 дней | Нет | Нет (период двойного ключа) |
| ENCRYPTION_MASTER_KEY | 180 дней | Нет | Да (перешифрование) |
| Пароль БД | 90 дней | Да (через cloud) | Нет |
| API ключи (внешние) | 365 дней | Варьируется | Нет |
| Webhook секреты | 180 дней | Нет | Требуется координация |
Ротация JWT Secret (Zero Downtime)
// Поддержка двойных JWT секретов во время ротации
const jwtConfig = {
// Текущий секрет (для подписи новых токенов)
current: process.env.JWT_SECRET,
// Предыдущий секрет (для валидации старых токенов во время ротации)
previous: process.env.JWT_SECRET_PREVIOUS || null,
// Окно ротации (как долго принимать старые токены)
rotationWindowDays: 7,
};
// Валидация принимает оба секрета
function validateToken(token: string): JwtPayload {
try {
return jwt.verify(token, jwtConfig.current);
} catch (e) {
if (jwtConfig.previous) {
return jwt.verify(token, jwtConfig.previous);
}
throw e;
}
}Процедура ротации:
jwt_rotation_steps:
1. Сгенерировать новый JWT_SECRET
2. Установить JWT_SECRET_PREVIOUS = текущий JWT_SECRET
3. Установить JWT_SECRET = новый секрет
4. Деплой (оба секрета теперь валидны)
5. Ждать 7 дней (токены истекают, refresh использует новый секрет)
6. Удалить JWT_SECRET_PREVIOUS
7. Деплой финальной конфигурацииРотация ключа шифрования
Ротация ключа шифрования требует перешифрования существующих данных.
// Версионирование ключей в зашифрованных данных
interface EncryptedField {
version: string; // 'v1', 'v2', и т.д.
ciphertext: string;
iv: string;
}
// Расшифровка с поддержкой версий
async function decrypt(field: EncryptedField): Promise<string> {
const key = getKeyByVersion(field.version);
return decryptWithKey(field.ciphertext, field.iv, key);
}
// Фоновая job перешифрования
async function reEncryptAllData(fromVersion: string, toVersion: string) {
const batchSize = 1000;
let offset = 0;
while (true) {
const records = await db.taxIdentification.findMany({
where: { encryptedData: { path: ['version'], equals: fromVersion } },
take: batchSize,
skip: offset,
});
if (records.length === 0) break;
for (const record of records) {
const decrypted = await decrypt(record.encryptedData);
const reEncrypted = await encrypt(decrypted, toVersion);
await db.taxIdentification.update({
where: { id: record.id },
data: { encryptedData: reEncrypted },
});
}
offset += batchSize;
logger.info(`Re-encrypted ${offset} records`);
}
}Процедура ротации:
encryption_key_rotation:
1. Сгенерировать новый ключ (ENCRYPTION_MASTER_KEY_V2)
2. Деплой с обоими ключами доступными
3. Новые шифрования используют v2
4. Запустить фоновую job перешифрования
5. Мониторить завершение job (может занять часы)
6. Проверить что все записи v2
7. Удалить старый ключ из конфигурации
8. Безопасно удалить старый ключ из менеджера секретовРотация Webhook секретов
Внешние webhook (платёжные провайдеры) требуют координации.
webhook_rotation:
stripe:
1. Сгенерировать новый webhook в Stripe dashboard
2. Добавить новый STRIPE_WEBHOOK_SECRET_V2 в конфигурацию
3. Деплой (принимать оба секрета)
4. Отключить старый webhook в Stripe dashboard
5. Удалить старый секрет из конфигурации
coordination: Self-service, без downtime
payment_provider:
1. Связаться с поддержкой провайдера
2. Запланировать окно ротации
3. Провайдер отправляет тестовый webhook с новым секретом
4. Подтвердить получение
5. Провайдер переключается на новый секрет
6. Обновить конфигурацию
coordination: Зависит от провайдера, может требовать окноПриоритет реализации
Обязательно (Фаза 1)
| Компонент | Причина |
|---|---|
| Build + Type check | Базовая корректность |
| Unit тесты (80%) | Точность финансовых расчётов |
| Интеграционные тесты | Корректность операций с БД |
| npm audit | Соответствие OWASP A06 |
| Сканирование секретов | Предотвращение утечки credentials |
| Валидация миграций | Целостность базы данных |
| Health checks | Верификация деплоя |
| Staging окружение | Pre-production валидация |
Желательно (Фаза 2)
| Компонент | Причина |
|---|---|
| E2E тесты (критические сценарии) | Валидация пользовательских путей |
| Сканирование контейнеров (Trivy) | Безопасность инфраструктуры |
| SAST (CodeQL) | Уязвимости на уровне кода |
| Blue-green деплой | Релизы без downtime |
| Автоматический откат | Быстрое восстановление |
| Уведомления о деплое | Осведомлённость команды |
| Coverage gates | Предотвращение регрессии |
Желательно (Фаза 3)
| Компонент | Причина |
|---|---|
| Preview environments | Тестирование PR |
| Нагрузочное тестирование | Валидация производительности |
| Визуальная регрессия | Консистентность UI |
| Авто-changelog | Документация релизов |
| Авто-обновление зависимостей | Автоматизация поддержки |
| Feature flags | Постепенные rollouts |
| Chaos testing | Верификация устойчивости |