← Kubernetes: дальше
8 мин · жёстко · Урок 2 из 3

Миграция Deployment между namespace без даунтайма

Почему namespace нельзя поменять на лету, какие поля чистить в экспорте и в каком порядке переносить объекты, чтобы сервис не лёг.

Почему миграция это не mv

Namespace в Kubernetes хранится прямо в metadata.namespace каждого объекта. Поменять его на лету нельзя: namespace входит в путь ресурса в etcd, и для apiserver deployment/web в dev и deployment/web в staging это два разных объекта.

Отсюда единственный рабочий сценарий: экспорт, чистка, импорт в новый namespace, проверка, удаление старого. Ровно в таком порядке, потому что любая перестановка шагов даёт окно недоступности.

Сначала инвентаризация

kubectl get all вводит в заблуждение: all это не «все объекты namespace», а короткий список из Pod, Service, Deployment, ReplicaSet, StatefulSet, DaemonSet, Job, CronJob. ConfigMap, Secret, Ingress, PVC, ServiceAccount, RoleBinding, NetworkPolicy, HorizontalPodAutoscaler и PodDisruptionBudget в него не попадают.

Полный список того, что вообще может лежать в namespace, показывает api-resources:

kubectl get all -n dev
kubectl get cm,secret,ingress,pvc,sa,netpol,hpa,pdb -n dev
kubectl api-resources --namespaced=true -o name

До миграции стоит выписать, что именно переезжает. Пропущенный ConfigMap обнаруживается уже по CrashLoop в новом namespace.

Что экспортируется лишнего

kubectl get deploy web -n dev -o yaml отдаёт всё, включая поля, которые apiserver заполняет сам. Если применить такой YAML без чистки, получите ошибки:

Аннотация kubectl.kubernetes.io/last-applied-configuration тоже приезжает из экспорта и тянет за собой старый namespace внутри JSON. Вреда от неё нет, но чище её удалить вместе с остальным.

Самый аккуратный способ почистить, без флага --export (он давно удалён из kubectl):

kubectl get deploy web -n dev -o yaml \
  | yq 'del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, .metadata.generation, .metadata.annotations."kubectl.kubernetes.io/last-applied-configuration", .status)' \
  | yq '.metadata.namespace = "staging"' \
  > /tmp/web.yaml

Забытый metadata.namespace превращает миграцию в обновление на месте: манифест уедет обратно в dev, поверх старого Deployment.

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

Отдельно про Pod: экспортировать их бессмысленно. Pod-ы создаёт ReplicaSet, которым владеет Deployment, связь держится через metadata.ownerReferences. Переносят Deployment, а Pod-ы в новом namespace появятся сами.

Порядок миграции без даунтайма

Главное правило: сначала поднимаем в новом namespace, потом убираем старое. Иначе в окне между удалением и применением сервис лежит.

dev              staging
 cm, secret  ─1─>  cm, secret
 deploy      ─2─>  deploy
 svc         ─3─>  svc
 delete dev  <─  после endpoints
  1. Создаём целевой namespace: kubectl create ns staging.
  2. Экспортируем ConfigMap и Secret, чистим, применяем в новый namespace. Без них Deployment уйдёт в Pending или CrashLoop.
  3. Экспортируем Deployment, чистим, применяем в новый namespace.
  4. Ждём готовности: kubectl rollout status deploy/web -n staging --timeout=120s.
  5. Применяем Service в новом namespace, без clusterIP.
  6. Проверяем, что Pod-ы прошли readiness и Service отдаёт адреса.
  7. Только теперь удаляем объекты в старом namespace.

Если наружу трафик идёт через Ingress, на шаге 5 создаётся новый Ingress в staging и переключается маршрут. Ingress ссылается на Service только внутри своего namespace, так что «указать чужой Service» не выйдет: объект Ingress должен переехать целиком. До момента переключения запросы продолжает обслуживать старый Deployment.

Отдельная точка внимания - DNS-имя. Внутри кластера Service доступен как web.dev.svc.cluster.local. После переезда имя становится web.staging.svc.cluster.local. Если другие приложения ходят по короткому web из своего namespace или по полному имени со старым namespace, их конфигурацию тоже надо обновить, иначе после удаления старого Service они начнут получать ошибки резолва.

Проверка перед удалением

После kubectl apply в новый namespace нужно убедиться, что Pod-ы реально готовы, а не просто запустились:

kubectl rollout status deploy/web -n staging --timeout=120s
kubectl get pods -n staging -l app=web
kubectl get endpoints web -n staging

Объект v1 Endpoints deprecated начиная с 1.33, и kubectl печатает про это предупреждение. Тот же список адресов через актуальный объект:

kubectl get endpointslice -n staging -l kubernetes.io/service-name=web

Смысл проверки: Service направляет трафик только на готовые Pod-ы. Пустой список адресов означает, что readiness ещё не прошёл, и запросы получают 503. Удалять старое можно после того, как в endpoints появились реальные IP.

Где живут зависимости

Что часто ломается

Полная последовательность командами

# Что вообще лежит в исходном namespace
kubectl get all -n dev
kubectl get cm,secret,ingress -n dev

# Целевой namespace
kubectl create ns staging

# Экспорт с чисткой и подменой namespace
kubectl get cm web-config -n dev -o yaml \
  | yq 'del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, .status) | .metadata.namespace = "staging"' \
  | kubectl apply -f -

kubectl get deploy web -n dev -o yaml \
  | yq 'del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, .metadata.generation, .status) | .metadata.namespace = "staging"' \
  | kubectl apply -f -

# Ждём готовности
kubectl rollout status deploy/web -n staging --timeout=120s

# Service без clusterIP
kubectl get svc web -n dev -o yaml \
  | yq 'del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, .spec.clusterIP, .spec.clusterIPs, .status) | .metadata.namespace = "staging"' \
  | kubectl apply -f -

# Проверяем, что Service нашёл готовые Pod-ы
kubectl get endpointslice -n staging -l kubernetes.io/service-name=web

# Только после этого убираем старое
kubectl delete deploy web cm web-config svc web -n dev

Коротко

Прошли материал до конца? Отметьте урок пройденным.