Представьте: 2025 год, и где-то в канале Slack младший разработчик предложил поместить их монолитное устаревшее приложение в контейнеры, работающее на одном Python-скрипте, который обрабатывает ежемесячные отчёты по заработной плате. Старший архитектор одобрительно кивает, не читая предложения. Все используют контейнеры, значит, контейнеры хороши, правда? Ну, садитесь, потому что нам нужно поговорить о том, как контейнеризация стала архитектурным эквивалентом предложения всем научиться языку Rust.

Контейнеризация столкнулась с реальностью

Контейнеры поистине революционны. Docker ворвался на сцену, как разработчик с переизбытком кофеина, обещая решить все наши проблемы с развёртыванием раз и навсегда. Фраза «работает на моей машине» внезапно стала мемом с ограниченным сроком годности. Мы получили переносимость, консистентность и масштабируемость в удобной маленькой упаковке. Индустрия коллективно праздновала, на конференциях проводились доклады, которые длились 45 минут, просто говоря слово «микросервисы», и внезапно все стали DevOps-инженерами.

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

Когда контейнеры на самом деле ужасны

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

Проблема монолитных приложений

Рассмотрим традиционное корпоративное приложение: единый развертываемый блок, обрабатывающий аутентификацию, бизнес-логику, обработку данных и отчётность. Эти приложения были разработаны как монолиты по веским причинам. Они разделяют состояние, у них глубокая связанность, они часто ориентированы на базу данных. Они не плохие приложения — они просто архитектурно несовместимы с философией микросервисов, которую поддерживают контейнеры.

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

Вот как выглядит контейнеризация устаревшего монолита:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "monolithic_app.py"]
# Соберите его
docker build -t legacy-app:1.0 .
# Запустите его
docker run -p 8000:8000 legacy-app:1.0
# И что теперь? Вам всё ещё нужно управлять им так же, как и раньше,
# но теперь вы также управляете инфраструктурой контейнеров.

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

Приложения, которым не нужно масштабирование

Допустим, у вас есть задание пакетной обработки, которое выполняется раз в месяц, обрабатывает данные и сохраняет результаты. Оно не параллельное. Оно не масштабируется. Ему не нужно быстрое развёртывание. Помещать его в контейнер не приносит пользы — просто ваша операционная команда должна понимать оркестрацию контейнеров, чтобы запустить Python-скрипт, который уже работал нормально как задание cron.

Аргумент о накладных расходах часто отвергается, но он реален. У контейнеров есть реальные накладные расходы по времени запуска, потреблению памяти и использованию ЦП по сравнению с запуском приложений непосредственно на хост-системе. Для большинства современных приложений эти накладные расходы незначительны. Для некоторых случаев использования — особенно для систем высокочастотной торговли, обработки в реальном времени или сред с ограниченными ресурсами — эти накладные расходы имеют значение.

Налог на сложность, который вы фактически заплатите

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

Кошмар оркестрации

С контейнерами вы быстро приходите к Kubernetes. Или Nomad. Или Swarm. Выбирайте свой яд. Эти инструменты мощные, но имеют кривую обучения, которая делает восхождение на Эверест похожим на пеший поход.

apiVersion: v1
kind: Pod
metadata:
  name: simple-app
spec:
  containers:
  - name: app
    image: my-app:1.0
    resources:
      requests:
        memory: "256Mi"
        cpu: "250m"
      limits:
        memory: "512Mi"
        cpu: "500m"

Этот YAML предназначен для одного пода. В реальности вы также пишете Deployments, Services, ConfigMaps, Secrets, правила Ingress, NetworkPolicies и достаточно YAML, чтобы ваши глаза кровоточили. Ваш DevOps-специалист становится YAML-инженером, тратя часы на отладку, почему ваше приложение не планирует из-за правил близости, которые вы не до конца понимаете.

Разработчик, который провёл пять лет, осваивая традиционное администрирование серверов, не может сразу перенести эти навыки на Kubernetes. Они изучают новую экосистему со своими конвенциями, лучшими практиками, подводными камнями и режимами сбоев.

Сложность конфигурации

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

Театрализованная безопасность, которую мы все наблюдаем

Контейнеры обещают изоляцию. На самом деле они предоставляют… negotiable.

Уязвимость общего ядра

Контейнеры работают поверх хост-ОС, деля ядро. Если кто-то выйдет из одного контейнера, у него потенциально будет доступ ко всем контейнерам на этом хосте и, возможно, к самой хост-системе. Это принципиально отличается от виртуализации на основе гипервизора, где у каждой виртуальной машины своё ядро и аппаратные ресурсы.

┌─────────────────────────────────────────┐
│           Операционная система хоста     │
│                (Ядро)                   │
├──────────┬──────────────┬───────────────┤
│Контейнер │  Контейнер   │   Контейнер   │
│    1     │      2       │       3       │
│(Общее    │  (Общее      │  (Общее      │
│ Ядро)    │  Ядро)       │  Ядро)       │
└──────────┴──────────────┴───────────────┘
Граница безопасности: На уровне ядра
(Слабее, чем изоляция на основе VM)

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

Уязвимости цепочки поставок образов

Когда вы извлекаете образ контейнера из Docker Hub или любого реестра, вы доверяете, что образ не взломан. Но что, если это не так? Что, если базовый образ, который вы используете в качестве основы для вашего приложения, содержит уязвимость нулевого дня, о которой вы не знаете? Вы втягиваете неизвестный код в свою цепочку поставок и надеетесь, что он безопасен.

# Вы запускаете эту безобидную команду
docker pull python:3.11
# Но что на самом деле в этом образе?
# - Полная операционная система
# - Сотни зависимостей
# - Всё, что решил включить автор образа
# - Возможно, устаревшее программное обеспечение с известными уязвимостями
# Вам нужно сканирование уязвимостей для каждого образа
trivy image python:3.11

Операционные накладные расходы

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

Эфемерность делает отладку адом

Контейнеры должны быть эфемерными. Ваше приложение сбоит? Kubernetes запускает новый. Отлично! Только теперь вы не можете подключиться к контейнеру по SSH для отладки. Журналы могут не быть захвачены. Состояние ушло. Вы летите вслепую, имея только журналы, которые вы явно настроили для захвата.

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

Мониторинг становится сложным

Теперь вам нужно отслеживать не только ваше приложение, но и контейнеры, состояние кластера, использование ресурсов, сеть, тома хранения и состояние платформы оркестрации.

# Что вы отслеживаете?
# - Производительность отдельных контейнеров
# - Состояние узлов
# - Кластерная сеть
# - Статус томов хранения
# - События планирования пода
# - Распределение ресурсов
# - Нарушения контекста безопасности
# - Уязвимости образа
#