10 инфраструктурных антипаттернов стартапа: как мы спасали РокетАпп от технического долга

Исходная ситуация: стартап на грани коллапса

В марте 2026 года к нам обратился CTO стартапа РокетАпп — мобильное приложение для доставки еды, работающее в трёх городах. Команда — 12 разработчиков, 4 сервера в облаке, 15 000 активных пользователей в день. Компания выросла за год с 2 до 12 человек, и инфраструктура, которую «временно» настроил один из основателей, начала рассыпаться.

За последний месяц произошло:

  • Полная потеря базы данных (48 часов простоя, восстановление из дампа недельной давности)
  • Утечка API-ключей платёжной системы в публичный GitHub-репозиторий
  • Три деплоя, которые положили production на 2-4 часа каждый
  • Увольнение разработчика, который унёс с собой единственный SSH-ключ от сервера мониторинга

Мы провели аудит инфраструктуры за 3 дня и нашли 10 критических антипаттернов. Каждый из них — типичная ошибка стартапов, которые быстро растут.

Антипаттерн 1: root SSH повсюду

Проблема: Все 12 разработчиков подключались к серверам по SSH как root. Один пароль root был расшарен в общем чате Slack. Никакого аудита — невозможно понять, кто что менял.

Реальный инцидент: Джуниор-разработчик запустил rm -rf /var/log/ на production-сервере, пытаясь освободить место. Удалил не только логи, но и сокеты — MySQL и Nginx упали. Кто именно это сделал, выяснили только через 2 дня, опрашивая каждого.

Решение:

# 1. Создаём именные аккаунты для каждого разработчика
for user in ivanov petrov sidorov; do
    useradd -m -s /bin/bash -G sudo "$user"
    mkdir -p /home/$user/.ssh
    # Каждый генерирует свой ключ: ssh-keygen -t ed25519
    # Публичный ключ добавляется на сервер
    echo "ssh-ed25519 AAAA... $user@rocketapp" >> /home/$user/.ssh/authorized_keys
    chmod 700 /home/$user/.ssh
    chmod 600 /home/$user/.ssh/authorized_keys
    chown -R $user:$user /home/$user/.ssh
done

# 2. Отключаем root-доступ по SSH
sed -i 's/^PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd

# 3. Настраиваем sudo с логированием
echo 'Defaults logfile="/var/log/sudo.log"' >> /etc/sudoers.d/logging
echo 'Defaults log_input, log_output' >> /etc/sudoers.d/logging
echo 'Defaults iolog_dir="/var/log/sudo-io"' >> /etc/sudoers.d/logging

Теперь каждое действие под sudo логируется с указанием пользователя, времени и команды. Увольнение сотрудника = удаление его аккаунта, а не смена общего пароля на всех серверах.

Антипаттерн 2: секреты в Git и нет staging

Проблема (секреты): API-ключи, пароли от БД и токены платёжных систем хранились в файле config.py, который был в Git. Репозиторий — публичный на GitHub ("чтобы фрилансеру удобнее было").

Реальный инцидент: Бот на GitHub обнаружил API-ключ Stripe в коммите через 14 минут после пуша. За эти 14 минут злоумышленник успел сделать 3 возврата на общую сумму $2 300. Stripe заблокировал аккаунт, клиенты не могли оплатить заказы 6 часов.

Решение:

# 1. Удаляем секреты из истории Git (BFG Repo-Cleaner)
java -jar bfg.jar --replace-text passwords.txt repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force  # после этого все ротируем ключи!

# 2. Переносим секреты в переменные окружения
# .env (добавлен в .gitignore!)
STRIPE_SECRET_KEY=sk_live_...
DATABASE_URL=postgresql://app:password@db:5432/rocketapp
REDIS_URL=redis://:password@redis:6379/0
SENTRY_DSN=https://key@sentry.io/123

# .env.example (в Git — без реальных значений)
STRIPE_SECRET_KEY=sk_test_CHANGE_ME
DATABASE_URL=postgresql://app:password@localhost:5432/rocketapp_dev

# 3. Pre-commit hook для защиты от случайных коммитов секретов
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

Проблема (нет staging): Код деплоился напрямую из ветки main в production. Тестирование — «на моём ноутбуке работает».

Решение:

