Kubernetes для самых маленьких. Часть 11: Зачем нужны Namespace, метки и аннотации?
Все части серии «Kubernetes для самых маленьких»В прошлой части мы мимоходом создали неймспейс quota-test, повесили на него квоту и удалили одной командой вместе со всем содержимым. А с части 7 Deployment находит свои поды по меткам, Service из части 15 будет находить их так же, и DaemonSet из части 8 выбирает узлы по меткам. Пора разобрать эти механизмы отдельно. Неймспейсы, метки и аннотации живут в metadata каждого объекта, с которым мы познакомились в части 3, и именно через них объекты находят друг друга. В конце части появится учебный неймспейс demo, в котором будут жить все примеры дальше.
Namespace: область имён
Неймспейс — это прежде всего область уникальности имён. Два Deployment с именем hello в одном неймспейсе существовать не могут, в разных — пожалуйста. Сам неймспейс — обычный объект API Namespace, а у большинства других объектов в metadata.namespace записано, какому неймспейсу они принадлежат. Ключ объекта в etcd (хранилище, где API-сервер держит все объекты) включает неймспейс: /registry/deployments/demo/hello.
Сразу после установки в кластере четыре неймспейса:
| Неймспейс | Что в нём |
|---|---|
default |
всё, что создано без указания неймспейса; в эксплуатации им лучше не пользоваться |
kube-system |
компоненты кластера: kube-proxy, CoreDNS (DNS-сервер кластера), статические поды control plane |
kube-public |
объекты, которые может читать кто угодно, даже без аутентификации; kubeadm хранит там ConfigMap cluster-info для присоединения узлов |
kube-node-lease |
объекты Lease — по одному на узел; kubelet обновляет свой каждые 10 секунд при сроке аренды 40 секунд, и по ним control plane понимает, что узел жив |
Компоненты, которые мы ставили, создали свои: calico-system и tigera-operator для Calico. Префикс kube- зарезервирован за системой, свои неймспейсы так называть не стоит. Имя неймспейса должно быть DNS-меткой по RFC 1123 — до 63 символов из строчных латинских букв, цифр и дефиса, — потому что оно входит в DNS-имена сервисов: hello.demo.svc.cluster.local. Имя с заглавной буквой API-сервер не примет: kubectl create namespace Demo2 получит отказ metadata.name: Invalid value: "Demo2": a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character.
Из этого же следует неочевидная опасность, о которой предупреждает документация. Если создать неймспейс с именем, совпадающим с публичным доменом верхнего уровня, например com или dev, и в нём сервис example, то поды любого неймспейса, запрашивающие example.com без точки на конце, через список поиска DNS попадут на этот сервис, а не в интернет. Поэтому право создавать неймспейсы дают только доверенным пользователям. Как работает список поиска — в части 16.
Что неймспейс изолирует, а что нет
Неймспейс часто воспринимают как «отдельное окружение». Это верно лишь отчасти.
Изолирует или ограничивает по неймспейсу:
- имена объектов;
- права: Role и RoleBinding в RBAC (механизме разграничения доступа к API, часть 29) действуют внутри одного неймспейса — можно дать команде полные права в
team-aи никаких вteam-b; - квоты и умолчания: ResourceQuota и LimitRange из прошлой части;
- сетевые политики: NetworkPolicy (часть 20) описывает правила для подов своего неймспейса и умеет выбирать соседей по меткам неймспейсов;
- уровень безопасности подов: Pod Security Admission (встроенная проверка спецификаций подов на опасные настройки, часть 30) включается метками на неймспейсе;
- доступ к Secret, ConfigMap и ServiceAccount: под может смонтировать Secret и использовать ServiceAccount только из своего неймспейса;
- ссылки на владельца: объект может принадлежать только объекту своего неймспейса или объекту вне неймспейсов.
Не изолирует:
- сеть. По умолчанию любой под может открыть соединение с любым другим подом в кластере, в каком бы неймспейсе они ни были. Без NetworkPolicy неймспейсы в сети не видны вовсе;
- узлы и ядро. Поды разных неймспейсов работают на одних и тех же узлах, рядом в одном ядре Linux; изоляция между ними — изоляция контейнеров, а не неймспейсов Kubernetes;
- ресурсы узлов — без квот неймспейс может занять весь кластер;
- DNS — имя сервиса из любого неймспейса разрешается из любого другого по полному имени;
- объекты уровня кластера — о них ниже.
Итого: неймспейс — граница для API (имена, права, политики, которые к нему привязаны), но не граница безопасности сама по себе. Мультиарендность — когда в одном кластере работают разные команды или клиенты — собирается из неймспейса плюс RBAC, NetworkPolicy, ResourceQuota и Pod Security, и только все вместе дают изоляцию.
Небольшое терминологическое замечание: неймспейсы Kubernetes и пространства имён Linux (namespaces ядра — net, pid, mnt и другие, на которых строится изоляция контейнеров) — разные вещи с похожим названием. Первые разделяют объекты в API, вторые — процессы на узле.
Объекты вне неймспейсов
Часть ресурсов существует на уровне всего кластера: они описывают сам кластер или общие для всех вещи.
| Ресурс | Почему не в неймспейсе |
|---|---|
Namespace |
неймспейс не может лежать в неймспейсе |
Node |
узел общий для всех |
PersistentVolume, StorageClass, CSIDriver |
хранилище кластера; а вот заявка PersistentVolumeClaim — в неймспейсе (часть 21) |
ClusterRole, ClusterRoleBinding |
права на весь кластер (часть 29) |
CustomResourceDefinition |
определение нового типа API (часть 28) |
PriorityClass, IngressClass, RuntimeClass |
общие классы, на которые ссылаются объекты в неймспейсах |
ValidatingWebhookConfiguration, MutatingWebhookConfiguration |
настройки обработчиков запросов API-сервера |
Полный список даёт kubectl api-resources --namespaced=false, а --namespaced=true — список типов, которые живут в неймспейсах. На таких объектах metadata.namespace пуст, а флаг -n в kubectl игнорируется.
Удаление неймспейса и финализаторы
kubectl delete namespace demo удаляет неймспейс со всем содержимым, но не мгновенно. Это хороший пример того, как в Kubernetes устроено удаление вообще.
Финализатор — строка в списке финализаторов объекта, означающая «прежде чем объект исчезнет, кто-то должен закончить работу». Пока список не пуст, удаление только помечает объект: API-сервер ставит metadata.deletionTimestamp, и объект остаётся видимым. Контроллер, поставивший финализатор, видит пометку, делает свою работу и убирает свою строку. Когда строк не осталось, API-сервер удаляет объект из etcd. Мы видели это в прошлых частях: batch.kubernetes.io/job-tracking на подах Job; у заявок на тома есть kubernetes.io/pvc-protection, который не даёт удалить PVC, пока её использует под.
У неймспейса финализатор особый — kubernetes в поле spec.finalizers. Удаление выглядит так:
kubectl delete namespace demo
│
▼
API-сервер: deletionTimestamp, status.phase = Terminating
новые объекты в неймспейсе создавать нельзя
│
▼
контроллер неймспейсов (kube-controller-manager):
1. опрашивает API: какие вообще есть типы объектов в неймспейсах
2. удаляет все поды и ждёт, пока они исчезнут
3. удаляет все остальные объекты всех типов
4. ждёт, пока исчезнут и они — со своими финализаторами
5. убирает "kubernetes" из spec.finalizers
│
▼
API-сервер удаляет Namespace
Шаг 2 — изменение 1.34 (упорядоченное удаление неймспейса стало стабильным): сначала уходят поды, а потом всё остальное. Раньше порядок был произвольным, и под мог какое-то время работать, когда его NetworkPolicy уже удалена, — то есть без защиты.
Ход удаления отражается в условиях status.conditions неймспейса, и если неймспейс «завис» в Terminating, ответ почти всегда там:
| Условие | Что значит, если True |
|---|---|
NamespaceDeletionDiscoveryFailure |
контроллер не смог получить список типов API — обычно из-за недоступного агрегированного API (его API-сервер не обслуживает сам, а проксирует стороннему серверу), например упавшего metrics-server — компонента, который отдаёт метрики потребления подов (часть 32) |
NamespaceDeletionGroupVersionParsingFailure |
не удалось разобрать какую-то группу API |
NamespaceDeletionContentFailure |
ошибка при удалении объектов |
NamespaceContentRemaining |
объекты ещё остались — перечислены по типам |
NamespaceFinalizersRemaining |
у оставшихся объектов есть финализаторы — перечислены по именам |
Самая частая причина зависания — объект с финализатором, который некому снять: оператор (контроллер для собственных типов объектов, часть 28) удалили раньше, чем его объекты, и его финализаторы на них остались навсегда. Правильное решение — вернуть контроллер, дать ему убрать за собой и только потом удалить. Если контроллера уже не будет, финализатор снимают руками с конкретного объекта, понимая, какую работу он должен был сделать (например, удалить внешний ресурс — диск или балансировщик в облаке, — который теперь придётся удалить самому).
В интернете часто советуют «лечить» зависший неймспейс, обнуляя его spec.finalizers через подресурс /finalize. Это убирает неймспейс из API, но оставляет в etcd все объекты, которые контроллер не успел удалить, — без неймспейса, невидимые для kubectl и ни для чего не нужные. Если неймспейс с тем же именем потом создадут снова, они «оживут» в нём. Этот приём — последнее средство, а не способ удаления.
Метки
Метка (label) — пара «ключ — значение» в metadata.labels, которая описывает объект так, чтобы по ней можно было выбирать: «все поды приложения hello», «все узлы с SSD», «всё, что относится к выпуску canary».
Синтаксис ключа: необязательный префикс и имя через /. Имя — до 63 символов из букв, цифр, -, _, ., начинается и кончается буквой или цифрой. Префикс — DNS-поддомен до 253 символов: app.kubernetes.io/name, example.com/team. Префиксы kubernetes.io/ и k8s.io/ зарезервированы за компонентами Kubernetes; API-сервер этого не проверяет (kubectl label pod … kubernetes.io/foo=bar пройдёт), так что это соглашение, а не запрет. Ключ без префикса считается принадлежащим пользователю. Значение — до 63 символов по тем же правилам, может быть пустым.
Ограничения длины и набора символов не случайны: метки предназначены для отбора, а не для хранения данных, и сервер должен быстро сравнивать их на каждом запросе с селектором. Фильтрация выполняется на стороне API-сервера: kubectl get pods -l app=web получает от сервера только подходящие поды, и контроллеры подписываются на изменения так же, с селектором, не вытягивая весь кластер.
Селекторы
Селектор меток (label selector) — условие на метки. Два вида требований:
- по равенству:
app=web(илиapp==web),tier!=frontend; - по множеству:
env in (prod,staging),env notin (dev),canary(ключ есть),!canary(ключа нет).
Требования через запятую объединяются логическим «И»; «ИЛИ» между требованиями нет — его заменяет in. Тонкость: tier!=frontend и env notin (dev) выбирают в том числе объекты, у которых такой метки нет вовсе.
В манифестах селектор записывается двумя способами:
selector:
matchLabels: # равенство, по каждой паре
app.kubernetes.io/name: hello
matchExpressions: # множества
- key: environment
operator: In # In, NotIn, Exists, DoesNotExist
values: [prod, staging]
Все условия matchLabels и matchExpressions объединяются через «И». Такую форму понимают Deployment, ReplicaSet, StatefulSet, DaemonSet, Job, NetworkPolicy, PodDisruptionBudget. У Service и у старого ReplicationController селектор — простой словарь, только равенство.
На селекторах держится связь контроллеров с подами, и здесь две ловушки:
- селектор Deployment, ReplicaSet, StatefulSet и DaemonSet неизменяем после создания (в
apps/v1): попытка его изменить кончается ошибкойspec.selector: Invalid value: …: field is immutable. Чтобы поменять селектор, объект пересоздают. Значит, в селектор нельзя класть метки, которые будут меняться, — например, версию; - селекторы не должны пересекаться. Если два Deployment выбирают одни и те же поды, их ReplicaSet будут считать чужие поды своими и бороться: один удаляет «лишние», другой создаёт «недостающие». Kubernetes это не запрещает. ReplicaSet защищён от части случаев меткой
pod-template-hash, которую Deployment добавляет и в шаблон, и в селектор, но от пересечения между разными приложениями защищает только аккуратность.
Рекомендуемые метки
Проект Kubernetes предлагает общий набор меток с префиксом app.kubernetes.io/, чтобы инструменты — kubectl, Helm, панели мониторинга — понимали объекты одинаково:
| Метка | Смысл | Пример |
|---|---|---|
app.kubernetes.io/name |
имя приложения | hello |
app.kubernetes.io/instance |
конкретный экземпляр установки | hello-demo |
app.kubernetes.io/version |
версия приложения | 1.0 |
app.kubernetes.io/component |
роль в архитектуре | web, database |
app.kubernetes.io/part-of |
более крупное приложение, частью которого это является | shop |
app.kubernetes.io/managed-by |
чем управляется объект | kubectl, Helm |
Ставить их стоит на все объекты приложения — Deployment, Service, ConfigMap, шаблон пода, — а в селектор брать только неизменные: name и instance. version в селекторе — та самая ловушка: при выпуске версии 1.1 селектор Deployment придётся менять, а он неизменяем. Менеджер пакетов Helm (часть 27) ставит эти метки сам, с managed-by: Helm.
Kubernetes и сам ставит хорошо известные метки, и на них опираются планировщик и контроллеры: на узлах kubernetes.io/hostname, kubernetes.io/os, kubernetes.io/arch, node-role.kubernetes.io/control-plane, в облаках — topology.kubernetes.io/zone; на неймспейсах kubernetes.io/metadata.name с именем неймспейса (её ставит API-сервер и молча возвращает, если метку изменить или удалить, поэтому по ней удобно выбирать неймспейс в NetworkPolicy); на подах контроллеров — pod-template-hash, controller-revision-hash, batch.kubernetes.io/job-name, которые мы видели в прошлых частях.
Аннотации
Аннотация — тоже пара «ключ — значение» в metadata.annotations, с тем же синтаксисом ключа, но для другой цели: хранить данные об объекте, по которым не выбирают. Значение — произвольная строка, хоть многострочный JSON; ограничение — 256 КиБ на все аннотации объекта вместе, ключи и значения. Запрос, который его превышает, API-сервер отвергает: The Deployment "hello" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes.
Аннотации читают инструменты и контроллеры:
kubectl.kubernetes.io/last-applied-configuration— копия манифеста, которую клиентскийkubectl applyхранит, чтобы при следующемapplyпонять, какие поля вы удалили (server-side apply из части 3 хранит это иначе, вmanagedFields);deployment.kubernetes.io/revision— номер ревизии на ReplicaSet;kubernetes.io/change-cause— причина изменения, которую показываетkubectl rollout history;batch.kubernetes.io/cronjob-scheduled-timestamp— время запуска Job из части 9;- аннотации внешних контроллеров: Reloader (часть 12) по аннотации на Deployment перезапускает его при смене конфигурации, cert-manager (часть 19) — выпускает сертификат по аннотации на Ingress.
Как выбрать между меткой и аннотацией:
| Вопрос | Метка | Аннотация |
|---|---|---|
| Нужно ли выбирать объекты по этому значению? | да | нет |
| Длина и формат значения | до 63 символов, ограниченный набор | любая строка до 256 КиБ всего |
| Кто обычно читает | селекторы контроллеров, kubectl -l |
инструменты, контроллеры, люди |
| Примеры | приложение, компонент, окружение, команда | ссылка на репозиторий, хеш коммита, контакт дежурного, настройки контроллера |
Хеш коммита, из которого собран образ, — аннотация: выбирать по нему не нужно, а смотреть при разборе полезно. Команда-владелец — метка: по ней строят отчёты и квоты.
Селекторы по полям
Метки — то, что вы повесили на объект. Иногда нужно выбрать по полю самого объекта: все поды на узле k8s-node-2, все не работающие поды, все события конкретного объекта. Для этого есть селектор по полям (field selector):
$ kubectl get pods -A --field-selector spec.nodeName=k8s-node-2
$ kubectl get pods -A --field-selector status.phase!=Running,status.phase!=Succeeded
$ kubectl get events --field-selector involvedObject.name=web-0
$ kubectl get secrets -A --field-selector type=kubernetes.io/tls
Поддерживаются только =, ==, != и только определённые поля: у всех типов — metadata.name и metadata.namespace, а дальше по типу. У подов — spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, spec.hostNetwork, status.phase, status.podIP, status.podIPs, status.nominatedNodeName; у событий — поля involvedObject.*, reason, reportingComponent, source, type; у Secret — type; у Service — spec.type и spec.clusterIP; у узлов — spec.unschedulable; у неймспейсов — status.phase; у ReplicaSet — status.replicas, у Job — status.successful, у CertificateSigningRequest — spec.signerName. Селектор по полям сочетается с селектором меток: --field-selector status.phase=Running -l app.kubernetes.io/instance=debug. Неподдерживаемое поле API-сервер отвергает, и текст ошибки зависит от типа: у ConfigMap и других типов, где есть только общие поля, — "data.a" is not a known field selector: only "metadata.name", "metadata.namespace", у подов — field label not supported: spec.containers. Операторов in и notin здесь нет: status.phase in (Running) получит invalid selector. Именно селектором spec.nodeName=<свой узел> kubelet подписывается на поды своего узла, не получая весь кластер.
kubectl и метки
$ kubectl label pod web-0 tier=backend # добавить
$ kubectl label pod web-0 tier=api --overwrite # изменить
$ kubectl label pod web-0 tier- # удалить
$ kubectl annotate deployment hello example.com/owner='team-a@example.com'
$ kubectl get pods --show-labels # все метки столбцом
$ kubectl get pods -L app.kubernetes.io/version # значение метки отдельным столбцом
$ kubectl get pods -l 'app.kubernetes.io/name=hello,environment in (prod,staging)'
$ kubectl get all -A -l app.kubernetes.io/part-of=shop
Изменение метки на поде, принадлежащем ReplicaSet, — способ «вынуть» под из-под управления для разбора: снимите метку, по которой ReplicaSet его выбирает, — он создаст замену, а старый под останется жить отдельно, без трафика от Service (если Service выбирает по той же метке), и его можно спокойно исследовать.
Контекст и неймспейс по умолчанию
Каждая команда kubectl без -n работает в неймспейсе по умолчанию, и он хранится не в кластере, а в вашем kubeconfig — файле ~/.kube/config, который мы получили в части 4. В нём три списка и указатель:
apiVersion: v1
kind: Config
clusters: # куда подключаться: адрес API-сервера и CA
- name: kubernetes
cluster:
server: https://k8s-api.example.com:6443
certificate-authority-data: LS0t...
users: # с чем подключаться: сертификат, токен
- name: kubernetes-admin
user:
client-certificate-data: LS0t...
client-key-data: LS0t...
contexts: # контекст = кластер + пользователь + неймспейс
- name: kubernetes-admin@kubernetes
context:
cluster: kubernetes
user: kubernetes-admin
namespace: demo
current-context: kubernetes-admin@kubernetes
Контекст связывает кластер, учётные данные и неймспейс по умолчанию; current-context — какой из них активен. Команды:
$ kubectl config get-contexts
$ kubectl config set-context --current --namespace=demo
$ kubectl config view --minify # только текущий контекст
$ kubectl config use-context <имя>
Для тех, кто часто переключается, есть маленькие утилиты kubectx и kubens: первая переключает контексты, вторая — неймспейс текущего контекста.
Неймспейс в манифесте и неймспейс в контексте взаимодействуют так: если metadata.namespace в файле задан, объект создаётся в нём, что бы ни было в контексте; если не задан — в неймспейсе из -n или из контекста. Если в файле указан один неймспейс, а флагом -n — другой, kubectl откажется с ошибкой the namespace from the provided object "demo" does not match the namespace "default". You must pass '--namespace=demo' to perform this operation. Практика — выбрать один стиль и держаться его: либо неймспейс прописан в каждом манифесте (надёжнее, особенно при выкатке из Git), либо не прописан нигде и задаётся при применении (так делает Kustomize — встроенный в kubectl инструмент сборки манифестов, часть 26). Полагаться на «текущий контекст» в скриптах опасно: однажды он окажется не тем.
Что касается учётных данных в этом файле: kubernetes-admin из admin.conf kubeadm — это полный администратор кластера, и хранить его на ноутбуке как единственный способ доступа — плохая практика. Как выдавать людям отдельные учётные записи с ограниченными правами — в части 29.
Учебный неймспейс demo
С этой части все примеры живут в неймспейсе demo. Создадим его манифестом — с метками и сразу с умолчаниями ресурсов из прошлой части:
apiVersion: v1
kind: Namespace
metadata:
name: demo
labels:
app.kubernetes.io/part-of: k8s-for-beginners
environment: lab
---
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: demo
spec:
limits:
- type: Container
defaultRequest:
cpu: 100m
memory: 128Mi
default:
memory: 128Mi
И перенесём туда hello из части 7, уже с рекомендуемыми метками:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
namespace: demo
labels: &labels
app.kubernetes.io/name: hello
app.kubernetes.io/instance: hello
app.kubernetes.io/version: "1.0"
app.kubernetes.io/component: web
app.kubernetes.io/part-of: k8s-for-beginners
app.kubernetes.io/managed-by: kubectl
annotations:
example.com/source: https://git.example.com/demo/hello
spec:
replicas: 3
selector:
matchLabels: # только неизменные метки
app.kubernetes.io/name: hello
app.kubernetes.io/instance: hello
template:
metadata:
labels: *labels
spec:
containers:
- name: hello
image: ghcr.io/example/hello:1.0
ports:
- name: http
containerPort: 8080
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
memory: 64Mi
&labels и *labels — якорь и ссылка YAML: один и тот же набор меток вставлен и в объект, и в шаблон пода без повторения. kubectl раскрывает их перед отправкой на сервер. Секрет ghcr-pull для доступа к реестру из части 5 нужно создать и в новом неймспейсе (тем же kubectl create secret docker-registry, но с -n demo) и привязать к ServiceAccount default неймспейса demo: Secret из default поду в demo недоступен — это как раз одна из границ неймспейса.
Смотрим руками
Неймспейсы и метка, которую ставит на них сам кластер:
$ kubectl get namespaces --show-labels
NAME STATUS AGE LABELS
calico-system Active 4d3h app.kubernetes.io/component=Installation.operator.tigera.io,app.kubernetes.io/instance=default,app.kubernetes.io/managed-by=tigera-operator,app.kubernetes.io/name=calico-system,app.kubernetes.io/part-of=Calico,k8s-app=calico-system,kubernetes.io/metadata.name=calico-system,name=calico-system,pod-security.kubernetes.io/enforce-version=latest,pod-security.kubernetes.io/enforce=privileged
default Active 4d3h kubernetes.io/metadata.name=default
kube-node-lease Active 4d3h kubernetes.io/metadata.name=kube-node-lease
kube-public Active 4d3h kubernetes.io/metadata.name=kube-public
kube-system Active 4d3h kubernetes.io/metadata.name=kube-system
tigera-operator Active 4d3h kubernetes.io/metadata.name=tigera-operator,name=tigera-operator,pod-security.kubernetes.io/enforce=privileged
$ kubectl apply -f demo-namespace.yaml
namespace/demo created
limitrange/defaults created
$ kubectl config set-context --current --namespace=demo
Context "kubernetes-admin@kubernetes" modified.
$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* kubernetes-admin@kubernetes kubernetes kubernetes-admin demo
На calico-system и tigera-operator стоят метки Pod Security: в этих неймспейсах поды Calico и его оператора работают с привилегиями, и это объявлено явно. А Tigera operator (контроллер, который устанавливает Calico) повесил на свой calico-system ещё и рекомендуемые метки app.kubernetes.io/* — с part-of=Calico и managed-by=tigera-operator. Сам объект неймспейса:
$ kubectl get namespace demo -o yaml
apiVersion: v1
kind: Namespace
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{},"labels":{"app.kubernetes.io/part-of":"k8s-for-beginners","environment":"lab"},"name":"demo"}}
creationTimestamp: "2026-10-28T13:36:08Z"
labels:
app.kubernetes.io/part-of: k8s-for-beginners
environment: lab
kubernetes.io/metadata.name: demo
name: demo
resourceVersion: "762975"
uid: 55406057-7dd7-4ac9-a424-55bdf02d445f
spec:
finalizers:
- kubernetes
status:
phase: Active
Финализатор kubernetes на месте с момента создания, метку kubernetes.io/metadata.name добавил API-сервер, а аннотацию last-applied-configuration — kubectl apply. Время в creationTimestamp API-сервер всегда пишет в UTC. Метку с именем неймспейса не изменить — API-сервер возвращает её молча:
$ kubectl label ns demo kubernetes.io/metadata.name=other --overwrite
namespace/demo labeled
$ kubectl get ns demo -L kubernetes.io/metadata.name
NAME STATUS AGE METADATA.NAME
demo Active 4m21s demo
Какие типы живут вне неймспейсов:
$ kubectl api-resources --namespaced=false -o name | head -16
componentstatuses
namespaces
nodes
persistentvolumes
mutatingadmissionpolicies.admissionregistration.k8s.io
mutatingadmissionpolicybindings.admissionregistration.k8s.io
mutatingwebhookconfigurations.admissionregistration.k8s.io
validatingadmissionpolicies.admissionregistration.k8s.io
validatingadmissionpolicybindings.admissionregistration.k8s.io
validatingwebhookconfigurations.admissionregistration.k8s.io
customresourcedefinitions.apiextensions.k8s.io
apiservices.apiregistration.k8s.io
selfsubjectreviews.authentication.k8s.io
tokenreviews.authentication.k8s.io
selfsubjectaccessreviews.authorization.k8s.io
selfsubjectrulesreviews.authorization.k8s.io
Дальше в этом списке — certificatesigningrequests, storageclasses, priorityclasses, clusterroles и собственные типы Calico — bgppeers, ippools и другие: настройки сети общие для кластера. Типы Calico в списке по два раза: как CRD в группе crd.projectcalico.org и в группе projectcalico.org, которую обслуживает calico-apiserver — тот самый агрегированный API, из-за которого может зависнуть удаление неймспейса. Всего в нашем кластере 79 типов вне неймспейсов и 41 — в неймспейсах.
Под из частного реестра в новом неймспейсе не запустится, пока там нет своего ghcr-pull. Создав Secret с -n demo, привязываем его к ServiceAccount default — контекст уже смотрит в demo:
$ kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"ghcr-pull"}]}'
serviceaccount/default patched
Теперь hello в demo и выборки по меткам:
$ kubectl apply -f hello.yaml
deployment.apps/hello created
$ kubectl get pods -L app.kubernetes.io/version,app.kubernetes.io/component
NAME READY STATUS RESTARTS AGE VERSION COMPONENT
hello-6d5c6db6b4-b5kn2 1/1 Running 0 10s 1.0 web
hello-6d5c6db6b4-wc775 1/1 Running 0 10s 1.0 web
hello-6d5c6db6b4-zhgzw 1/1 Running 0 10s 1.0 web
$ kubectl get pods -l 'app.kubernetes.io/name=hello,app.kubernetes.io/version notin (0.9)' -o name
pod/hello-6d5c6db6b4-b5kn2
pod/hello-6d5c6db6b4-wc775
pod/hello-6d5c6db6b4-zhgzw
$ kubectl get pods --field-selector spec.nodeName=k8s-node-2 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hello-6d5c6db6b4-b5kn2 1/1 Running 0 18s 172.20.172.20 k8s-node-2 <none> <none>
hello-6d5c6db6b4-wc775 1/1 Running 0 18s 172.20.172.19 k8s-node-2 <none> <none>
Два пода из трёх планировщик поставил на k8s-node-2, третий, zhgzw, — на k8s-node-1.
Вынем один под из-под управления ReplicaSet, сменив метку из селектора:
$ kubectl label pod hello-6d5c6db6b4-zhgzw app.kubernetes.io/instance=debug --overwrite
pod/hello-6d5c6db6b4-zhgzw labeled
$ kubectl get pods -L app.kubernetes.io/instance
NAME READY STATUS RESTARTS AGE INSTANCE
hello-6d5c6db6b4-b5kn2 1/1 Running 0 31s hello
hello-6d5c6db6b4-fnnj2 1/1 Running 0 3s hello
hello-6d5c6db6b4-wc775 1/1 Running 0 31s hello
hello-6d5c6db6b4-zhgzw 1/1 Running 0 31s debug
$ kubectl get pod hello-6d5c6db6b4-zhgzw -o jsonpath='{.metadata.ownerReferences}{"\n"}'
ReplicaSet перестал видеть под, создал замену fnnj2 и снял с отвязанного пода ссылку на владельца — теперь это самостоятельный под, который никто не перезапустит и не удалит. После разбора его удаляют руками: kubectl delete pod hello-6d5c6db6b4-zhgzw.
Без --overwrite существующую метку или аннотацию kubectl не меняет, а неправильные ключи и значения отвергаются до сохранения: ключ проверяет API-сервер, значение — сам kubectl, ещё до запроса:
$ kubectl label pod hello-6d5c6db6b4-zhgzw app.kubernetes.io/instance=debug2
error: 'app.kubernetes.io/instance' already has a value (debug), and --overwrite is false
$ kubectl annotate deployment hello example.com/owner='team-a@example.com'
deployment.apps/hello annotated
$ kubectl annotate deployment hello example.com/owner=x
error: --overwrite is false but found the following declared annotation(s): 'example.com/owner' already has a value (team-a@example.com)
$ kubectl annotate deployment hello example.com/owner-
deployment.apps/hello annotated
$ kubectl label pod hello-6d5c6db6b4-zhgzw 'Bad Key=1'
The Pod "hello-6d5c6db6b4-zhgzw" is invalid: metadata.labels: Invalid value: "Bad Key": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')
$ kubectl label pod hello-6d5c6db6b4-zhgzw example_com/team=a
The Pod "hello-6d5c6db6b4-zhgzw" is invalid: metadata.labels: Invalid value: "example_com/team": prefix part a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
$ kubectl label pod hello-6d5c6db6b4-zhgzw tier=-bad
error: invalid label value: "tier=-bad": a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')
$ kubectl label pod hello-6d5c6db6b4-zhgzw tier=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
error: invalid label value: "tier=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": must be no more than 63 bytes
Обратите внимание: удаление аннотации kubectl сообщает тем же словом annotated, а удаление метки — словом unlabeled. Предел длины считается в байтах, а не в символах. И селектор Deployment действительно не изменить — даже добавив к нему метку, которая и так стоит на всех подах:
$ kubectl patch deployment hello --type=merge -p '{"spec":{"selector":{"matchLabels":{"app.kubernetes.io/version":"1.0"}}}}'
The Deployment "hello" is invalid: spec.selector: Invalid value: {"matchLabels":{"app.kubernetes.io/instance":"hello","app.kubernetes.io/name":"hello","app.kubernetes.io/version":"1.0"}}: field is immutable
Неймспейс, который не удаляется. Создадим trash с ConfigMap, на котором висит финализатор, за которым никто не придёт:
$ kubectl create namespace trash
namespace/trash created
$ kubectl -n trash create configmap stuck --from-literal=a=b
configmap/stuck created
$ kubectl -n trash patch configmap stuck --type=merge -p '{"metadata":{"finalizers":["example.com/hold"]}}'
configmap/stuck patched
$ kubectl delete namespace trash --wait=false
namespace "trash" deleted
$ kubectl get namespace trash
NAME STATUS AGE
trash Terminating 32s
$ kubectl get namespace trash -o jsonpath='{range .status.conditions[*]}{.type}={.status}: {.message}{"\n"}{end}'
NamespaceDeletionDiscoveryFailure=False: All resources successfully discovered
NamespaceDeletionGroupVersionParsingFailure=False: All legacy kube types successfully parsed
NamespaceDeletionContentFailure=False: All content successfully deleted, may be waiting on finalization
NamespaceContentRemaining=True: Some resources are remaining: configmaps. has 1 resource instances
NamespaceFinalizersRemaining=True: Some content in the namespace has finalizers remaining: example.com/hold in 1 resource instances
$ kubectl -n trash get configmap stuck -o jsonpath='{.metadata.deletionTimestamp}{"\n"}'
2026-10-28T13:38:18Z
$ kubectl -n trash create configmap new --from-literal=a=b
error: failed to create configmap: configmaps "new" is forbidden: unable to create new content in namespace trash because it is being terminated
Всё по схеме: контроллер удалил что мог (в том числе ConfigMap kube-root-ca.crt, который есть в каждом неймспейсе), ConfigMap stuck помечен на удаление, но держится за финализатор, условия неймспейса прямо называют виновника, а создавать новое в удаляемом неймспейсе нельзя. Снимаем финализатор — неймспейс исчезает сам, хотя и не в ту же секунду: контроллер перепроверяет зависший неймспейс с паузой, и на стенде он пропадал через несколько секунд:
$ kubectl -n trash patch configmap stuck --type=merge -p '{"metadata":{"finalizers":null}}'
configmap/stuck patched
$ kubectl get namespace trash
NAME STATUS AGE
trash Terminating 47s
$ kubectl get namespace trash
Error from server (NotFound): namespaces "trash" not found
Источники
- Namespaces — начальные неймспейсы, DNS-имена и опасность имён, совпадающих с доменами верхнего уровня, объекты вне неймспейсов.
- Share a Cluster with Namespaces — фазы
ActiveиTerminating, удаление неймспейса и полеfinalizers. - KEP-5080: Ordered Namespace Deletion — почему поды удаляются раньше остальных объектов.
- Finalizers — как финализаторы задерживают удаление объекта.
- Object Names and IDs — правила имён: DNS-метки и поддомены по RFC 1123.
- Labels and Selectors — синтаксис меток, селекторы по равенству и по множеству,
matchLabelsиmatchExpressions. - Recommended Labels — метки
app.kubernetes.io/*. - Annotations — синтаксис ключей и предел 256 КиБ.
- Field Selectors — поддерживаемые поля по типам и текст ошибки.
- Well-Known Labels, Annotations and Taints — справочник меток и аннотаций, которые ставит сам Kubernetes.
- Organizing Cluster Access Using kubeconfig Files — кластеры, пользователи и контексты в kubeconfig.
Метки, аннотации и неймспейсы — это метаданные. Но у приложения есть и собственная конфигурация: адрес базы, параметры, пароли. Пока мы передавали их переменными прямо в манифесте, а пароль базы в части 9 взяли из Secret, не объяснив, что это такое.
Дальше — ConfigMap и Secret: как передать конфигурацию в под переменными и файлами, что происходит при её изменении и почему переменные не обновляются, как перезапускать поды по изменению через Reloader, почему base64 — не шифрование, кто может читать Secret и как зашифровать секреты в etcd.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.