Distroless-образы убирают из контейнера shell, пакетный менеджер и привычные утилиты. Это делает боевой образ чище, меньше и спокойнее с точки зрения безопасности, но ломает старый рефлекс: «зайти внутрь контейнера и посмотреть руками». В Kubernetes для этого есть временные контейнеры — ephemeral containers. Только важно понимать: они не возвращают bash в приложение. Они выносят дебаг в платформу.
Начнём с базы, чтобы дальше не путаться. Distroless — это не «контейнер без Linux» и не магический режим безопасности. Это минимальный образ, где оставляют приложение и его зависимости времени выполнения, но убирают привычное пользовательское окружение: shell, пакетный менеджер и набор программ, которые обычно есть в Debian, Ubuntu или Alpine. В официальном проекте GoogleContainerTools/distroless это описано именно так: внутри остаётся приложение и runtime-зависимости, но нет package manager, shell и прочих программ из стандартного Linux-дистрибутива.
Зачем вообще на это идти? Не ради моды. Distroless уменьшает размер образа, сокращает поверхность атаки и снижает шум от уязвимостей в пакетах, которыми приложение никогда осознанно не пользовалось. В боевом контейнере остаётся меньше случайных бинарей, меньше старого системного мусора, меньше соблазна чинить production руками. Образ становится скучнее. И для эксплуатации это не всегда приятно, но для безопасности и воспроизводимости — обычно хорошо.
Цена тоже есть. В distroless-образе обычно нет /bin/sh, bash, curl, ps, ss, dig, tcpdump, пакетного менеджера и прочих привычных вещей. Иногда нет даже того, что неожиданно оказывается важным: CA bundle, timezone data, записей пользователей, локалей. Часть этого надо явно добавлять на этапе сборки, часть — осознанно не добавлять вообще. И вот здесь многие команды впервые понимают, что раньше они дебажили не приложение, а свой комфорт внутри контейнера.
История почти всегда начинается красиво. Образ стал меньше. Сканер утих. SBOM стал чище. Security-команда довольна, платформа довольна, разработчики не спорят, потому что в тестовом окружении всё прошло нормально.
А потом в production один Pod начинает странно отдавать 502.
Не весь Deployment. Не весь namespace. Один Pod.
Логи говорят мало. Метрики показывают, что процесс жив. Readiness иногда краснеет, иногда зеленеет. Соседние реплики работают. Сервис вроде бы слушает порт, но sidecar жалуется на connection refused. Кто-то открывает терминал и делает то, что делал последние пять лет:
kubectl exec -n <namespace> -it <pod> -c <container> -- sh
В ответ прилетает:
exec: "sh": executable file not found in $PATH
И вот здесь рождается плохой вывод: «distroless неудобен для продакшена».
Нет. Неудобен не distroless. Неудобна эксплуатационная модель, в которой боевой образ одновременно является и артефактом приложения, и аварийной аптечкой, и интерактивной рабочей станцией инженера. А Distroless просто отнял костыль.
Как вообще работают временные контейнеры
В Kubernetes для такой ситуации есть отдельный механизм — ephemeral containers, то есть временные контейнеры. Это специальные контейнеры, которые можно добавить в уже существующий Pod для ручной диагностики. Kubernetes прямо описывает их как инструмент для интерактивного troubleshooting, когда kubectl exec недостаточен: например, контейнер упал или образ не содержит отладочных утилит, как часто бывает с distroless. Возможность стабильна с Kubernetes v1.25.
Самая важная мысль: временный контейнер не «заходит внутрь» вашего distroless-контейнера.
Он не добавляет туда /bin/sh.
Не меняет файловую систему приложения.
Не чинит образ.
Не становится обычным sidecar.
Он запускается рядом, внутри того же Pod, и даёт инженеру набор инструментов в контексте этой Pod-сущности. А Pod в Kubernetes — это не просто один контейнер, а группа контейнеров с общими сетевыми и storage-ресурсами, которые запускаются вместе и живут в общем контексте.
Механика примерно такая. В Pod уже работает контейнер приложения, собранный на distroless. Внутри него нет shell. Вы добавляете к этому Pod отладочный контейнер с нормальными инструментами: curl, dig, ss, openssl, иногда tcpdump или strace, если это разрешено. Kubernetes добавляет его не как обычное редактирование Pod, а через отдельный подресурс pods/ephemeralcontainers; временные контейнеры нельзя задать при создании Pod и нельзя добавить через обычный kubectl edit.
Базовая команда выглядит так:
kubectl debug -n <namespace> -it pod/<pod> \ --container=debug-<incident-id> \ --image=<registry>/platform/debug-tools@sha256:<digest> \ --target=<container> \ --profile=general \ -- /bin/bash
Для локального стенда вы часто увидите что-то вроде nicolaka/netshoot:latest. Это удобно. Но для боевого окружения лучше иметь свой отладочный образ: из своего registry, с фиксированной версией или digest, с проверенным набором инструментов, сканированием и подписью. Сегодня вы просто хотите dig и curl, завтра кто-то запускает tcpdump, послезавтра в образе появляется пакетный менеджер, а ещё через месяц у вас уже не отладочный контейнер, а маленький bastion внутри каждого namespace.
Профиль тоже лучше указывать явно. kubectl debug поддерживает профили, которые меняют настройки отладочного контейнера или копии Pod; среди встроенных профилей есть general, baseline, restricted, netadmin и sysadmin. Не надо в проде надеяться на «что там по умолчанию у моей версии kubectl». Команда для инцидента должна быть скучной и повторяемой.
--target=<container> нужен, чтобы временный контейнер попытался присоединиться к пространству процессов целевого контейнера. Если среда выполнения контейнеров это поддерживает, вы увидите процессы приложения через ps. Если нет — увидите только процессы самого отладочного контейнера. Kubernetes отдельно предупреждает: если процессы целевого контейнера не видны, причина может быть именно в поддержке runtime.
И это первый практический капкан.
Инженер заходит в отладочный контейнер, запускает:
ps aux
Видит только bash и сам ps, после чего говорит: «процесс приложения умер». А он не умер. Вы просто смотрите не в то пространство процессов.
С сетью обычно проще: контейнеры внутри одного Pod живут в общем сетевом пространстве. Поэтому даже если процессы приложения не видны, сетевые проверки часто остаются полезными:
ip addrip routecat /etc/resolv.confss -lntpcurl -v http://127.0.0.1:<port>/healthcurl -v http://<pod-ip>:<port>/healthdig <service>.<namespace>.svc.cluster.localopenssl s_client -connect <host>:443 -servername <host> </dev/null
Так можно быстро понять, что именно сломалось: DNS, маршрут, локальный порт, bind address, TLS, egress, NetworkPolicy или ожидания sidecar’а.
Но есть второй капкан. Он менее очевидный и гораздо более вредный.
Файловая система, которую вы видите внутри отладочного контейнера, — это файловая система отладочного контейнера. Не приложения.
Если вы зашли в <registry>/platform/debug-tools и посмотрели /etc/ssl/certs, вы проверили сертификаты отладочного образа. Если приложение падает на TLS из-за отсутствующего CA bundle в distroless-образе, такая проверка может дать ложное спокойствие. То же самое с /etc/passwd, timezone data, локальными конфигами и файлами, которые должны были попасть в образ на этапе сборки.
Общие тома — отдельная история. Если том действительно примонтирован в отладочный контейнер, его можно смотреть. Но это не делает временный контейнер «тем же самым контейнером приложения». Корневая файловая система всё равно другая.
Если процесс приложения виден и хватает прав, можно смотреть корень файловой системы целевого процесса через /proc:
ps auxls -la /proc/<app-pid>/rootls -la /proc/<app-pid>/root/etc/ssl/certscat /proc/<app-pid>/root/etc/resolv.confls -la /proc/<app-pid>/root/app
Это не красивая лабораторная команда. Это нормальная эксплуатационная грязь. Но именно она часто отделяет реальную проверку от «я зашёл куда-то рядом и убедился, что там всё хорошо».
Представим обычный инцидент. Go-сервис переехал на distroless и начал падать при подключении к внешнему HTTPS API только в одном окружении. Дежурный зашёл через временный контейнер, выполнил openssl s_client, получил нормальную цепочку и закрыл гипотезу про сертификаты. Через час выяснилось, что openssl работал из отладочного образа, где CA bundle был полный, а приложение запускалось из другого корня файловой системы, куда нужный bundle при сборке не попал.
Проверка была технически правильной, но относилась не к тому контейнеру.
Временные контейнеры не отменяют необходимость думать, откуда именно вы смотрите.
Когда это правильный инструмент, а когда нет
Временный контейнер хорош, когда Pod жив и важно посмотреть состояние на месте. Именно на месте, а не «создать похожий Pod где-нибудь рядом».
Например, у вас плавающая DNS-проблема. Один Pod попал на узел с другим состоянием CNI. Приложение слушает только 127.0.0.1, а sidecar пытается достучаться до Pod IP. После ротации Secret часть реплик держит старое соединение, а часть уже перечитала конфиг. В таких случаях копия Pod может оказаться слишком чистой: она стартует заново, получит новый IP, попадёт на другой узел, перечитает Secret, заново выполнит init-контейнеры и перестанет быть тем объектом, который вы расследовали.
Временный контейнер ценен тем, что не стирает место происшествия.
Но это не универсальная отвёртка. Если контейнер падает сразу после старта, вы не успеете «зайти внутрь процесса». Если нужно поменять команду запуска, заменить образ или удержать контейнер живым, нужен не временный контейнер, а копия Pod через --copy-to. kubectl debug умеет не только добавлять временный контейнер, но и создавать копию Pod с изменёнными атрибутами, включая образ или команду запуска.
kubectl debug -n <namespace> pod/<pod> -it \ --copy-to=<pod>-debug \ --container=<container> \ --set-image=<container>=<registry>/<service>:debug \ -- /bin/sh
Разница простая:
|
Ситуация |
Что использовать |
|---|---|
|
Pod жив, нужно проверить сеть, DNS, порт или TLS рядом с приложением |
временный контейнер |
|
Контейнер падает на старте, нужно поменять команду или образ |
|
|
Нужно посмотреть события, причины рестартов, image pull, mounts |
|
|
Нужно понять, что было до последнего рестарта |
|
|
Подозрение на CNI, kubelet, node-local DNS, container runtime или сеть узла |
|
Node-level debug — это уже другой уровень риска. При kubectl debug node/<node> Kubernetes создаёт отладочный Pod на конкретном узле, а файловая система узла монтируется в /host; в документации этот сценарий описан отдельно от обычной отладки Pod. Это не первый рефлекс. Это инструмент для ситуаций, где проблема ниже уровня Pod.
Плохая аварийная инструкция звучит так: «если не получилось — попробуй debug node». Хорошая инструкция сначала заставляет сформулировать, какой слой мы проверяем. DNS внутри Pod? Временный контейнер. Падение на старте? Копия через --copy-to. Сетевой стек узла или CNI? Тогда уже debug узла, но с отдельными правами и пониманием цены ошибки.
У временных контейнеров есть ещё одно важное ограничение: их нельзя использовать как обычные контейнеры приложения. Они не предназначены для построения сервиса, не имеют нормального жизненного цикла приложения, не поддерживают часть полей обычного контейнера: ports, livenessProbe, readinessProbe, resources. После добавления временный контейнер нельзя изменить или удалить из Pod. Эти ограничения прямо описаны в документации Kubernetes.
Звучит неудобно, но это защита от нас самих.
Kubernetes как будто говорит: «Не надо строить приложение из отладочных контейнеров. Не надо добавлять временный sidecar и забывать о нём. Не надо чинить архитектуру через аварийный вход».
Временный контейнер — это фонарик. Не новая комната в доме.
Distroless подходит не всем, и это нормально
Здесь стоит сказать неприятную, но честную вещь: distroless не обязан быть целью каждой команды.
Если у вас маленький сервис, нет зрелого CI, нет нормального процесса сборки образов, нет доступа к кластеру по ролям, нет отладочного образа, нет аварийной инструкции, а production чинится руками через общий kubeconfig, то переход на distroless может сначала ухудшить жизнь. Не потому что distroless плохой, а потому что он делает видимыми проблемы, которые раньше прятались за bash внутри контейнера.
Иногда slim-образ с нормальным hardening, понятным Dockerfile, сканированием, non-root пользователем, read-only filesystem и аккуратной поставкой сертификатов может быть более взрослым шагом, чем «модный distroless», после которого дежурные в панике запускают ubuntu:latest рядом с приложением.
Distroless хорошо окупается там, где команда готова относиться к образу как к артефакту, а к дебагу — как к отдельному production-доступу. Если этого нет, лучше сначала навести порядок в сборке, правах, логах и диагностике. Потом уже убирать shell.
Это не откат от безопасности. Это нормальная инженерная последовательность.
Безопасность: отладка — это тоже доступ к production
Сама команда kubectl debug редко ломает систему. Систему ломает право запускать в production любой контейнер с любым контекстом безопасности.
Это особенно чувствительно именно с distroless. Команда привыкла думать: «у нас безопасный минимальный образ». А потом дежурный запускает рядом отладочный образ с bash, tcpdump, strace, пакетным менеджером и расширенными Linux capabilities. Формально образ приложения остался чистым. Фактически вы только что дали человеку мощный интерактивный инструмент внутри боевого namespace.
Это не значит, что так нельзя. Это значит, что это должно быть оформлено как production-доступ.
Минимальная роль для дежурного инженера может выглядеть так:
apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: oncall-debug namespace: <namespace>rules: - apiGroups: [""] resources: ["pods", "events"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["pods/log"] verbs: ["get"] - apiGroups: [""] resources: ["pods/exec", "pods/portforward"] verbs: ["create"] - apiGroups: ["apps"] resources: ["deployments", "replicasets"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["pods/ephemeralcontainers"] verbs: ["update"]
Последняя строчка здесь принципиальная. Доступ к временным контейнерам — это не то же самое, что доступ к логам. Это отдельное write-действие через подресурс pods/ephemeralcontainers.
Но RBAC — только нижний слой. Он отвечает на вопрос «можно ли этому субъекту вызвать этот Kubernetes API». Он не отвечает на вопросы: какой отладочный образ разрешён, можно ли использовать sysadmin, нужен ли approval на NET_ADMIN, кто и зачем открыл сессию, можно ли запускать tcpdump, куда пишется аудит.
До инцидента должны быть готовы хотя бы четыре вещи: отладочный образ в своём registry, права на pods/ephemeralcontainers не шире нужного, понятные профили kubectl debug и след в аудите. Иначе distroless просто поменяет один хаос на другой: раньше все заходили в контейнер через sh, теперь все будут запускать ubuntu:latest рядом с приложением.
Скучный хороший вариант:
kubectl debug -n <namespace> -it pod/<pod> \ --container=debug-inc-1842 \ --image=<registry>/platform/debug-tools@sha256:<digest> \ --target=<container> \ --profile=general \ -- /bin/bash
Плохой вариант:
kubectl debug -it pod/<pod> \ --image=ubuntu:latest \ --profile=sysadminapt updateapt install -y tcpdump curl dnsutils strace vim
Да, иногда очень хочется. Да, иногда инцидент горит. Но если это нормальный путь, вы не дебажите production. Вы каждый раз заново собираете аварийный доступ прямо в production.
Нормальный компромисс такой: для большинства случаев есть general или baseline отладочный образ с сетевыми и HTTP-инструментами. Для сетевой диагностики с packet capture — отдельный образ и отдельный профиль, например netadmin, с подтверждением. Для отладки узла — ещё более узкий доступ, короткая сессия и обязательный след в аудите. Не потому что безопасность любит мешать, а потому что отладочная сессия в Kubernetes — это вмешательство в живую систему.
И distroless здесь снова полезен. Он делает этот факт видимым.
Пока shell лежит в каждом боевом образе, доступ выглядит как бытовая мелочь: «я просто зайду в контейнер». Когда shell исчезает, приходится признать честно: «я хочу получить интерактивный инструмент внутри production-контекста».
Аварийная инструкция без героизма
Хорошая инструкция для distroless не должна быть огромной. Она должна не давать инженеру в три часа ночи перепутать слой, в котором он находится.
Сначала внешняя картина:
kubectl get pod -n <namespace> <pod> -o widekubectl describe pod -n <namespace> <pod>kubectl logs -n <namespace> <pod> -c <container>kubectl logs -n <namespace> <pod> -c <container> --previous
Если Pod жив и нужно посмотреть состояние на месте:
kubectl debug -n <namespace> -it pod/<pod> \ --container=debug-<incident-id> \ --image=<registry>/platform/debug-tools:<version> \ --target=<container> \ --profile=general \ -- /bin/bash
Дальше не «посмотрим всё», а проверка конкретной гипотезы.
DNS:
cat /etc/resolv.confdig <service>.<namespace>.svc.cluster.localdig @<cluster-dns-ip> <name>
Локальный порт и адрес, на котором слушает приложение:
ss -lntpcurl -v http://127.0.0.1:<port>/healthcurl -v http://<pod-ip>:<port>/health
TLS и сертификаты:
openssl s_client -connect <host>:443 -servername <host> </dev/nullps auxls -la /proc/<app-pid>/root/etc/ssl/certs
Проверка, что вы действительно видите процессы целевого контейнера:
ps auxcat /proc/1/cgroup
Если приложение падает на старте, не надо пытаться поймать его временным контейнером между рестартами. Делайте копию:
kubectl debug -n <namespace> pod/<pod> -it \ --copy-to=<pod>-debug \ --container=<container> \ --set-image=<container>=<registry>/<service>:debug \ -- /bin/sh
После работы копию удалить:
kubectl delete pod -n <namespace> <pod>-debug
Временный контейнер из исходного Pod удалить нельзя; запись останется до смерти Pod. Поэтому имя должно быть осмысленным. debug-inc-1842 лучше, чем debugger-q9x2k. Через неделю это будет не эстетика, а археология инцидента.
Самый взрослый вывод здесь не про флаг --target и не про то, какой образ лучше — busybox, ubuntu или netshoot.
Главный вывод: distroless требует перенести удобство из образа приложения в платформу.
Не надо возвращать bash в боевой образ, чтобы инженеру было спокойно. Надо дать инженеру нормальный разрешённый путь: проверенный отладочный образ, явные профили, ограниченные права, аудит, короткие сессии и инструкция, которая различает отладку Pod, копию Pod и отладку узла.
Distroless не делает систему недебажной. Он просто прекращает врать, что каждый production-контейнер должен быть маленьким сервером, куда можно зайти и жить внутри.
ссылка на оригинал статьи https://habr.com/ru/articles/1040748/