# Docker Compose для staging-окружения (идентичное production)
# docker-compose.staging.yml
version: '3.8'
services:
  app:
    image: rocketapp/api:${GIT_SHA}
    environment:
      - DATABASE_URL=postgresql://app:staging_pass@db:5432/rocketapp_staging
      - STRIPE_SECRET_KEY=sk_test_...  # тестовый ключ Stripe
      - ENV=staging
    depends_on:
      - db
      - redis
  db:
    image: postgres:15
    volumes:
      - staging_db:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: rocketapp_staging
      POSTGRES_PASSWORD: staging_pass
  redis:
    image: redis:7-alpine

После внедрения staging количество "деплоев, которые ломают production" упало с 3 в месяц до 0.

Антипаттерн 3: нет бэкапов и ручные деплои

Проблема (бэкапы): Единственный бэкап — pg_dump, который основатель запускал вручную «когда вспоминал». Последний бэкап — неделю назад. Бэкап лежал на том же сервере, что и база.

Реальный инцидент: Во время неудачного ALTER TABLE разработчик уронил production-базу. Последний бэкап был 7-дневной давности. Потеряно 12 000 заказов, 3 400 новых пользователей, все транзакции за неделю. Восстановление заняло 48 часов. Финансовые потери — 1.2 миллиона рублей.

Решение:

#!/bin/bash
# /opt/scripts/backup_postgresql.sh

set -euo pipefail

BACKUP_DIR="/backup/postgresql"
S3_BUCKET="s3://rocketapp-backups/postgresql"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/rocketapp_${DATE}.sql.gz"

# Создаём дамп с сжатием
pg_dump -h localhost -U app -Fc rocketapp | gzip > "$BACKUP_FILE"

# Проверяем целостность дампа
if ! pg_restore -l "$BACKUP_FILE" > /dev/null 2>&1; then
    echo "BACKUP VERIFICATION FAILED" >&2
    curl -s -X POST "https://api.telegram.org/bot${TG_BOT}/sendMessage" \
        -d chat_id="$TG_CHAT" \
        -d text="BACKUP FAILED: verification error on $(hostname)"
    exit 1
fi

# Копируем в S3 (другой дата-центр)
aws s3 cp "$BACKUP_FILE" "$S3_BUCKET/" --storage-class STANDARD_IA

# Удаляем старые локальные бэкапы
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +${RETENTION_DAYS} -delete

# Отправляем отчёт
SIZE=$(du -sh "$BACKUP_FILE" | awk '{print $1}')
curl -s -X POST "https://api.telegram.org/bot${TG_BOT}/sendMessage" \
    -d chat_id="$TG_CHAT" \
    -d text="Backup OK: $BACKUP_FILE ($SIZE), uploaded to S3"

# Systemd timer: каждые 6 часов
# /etc/systemd/system/backup-postgresql.timer
# [Timer]
# OnCalendar=*-*-* 00/6:00:00
# Persistent=true

Проблема (ручные деплои): Деплой = SSH на сервер, git pull, pip install -r requirements.txt, systemctl restart app. Каждый раз что-то забывали: миграцию, зависимость, перезапуск воркеров.

Решение — GitHub Actions CI/CD:

# .github/workflows/deploy.yml
name: Deploy to Production
on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v --tb=short
      - run: flake8 app/ --max-line-length=120

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Build and push Docker image
        run: |
          docker build -t rocketapp/api:${{ github.sha }} .
          docker push rocketapp/api:${{ github.sha }}
      - name: Deploy to production
        run: |
          ssh deploy@prod "docker pull rocketapp/api:${{ github.sha }} && \
            docker-compose up -d --no-deps app && \
            docker exec app python manage.py migrate --noinput"

Антипаттерн 4: нет мониторинга и single point of failure

Проблема (мониторинг): Ноль мониторинга. О проблемах узнавали из звонков клиентов или 1-звёздочных отзывов в App Store. Один раз сервер лежал 4 часа ночью — узнали утром из Telegram-чата курьеров.

Решение — стек мониторинга за 2 часа:

# docker-compose.monitoring.yml
version: '3.8'
services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    volumes:
      - grafana_data:/var/lib/grafana
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASS}

  alertmanager:
    image: prom/alertmanager:latest
    volumes:
      - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml

  node-exporter:
    image: prom/node-exporter:latest
    pid: host
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro

  postgres-exporter:
    image: quay.io/prometheuscommunity/postgres-exporter:latest
    environment:
      DATA_SOURCE_NAME: "postgresql://monitor:pass@db:5432/rocketapp?sslmode=disable"
