Docker и Kubernetes: сборка образов, оркестрация и когда кластер не нужен

Практика Docker и Kubernetes: многоэтапный Dockerfile, Docker Compose, объекты Kubernetes, проверки здоровья и ресурсы, Helm и GitOps, безопасность, наблюдаемость, управляемый Kubernetes и случаи, когда он не нужен.

Контейнеры давно перестали быть экзотикой: «у меня работает» заменилось на «работает в контейнере — работает везде». Docker упаковывает приложение вместе со всеми зависимостями в образ, который одинаково запускается на ноутбуке разработчика, тестовом стенде и в продакшене. Kubernetes управляет множеством таких контейнеров: запускает их на серверах, перезапускает упавшие, распределяет нагрузку, обновляет без простоя и масштабирует. При этом Kubernetes — один из самых переоценённых инструментов для небольших проектов и один из самых недооценённых по сложности эксплуатации. Эта статья — практическое руководство по обоим: как правильно собирать образы, как организовать локальную разработку, из чего состоит приложение в Kubernetes, как обеспечить безопасность и наблюдаемость и, главное, когда Kubernetes действительно нужен, а когда достаточно более простых решений. О процессах поставки изменений — CI/CD, метриках скорости и порядке DevOps-трансформации — мы писали отдельно в статье о time-to-market . Контейнер и образ Образ — неизменяемый шаблон: файловая система с приложением, зависимостями и настройками запуска. Собирается один раз и хранится в реестре образов. Контейнер — запущенный экземпляр образа, изолированный процесс со своей файловой системой, сетью и ограничениями ресурсов. В отличие от виртуальной машины, контейнер не содержит собственную операционную систему и использует ядро хоста. Поэтому он запускается за секунды и расходует меньше ресурсов, но изоляция у него слабее, чем у виртуальной машины, — это важно для безопасности. Dockerfile: как собирать образы правильно Многоэтапная сборка Инструменты сборки, исходники и зависимости для разработки не должны попадать в итоговый образ. Многоэтапная сборка разделяет этап сборки и этап выполнения. # Этап 1: зависимости и сборка FROM node:22-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build npm prune --omit=dev # Этап 2: минимальный образ для запуска FROM node:22-alpine WORKDIR /app ENV NODE_ENV=production COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist COPY package.json ./ USER node EXPOSE 3000 HEALTHCHECK CMD wget -qO- http://localhost:3000/health || exit 1 CMD ["node", "dist/server.js"] Порядок слоёв и кэш Каждая инструкция создаёт слой, и Docker переиспользует слои, если входные данные не изменились. Сначала копируют файлы зависимостей и устанавливают их, и только потом — исходный код. Тогда изменение одной строки кода не приводит к повторной установке всех зависимостей. Правила хорошего образа Конкретные версии базовых образов , а не latest : сборка должна быть воспроизводимой. Минимальный базовый образ — slim-варианты, Alpine или distroless. Меньше пакетов — меньше уязвимостей и быстрее загрузка. Учитывайте, что Alpine использует другую стандартную библиотеку C, и некоторые нативные зависимости ведут себя в нём иначе. Запуск не от root — инструкция USER . Если процесс в контейнере скомпрометирован, у злоумышленника меньше возможностей. Файл .dockerignore исключает из контекста сборки node_modules , .git , локальные файлы окружения и артефакты. Никаких секретов в образе. Пароли, ключи и токены передаются при запуске через переменные окружения или хранилище секретов. Секрет, скопированный в образ, остаётся в его слоях, даже если на следующем шаге файл удалён. Один процесс на контейнер и корректная обработка сигнала остановки, чтобы приложение успевало завершить текущие запросы. Docker Compose для локальной разработки Compose описывает набор сервисов, которые нужны приложению: базу данных, кэш, очередь, почтовый сервер для тестов. Новый разработчик запускает окружение одной командой. services: app: build: . ports: ["3000:3000"] environment: DATABASE_URL: postgres://app:app@db:5432/app REDIS_URL: redis://cache:6379 depends_on: db: condition: service_healthy db: image: postgres:16 environment: POSTGRES_USER: app POSTGRES_PASSWORD: app POSTGRES_DB: app volumes: [dbdata:/var/lib/postgresql/data] healthcheck: test: ["CMD-SHELL", "pg_isready -U app"] interval: 5s cache: image: redis:7 volumes: dbdata: Для небольших продуктов Compose на одном сервере — вполне рабочий вариант продакшена, если настроены перезапуск контейнеров, резервное копирование, мониторинг и процедура обновления. Kubernetes: основные объекты Pod — минимальная единица запуска: один или несколько контейнеров с общей сетью и хранилищем. Deployment — описывает, сколько копий Pod должно работать и как их обновлять. Kubernetes поддерживает заданное число копий и перезапускает упавшие. Service — стабильный сетевой адрес для набора Pod и распределение трафика между ними. Ingress или Gateway — маршрутизация внешних HTTP-запросов к сервисам, TLS-сертификаты. ConfigMap и Secret — конфигурация и секреты, которые передаются в контейнеры. Namespace — логическое разделение ресурсов кластера между командами или окружениями. HorizontalPodAutoscaler — автоматическое изменение числа копий по нагрузке. StatefulSet и PersistentVolume — для приложений с постоянными данными. Deployment с проверками здоровья и ресурсами apiVersion: apps/v1 kind: Deployment metadata: name: api spec: replicas: 3 selector: matchLabels: { app: api } template: metadata: labels: { app: api } spec: containers: - name: api image: registry.example.ru/api:1.14.2 ports: [{ containerPort: 3000 }] envFrom: - secretRef: { name: api-secrets } resources: requests: { cpu: "250m", memory: "256Mi" } limits: { memory: "512Mi" } readinessProbe: httpGet: { path: /ready, port: 3000 } livenessProbe: httpGet: { path: /health, port: 3000 } securityContext: runAsNonRoot: true readOnlyRootFilesystem: true allowPrivilegeEscalation: false readinessProbe определяет, готов ли Pod принимать трафик. Пока приложение прогревается или теряет соединение с базой, трафик на него не идёт. livenessProbe определяет, жив ли процесс. Не делайте её зависимой от внешних систем: если база данных недоступна, перезапуск всех Pod не поможет, а только усугубит ситуацию. requests — ресурсы, которые планировщик резервирует для Pod; limits — верхняя граница. Без requests планировщик не может разумно распределять нагрузку, без лимита памяти один процесс может вытеснить соседей. Обновления без простоя Deployment по умолчанию обновляет Pod постепенно: запускает новые копии, дожидается их готовности и только потом останавливает старые. Чтобы это действительно работало без ошибок, приложение должно корректно завершать текущие запросы при получении сигнала остановки, а изменения схемы базы данных — быть совместимыми со старой и новой версией одновременно. Helm и GitOps Когда приложений и окружений несколько, манифесты повторяются. Helm упаковывает манифесты в параметризованные чарты: одна структура, разные значения для тестового и продуктивного окружения. Kustomize решает похожую задачу через наложение изменений на базовые манифесты. Подход GitOps хранит желаемое состояние кластера в репозитории, а специальный контроллер — например, Argo CD или Flux — приводит кластер в соответствие с ним. Любое изменение проходит через репозиторий, ревью и историю, а ручные изменения в кластере автоматически откатываются. Безопасность Сканирование образов на известные уязвимости в конвейере сборки — сборка с критическими уязвимостями не выкладывается. Непривилегированные контейнеры : запуск не от root, файловая система только для чтения, запрет повышения привилегий. Сетевые политики ограничивают, какие Pod могут обращаться друг к другу. По умолчанию в кластере разрешено всё. RBAC — минимально необходимые права для людей и сервисных учётных записей. Секреты : объекты Secret по умолчанию лишь закодированы. Включайте шифрование в хранилище кластера или используйте внешние хранилища секретов. Доверенные реестры и подписанные образы: в кластер попадают только образы из разрешённых источников. Обновление кластера : версии Kubernetes поддерживаются ограниченное время, и отставание от поддерживаемых версий — риск безопасности. Защиту самих приложений и API мы разбирали в статье об API Security . Наблюдаемость В кластере с десятками Pod, которые появляются и исчезают, нельзя зайти на сервер и посмотреть журнал. Нужны три вещи: метрики — обычно Prometheus и Grafana: загрузка ресурсов, число запросов, ошибки, задержки; централизованные журналы — приложение пишет в стандартный вывод в структурированном формате, а сборщик отправляет журналы в хранилище; трассировка запросов через сервисы — стандарт OpenTelemetry. Подробно — в статье о мониторинге и observability . Базовые команды диагностики kubectl get pods -n production kubectl describe pod api-7d9f8b6c5-x2k4p -n production # события, причины перезапусков kubectl logs api-7d9f8b6c5-x2k4p -n production --previous # журнал упавшего контейнера kubectl top pods -n production # потребление ресурсов kubectl rollout status deployment/api -n production kubectl rollout undo deployment/api -n production # откат к предыдущей версии Управляемый Kubernetes Самостоятельная установка и обслуживание управляющего слоя Kubernetes — сложная инженерная задача. Для большинства компаний разумнее управляемый сервис облачного провайдера, который берёт на себя управляющие компоненты, обновления и отказоустойчивость. Российские облачные провайдеры предлагают управляемый Kubernetes, что важно для проектов с требованиями к размещению данных в России. Переезд инфраструктуры с зарубежных облаков мы разбирали в статье об импортозамещении digital-стека . Даже в управляемом кластере на команде остаются приложения, их манифесты, безопасность, мониторинг, обновления версий и стоимость ресурсов. Когда Kubernetes не нужен одно-два приложения с предсказуемой нагрузкой — достаточно Docker Compose на сервере или платформы запуска контейнеров у облачного провайдера; в команде нет человека, готового сопровождать кластер, — управляемый сервис снижает, но не убирает эту потребность; продукт на стадии проверки гипотезы — время лучше вложить в продукт; основная нагрузка — статический сайт или несколько функций, которые удобнее запускать на бессерверной платформе. Когда Kubernetes оправдан много сервисов и команд, которым нужна единая платформа выкладки; нагрузка меняется и требует автоматического масштабирования; важны обновления без простоя, самовосстановление и единообразие окружений; есть компетенции для эксплуатации или бюджет на управляемый сервис и поддержку. Экономический эффект от контейнеров и оркестрации — плотность размещения, автоматическое масштабирование, меньше ручных операций — сильно зависит от исходной инфраструктуры и нагрузки, поэтому оценивать его стоит на собственных данных, а не по усреднённым цифрам из презентаций. Архитектурные решения вокруг контейнеров — отказоустойчивость, распределённые данные, стоимость облака — разобраны в статье о cloud-native архитектуре . Контейнеризацию, настройку кластеров и конвейеров поставки мы выполняем в рамках автоматизации и разработки веб-проектов . Частые вопросы Docker и Kubernetes — конкуренты? Нет. Docker собирает образы и запускает контейнеры, Kubernetes управляет множеством контейнеров на кластере серверов. Образы, собранные Docker, запускаются в Kubernetes. Сам Kubernetes использует совместимые с Docker среды выполнения контейнеров. Можно ли держать базу данных в Kubernetes? Можно, с помощью StatefulSet и операторов баз данных, но это требует опыта: хранилище, резервное копирование, восстановление и обновления усложняются. Для многих команд управляемая база данных облачного провайдера надёжнее и дешевле в эксплуатации. Сколько узлов нужно для продакшена? Минимум, при котором отказ одного узла не приводит к недоступности сервиса: копии приложения должны быть распределены по нескольким узлам, а лучше — по зонам доступности. Точное число зависит от нагрузки и требований к доступности. Как уменьшить размер образа? Многоэтапная сборка, минимальный базовый образ, исключение зависимостей для разработки и лишних файлов через .dockerignore, объединение шагов установки пакетов с очисткой кэша менеджера пакетов. Почему Pod постоя