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.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.