# alertmanager.yml — алерты в Telegram
route:
  receiver: 'telegram'
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

receivers:
  - name: 'telegram'
    telegram_configs:
      - bot_token: '7012345678:AAH...'
        chat_id: -100123456789
        parse_mode: 'HTML'
        message: |
          {{ range .Alerts }}
          {{ .Labels.alertname }}
          {{ .Annotations.description }}
          {{ end }}

Проблема (SPOF): Один сервер PostgreSQL, один сервер API, один Redis. Падение любого компонента = полный простой.

Решение — устранение критических SPOF:

# PostgreSQL: streaming replication (primary → standby)
# На standby-сервере:
# postgresql.conf
primary_conninfo = 'host=primary-db port=5432 user=replicator password=...
    application_name=standby1'
hot_standby = on

# Автоматический failover через Patroni
# /etc/patroni/config.yml
scope: rocketapp-cluster
name: node1
restapi:
  listen: 0.0.0.0:8008
bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      parameters:
        max_connections: 200
        shared_buffers: 8GB

После внедрения мониторинга среднее время обнаружения проблемы сократилось с 2-4 часов до 60 секунд.

Антипаттерн 5: нет документации и общие базы данных

Проблема (документация): Знания о том, как работает инфраструктура, существовали только в голове основателя. Когда он ушёл в отпуск на 2 недели, команда не смогла сделать rollback неудачного деплоя — никто не знал, где лежат бэкапы и как откатить миграцию.

Решение — runbook в Git:

# docs/runbooks/deploy-rollback.md
## Откат деплоя

### Быстрый откат (< 5 минут)
```bash
# Посмотреть предыдущую версию
docker ps --format '{{.Image}}' | grep rocketapp
# Откатить на предыдущий образ
docker-compose up -d --no-deps app  # image tag в .env
```

### Откат миграции БД
```bash
# Посмотреть текущую миграцию
python manage.py showmigrations | grep '\[X\]' | tail -5
# Откатить последнюю миграцию
python manage.py migrate app_name 0042_previous_migration
```

### Эскалация
- Дежурный инженер: @oncall в Slack
- CTO: +7-xxx-xxx-xx-xx (только если даунтайм > 30 мин)

Проблема (общие БД): Три микросервиса (API, воркер очередей, аналитика) работали с одной базой PostgreSQL напрямую. Аналитический запрос с 5-минутным JOIN блокировал таблицу orders, и курьеры не могли принять заказы.

Решение:

-- 1. Read replica для аналитики
-- Аналитические запросы идут на standby
-- connection string для аналитики:
-- DATABASE_URL_READONLY=postgresql://analyst:pass@standby-db:5432/rocketapp

-- 2. Отдельные пользователи БД с ограниченными правами
CREATE USER app_api WITH PASSWORD '...';
GRANT SELECT, INSERT, UPDATE ON orders, users, deliveries TO app_api;
REVOKE DELETE ON orders FROM app_api;  -- API не может удалять заказы

CREATE USER app_worker WITH PASSWORD '...';
GRANT SELECT, UPDATE ON orders TO app_worker;
GRANT INSERT ON delivery_logs TO app_worker;

CREATE USER app_analytics WITH PASSWORD '...';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_analytics;
-- Аналитик может только читать

-- 3. statement_timeout для аналитики — не более 60 секунд
ALTER USER app_analytics SET statement_timeout = '60s';

Антипаттерн 6: нет ресурсных лимитов

Проблема: Docker-контейнеры работали без лимитов CPU и RAM. Один контейнер мог сожрать всю память сервера. Также не было лимитов на подключения к PostgreSQL — при всплеске трафика API открывал 500+ соединений, и PostgreSQL падал с ошибкой too many connections.

Реальный инцидент: Celery-воркер при обработке большого CSV-файла с заказами выделил 28 GB RAM (читал весь файл в память). OOM Killer убил PostgreSQL — как самый «жирный» процесс. База данных упала, 20 минут простоя.

Решение:

