Почему ваши теги контейнеров врут вам

Вы выполняете docker pull myapp:latest и думаете, что точно знаете, что получаете. Но это не так. Завтра этот тег может указывать на другой образ, или уже сейчас он указывает на другой образ, чем час назад. Тот же тег, та же команда, но совершенно другое программное обеспечение работает в продакшене.

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

Где живут образы

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

Когда вы собираете образ Docker на своем ноутбуке, этот образ существует только на вашей машине. Чтобы запустить его на сервере, в стейджинге или в продакшене, нужно место для хранения и обмена этим образом. Это место называется реестром.

Представьте реестр как файловый сервер для образов контейнеров, но с дополнительными возможностями. Он управляет версиями, проверяет целостность и контролирует доступ. Когда вы выполняете docker pull nginx:latest, вы тянете из Docker Hub, который является публичным реестром. Любой может тянуть из него, но и любой может пушить в него, поэтому не стоит полагаться на публичные образы для продакшена.

Большинство компаний запускают собственный внутренний реестр. Распространенные варианты: Harbor, Nexus, GitLab Container Registry или Amazon ECR. Внутренний реестр дает три вещи:

  • Происхождение: вы знаете, откуда взялся каждый образ. Никаких случайных образов из интернета.
  • Скорость: передача образов по внутренней сети намного быстрее, чем загрузка из публичного интернета.
  • Контроль: вы решаете, кто может пушить образы, а кто — тянуть.

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

Теги — это метки, а не идентификаторы

Каждый образ в реестре имеет один или несколько тегов. Тег — это метка, которую вы прикрепляете к образу, чтобы отметить конкретную версию или вариант. Вы видите теги повсюду: myapp:1.0.0, myapp:staging, myapp:latest.

Теги удобны. Они дают человекопонятный способ ссылаться на образы. Вместо запоминания длинного хеша вы вводите myapp:1.0.0 и получаете нужный образ.

Но у тегов есть фундаментальный недостаток: они изменчивы. Вы можете в любой момент изменить, на какой образ указывает тег. Сегодня myapp:latest указывает на хеш образа abc123. Завтра, после новой сборки, он указывает на def456. Тег остается тем же, но образ меняется.

Эта изменчивость создает реальные проблемы. Рассмотрим сценарий, где ваше стейджинговое окружение работает с myapp:staging. Ваш пайплайн собирает новый образ, помечает его тегом staging и пушит. Теперь стейджинг работает с новым кодом. Но что, если кто-то вручную перезапишет тег staging другим образом? Или если ваш пайплайн случайно пометит тегом неправильную сборку? Вы не сможете узнать, какой образ на самом деле работает в стейджинге, если не проверите диджест.

Паттерн неизменяемых тегов

Решение — использовать неизменяемые теги — теги, которые никогда не меняются после создания. Как только вы назначили тег образу, этот тег остается с этим образом навсегда.

Неизменяемые теги содержат уникальную информацию, идентифицирующую конкретную сборку. Распространенные паттерны:

  • Семантическая версия: myapp:1.2.3
  • Хеш коммита Git: myapp:a1b2c3d
  • Временная метка сборки: myapp:20240515-1430
  • ID запуска пайплайна: myapp:build-456

С неизменяемыми тегами вы можете точно отследить, какой код работает в любом окружении. Если кто-то сообщает об ошибке в продакшене, вы смотрите на тег, находите хеш коммита и точно знаете, какой код создал этот образ. Никаких догадок, никаких "какая это версия latest?".

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

Диджесты: истина

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

У каждого образа контейнера есть диджест. Диджест — это криптографический хеш содержимого образа. Выглядит он примерно так: sha256:abc123def456.... Диджест уникален для этого конкретного образа. Если содержимое образа изменится хотя бы на один байт, диджест изменится полностью.

В отличие от тегов, диджесты нельзя переместить или переназначить. Диджест sha256:abc123 всегда будет ссылаться на тот же самый образ, навсегда. Вы не сможете указать этот диджест на другой образ, что бы вы ни делали.

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

# Это безопасно — вы получаете именно то, что запросили
docker pull myapp@sha256:abc123def456

Использование тегов и диджестов вместе в пайплайнах

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

Вот как типичный пайплайн это обрабатывает:

  1. Собрать образ.
  2. Пометить его неизменяемым тегом (хеш коммита или версия).
  3. Запушить образ в реестр.
  4. Записать диджест в метаданные развертывания.
  5. При продвижении из стейджинга в продакшен проверить, что диджест совпадает точно.

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

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

# Собрать и запушить образ с неизменяемым тегом
docker build -t myapp:build-456 .
docker push myapp:build-456

# Получить диджест из запушенного образа
digest=$(docker inspect --format='{{index .RepoDigests 0}}' myapp:build-456 | cut -d'@' -f2)
echo "Digest: $digest"

# Использовать диджест в манифесте развертывания Kubernetes
cat <<EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp@$digest
EOF

# Применить развертывание
kubectl apply -f deployment.yaml

Это гарантирует, что каждый под запускает точно тот же образ, независимо от того, что происходит с тегами в реестре.

Многие инструменты развертывания поддерживают развертывание на основе диджестов. Kubernetes, например, позволяет указать образ по диджесту вместо тега:

spec:
  containers:
  - name: myapp
    image: myregistry.com/myapp@sha256:abc123def456

Это гарантирует, что каждый под запускает точно тот же образ, независимо от того, что происходит с тегами в реестре.

Практический чек-лист

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

  • Каждая сборка создает уникальный, неизменяемый тег (хеш коммита, версия или временная метка)
  • Тег latest никогда не используется в продакшен-развертываниях
  • Ваш пайплайн записывает диджест каждого собираемого образа
  • Продвижение образа между окружениями проверяет диджест, а не только тег
  • В вашем реестре настроены права доступа для предотвращения несанкционированных пушей
  • Старые образы регулярно очищаются во избежание разрастания хранилища

Вывод

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