Когда образ контейнера готов, где он на самом деле запускается?
Вы собрали образ. Вы проверили его на уязвимости. Вы отправили его в registry. Теперь наступает момент, который отделяет рабочий пайплайн от реального развертывания: запуск этого контейнера там, где пользователи могут до него добраться.
Способ запуска контейнера полностью зависит от того, куда он попадает. Один сервер и кластер Kubernetes на бумаге выглядят похоже — оба запускают контейнеры, — но операционный опыт совершенно разный. Выбор влияет на то, как вы обновляетесь, как восстанавливаетесь после сбоев и сколько ручной координации требуется вашей команде при каждом выходе новой версии.
Запуск контейнеров на одном сервере
Развертывание на одном сервере выглядит просто. Вы подключаетесь по SSH к машине, запускаете docker run с тегом образа, который только что продвинули, и приложение стартует. В демо-среде на этом история заканчивается.
На практике один сервер редко запускает только один контейнер. Обычно у вас есть контейнер приложения, контейнер базы данных, кеш, возможно, очередь задач. Эти контейнеры должны запускаться в правильном порядке, общаться друг с другом по правильной сети и обрабатывать ситуацию, когда один из них падает. Здесь на помощь приходит docker-compose. Вы определяете все сервисы, их зависимости, порты и политики перезапуска в одном файле. Одна команда поднимает всё в правильной последовательности.
Настоящая проблема проявляется, когда нужно обновить версию приложения. На одном сервере вы останавливаете старый контейнер и запускаете новый. В это окно приложение не может обрабатывать запросы. Если приложение используется реальными людьми, этот даунтайм имеет значение.
Самый простой способ уменьшить время простоя — запустить два контейнера рядом. Держите старую версию работающей, пока запускается новая. Как только новый контейнер будет готов принимать соединения, переключите трафик на него, затем остановите старый контейнер. Это rolling update в его базовой форме. Вы можете сделать это вручную с помощью скрипта или использовать обратный прокси, например Nginx или Traefik, для переключения трафика.
Но даже с паттерном rolling update у одного сервера есть жесткое ограничение. Если сам сервер выходит из строя, приложение падает. Если нужно применить патч безопасности к операционной системе хоста, приходится планировать даунтайм. Для внутренних инструментов, используемых небольшой командой, такой компромисс часто приемлем. Для внешних приложений — обычно нет.
Запуск контейнеров на Kubernetes
Kubernetes рассматривает проблемы развертывания на одном сервере как решенные и строит поверх них. Вы не управляете контейнерами напрямую. Вы определяете объект Deployment, который описывает желаемое состояние: какой образ запускать, сколько реплик, какие health checks использовать и как выполнять обновления.
Когда вы обновляете тег образа в Deployment, Kubernetes не останавливает всё и не перезапускает. Он создает новые pod'ы с новым образом, ждет, пока они пройдут health checks, затем постепенно завершает старые pod'ы. В течение всего процесса как минимум один pod обслуживает трафик. Пользователи не видят прерывания сервиса.
Pod — это наименьшая единица в Kubernetes. Он может запускать один или несколько контейнеров, но ключевая идея в том, что pod эфемерен. Kubernetes создает pod'ы, уничтожает их и перемещает на разные узлы по мере необходимости. Вы никогда не думаете о том, на каком конкретном сервере работает pod. Кластер управляет этим.
Разница между одним сервером и Kubernetes не только в масштабировании под больший трафик. Дело в том, кто владеет координацией. На одном сервере вы решаете порядок запуска, политику перезапуска и обработку сбоев. Вы пишете скрипты или используете docker-compose для обеспечения этих решений. На Kubernetes оркестратор владеет этой координацией. Он периодически проверяет здоровье pod'ов, перезапускает упавшие pod'ы и перераспределяет pod'ы на здоровые узлы, когда узел выходит из строя.
Этот сдвиг в ответственности меняет то, как работает ваша команда. Вы перестаете писать скрипты, управляющие жизненным циклом контейнеров. Вы начинаете писать манифесты Deployment, описывающие желаемое состояние, и позволяете кластеру выяснять, как достичь этого состояния.
Вот как выглядит минимальный манифест Deployment на практике:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-registry/my-app:v1.2.3
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
Этот манифест указывает Kubernetes запустить три реплики, обновлять их по одной и направлять трафик только к pod'у после успешного ответа его эндпоинта /health.
Как выбрать между двумя вариантами
Выбор между одним сервером и Kubernetes — это не тест на техническую чистоту. Это решение, основанное на операционных требованиях.
Следующая блок-схема поможет вам решить, какой путь подходит для вашей ситуации:
Используйте один сервер с docker-compose, когда:
- Приложение используется небольшой внутренней командой.
- Даунтайм для обновлений или обслуживания приемлем.
- У вас один или два сервиса для управления.
- Вам не нужно масштабироваться горизонтально.
- Ваша команда небольшая, и вы хотите минимальной инфраструктурной сложности.
Используйте Kubernetes, когда:
- Приложение должно быть доступно даже во время обновлений.
- Вам нужно масштабировать сервисы независимо в зависимости от трафика.
- Вы запускаете несколько сервисов, которые нужно развертывать и обновлять отдельно.
- Вы хотите автоматическое восстановление после сбоев узлов.
- Ваша команда обладает операционной зрелостью для управления кластером.
Существует золотая середина. Некоторые команды запускают небольшой кластер Kubernetes с одним узлом, используя такие инструменты, как K3s или MicroK8s. Это дает вам возможности rolling update и health check от Kubernetes без полной сложности многоузлового кластера. Это стоит рассмотреть, если вы хотите использовать паттерны развертывания, но пока не нуждаетесь в масштабе.
Единственное правило, которое никогда не меняется
Независимо от того, куда вы развертываете, одно правило остается неизменным: образ, работающий в продакшене, должен быть точно таким же образом, который прошел все тесты и сканирования в пайплайне.
Никогда не пересобирайте образ на сервере. Никогда не тяните другой тег, потому что "он должен быть таким же". Никогда не позволяйте никому подключаться по SSH к серверу и запускать контейнер с локально измененным образом. Если образ в registry не является образом, который работает, вы теряете возможность воспроизвести, аудировать и откатить изменения.
Вот почему тегирование и продвижение образов имеют значение. Когда вы продвигаете образ из staging в production, вы ничего не пересобираете. Вы просто меняете, какое окружение может использовать этот конкретный тег. Байты идентичны.
Практический чек-лист для развертывания контейнера
Перед развертыванием контейнера в любом окружении убедитесь в следующем:
- Тег образа в развертывании соответствует тегу, прошедшему пайплайн.
- У контейнера есть эндпоинт health check, который сообщает оркестратору, когда он готов.
- Переменные окружения и секреты установлены правильно для целевого окружения.
- Определена стратегия обновления: rolling update для нулевого даунтайма, recreate для простых случаев.
- У вас есть способ увидеть, какая версия образа сейчас работает.
- У вас есть план отката: либо предыдущий тег образа, либо предыдущий манифест развертывания.
Что дальше
Запуск контейнера — это только половина работы. После запуска вам нужно знать, какая версия на самом деле обслуживает трафик, здорова ли она и что делать, если у новой версии возникла проблема. Здесь на помощь приходят отслеживание версий образов и откат. Это темы для следующей части обсуждения.
А пока важно выбрать цель развертывания, которая соответствует вашей операционной реальности, и убедиться, что запускаемый образ — это тот образ, который вы тестировали. Всё остальное следует из этого.