# docker-compose.yml — лимиты ресурсов
services:
  app:
    image: rocketapp/api:latest
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 1G
    # Healthcheck для автоматического перезапуска
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  worker:
    image: rocketapp/worker:latest
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G  # Воркер не сможет сожрать 28 GB

  db:
    image: postgres:15
    deploy:
      resources:
        limits:
          memory: 16G
    # OOM score adj — PostgreSQL последний в очереди на убийство
    # В systemd: OOMScoreAdjust=-900
# PgBouncer — пул соединений перед PostgreSQL
# /etc/pgbouncer/pgbouncer.ini
[databases]
rocketapp = host=127.0.0.1 port=5432 dbname=rocketapp

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

# Ключевые лимиты
pool_mode = transaction           # Переиспользование соединений между транзакциями
max_client_conn = 500             # Максимум от приложений
default_pool_size = 25            # Реальных соединений к PostgreSQL
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3

# Таймауты
server_idle_timeout = 600
client_idle_timeout = 0
query_timeout = 30                # Запрос дольше 30 сек — убиваем

После внедрения PgBouncer количество реальных соединений к PostgreSQL сократилось с 500+ до стабильных 25, а OOM-инциденты прекратились полностью.

Результаты: 3 месяца спустя

За 3 недели активной работы и 2 месяца поддержки мы устранили все 10 антипаттернов. Результаты:

МетрикаДоПосле
Простой production в месяц8-12 часов15 минут
Время обнаружения проблемы2-4 часа60 секунд
Время деплоя30-45 минут (ручной)5 минут (CI/CD)
Частота потери данных1 раз в 3 месяца0
БэкапыРучные, иногдаКаждые 6 часов + WAL-архивирование
Инцидентов безопасности2 за квартал0

Чек-лист для стартапов от инженеров itfresh.ru — минимальный набор практик, который нужно внедрить до первого пользователя:

  • Именные SSH-аккаунты с ключами, root отключён
  • Секреты — в переменных окружения или HashiCorp Vault, никогда в Git
  • Автоматические бэкапы каждые 6 часов + тест восстановления раз в неделю
  • CI/CD с тестами — код не попадает в production без прохождения пайплайна
  • Staging-окружение, идентичное production
  • Мониторинг + алерты в Telegram/Slack
  • Лимиты ресурсов на все контейнеры
  • PgBouncer перед PostgreSQL
  • Runbook-документация для типовых операций
  • Регулярный аудит инфраструктуры (раз в квартал)

Часто задаваемые вопросы

Три приоритета: бэкапы, мониторинг, CI/CD — именно в таком порядке. Бэкапы спасают от катастрофы (1 день на настройку). Мониторинг позволяет узнавать о проблемах быстрее клиентов (2-3 часа). CI/CD предотвращает «деплои, которые всё ломают» (1-2 дня). Всё остальное — следующий этап.
Используйте BFG Repo-Cleaner — он в 10-720x быстрее git filter-branch. Команда: java -jar bfg.jar --replace-text passwords.txt repo.git. После очистки обязательно ротируйте ВСЕ утёкшие секреты — считайте их скомпрометированными. Также добавьте pre-commit hook с detect-secrets для предотвращения повторных утечек.
Да, даже при 10 пользователях. PostgreSQL создаёт отдельный процесс на каждое соединение (~10 MB RAM). При 200 соединениях это 2 GB только на процессы. PgBouncer в режиме transaction pooling позволяет обслуживать 500 клиентов через 20-30 реальных соединений. Установка занимает 15 минут, а при росте нагрузки не придётся перестраивать архитектуру.
Docker Compose на отдельной VM — самый дешёвый вариант. VM с 4 GB RAM стоит 500-1000 рублей в месяц. Staging должен быть максимально похож на production: те же версии ПО, та же конфигурация, анонимизированная копия базы данных. Деплой в staging — автоматический при push в ветку develop.
Три метрики: uptime (сайт отвечает), CPU/RAM/диск (ресурсы не исчерпаны), ошибки приложения (5xx). Для старта достаточно бесплатного UptimeRobot (проверка каждые 5 минут) + node_exporter с простым bash-скриптом, отправляющим алерты в Telegram. Полноценный Prometheus + Grafana — следующий этап при росте до 5+ серверов.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#антипаттерны инфраструктуры#технический долг стартап#devops best practices#секреты в git#ручной деплой проблемы#нет бэкапов последствия#staging окружение#мониторинг серверов
Комментарии 0

Оставить комментарий

загрузка...