Когда образ контейнера готов, где он на самом деле запускается?

Вы собрали образ. Вы проверили его на уязвимости. Вы отправили его в 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 — это не тест на техническую чистоту. Это решение, основанное на операционных требованиях.

Следующая блок-схема поможет вам решить, какой путь подходит для вашей ситуации:

flowchart TD A[Старт] --> B{Высокий трафик или нулевой даунтайм?} B -- Да --> C{Несколько сервисов для управления?} B -- Нет --> D{Маленькая команда, простое приложение?} C -- Да --> E[Используйте Kubernetes] C -- Нет --> F[Рассмотрите K3s или MicroK8s] D -- Да --> G[Используйте один сервер с docker-compose] D -- Нет --> H{Операционная зрелость для кластера?} H -- Да --> E H -- Нет --> G

Используйте один сервер с docker-compose, когда:

  • Приложение используется небольшой внутренней командой.
  • Даунтайм для обновлений или обслуживания приемлем.
  • У вас один или два сервиса для управления.
  • Вам не нужно масштабироваться горизонтально.
  • Ваша команда небольшая, и вы хотите минимальной инфраструктурной сложности.

Используйте Kubernetes, когда:

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

Существует золотая середина. Некоторые команды запускают небольшой кластер Kubernetes с одним узлом, используя такие инструменты, как K3s или MicroK8s. Это дает вам возможности rolling update и health check от Kubernetes без полной сложности многоузлового кластера. Это стоит рассмотреть, если вы хотите использовать паттерны развертывания, но пока не нуждаетесь в масштабе.

Единственное правило, которое никогда не меняется

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

Никогда не пересобирайте образ на сервере. Никогда не тяните другой тег, потому что "он должен быть таким же". Никогда не позволяйте никому подключаться по SSH к серверу и запускать контейнер с локально измененным образом. Если образ в registry не является образом, который работает, вы теряете возможность воспроизвести, аудировать и откатить изменения.

Вот почему тегирование и продвижение образов имеют значение. Когда вы продвигаете образ из staging в production, вы ничего не пересобираете. Вы просто меняете, какое окружение может использовать этот конкретный тег. Байты идентичны.

Практический чек-лист для развертывания контейнера

Перед развертыванием контейнера в любом окружении убедитесь в следующем:

  • Тег образа в развертывании соответствует тегу, прошедшему пайплайн.
  • У контейнера есть эндпоинт health check, который сообщает оркестратору, когда он готов.
  • Переменные окружения и секреты установлены правильно для целевого окружения.
  • Определена стратегия обновления: rolling update для нулевого даунтайма, recreate для простых случаев.
  • У вас есть способ увидеть, какая версия образа сейчас работает.
  • У вас есть план отката: либо предыдущий тег образа, либо предыдущий манифест развертывания.

Что дальше

Запуск контейнера — это только половина работы. После запуска вам нужно знать, какая версия на самом деле обслуживает трафик, здорова ли она и что делать, если у новой версии возникла проблема. Здесь на помощь приходят отслеживание версий образов и откат. Это темы для следующей части обсуждения.

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