Imperative vs Declarative: два паттерна работы с API
С точки зрения API сервер не делает разницы: что kubectl run nginx --image=nginx, что kubectl apply -f pod.yaml — оба превращаются в HTTP-запрос с одинаковым телом. Но поведение клиента перед и после запроса разное, и это формирует два паттерна. Imperative — это «сделай прямо сейчас вот это» (analogue mkdir, mv, rm). Declarative — это «вот желаемое состояние, приведи к нему» (analogue git apply patch). На CKAD-экзамене вы будете комбинировать оба: imperative с --dry-run=client -o yaml для быстрой генерации YAML-заготовки, и apply для финального применения. Хорошее владение этой комбинацией экономит 10-15 минут на экзамене.
docker run: базовая команда и её флаги
Imperative: прямое действие, без YAML
Imperative-команды описывают действие, а не состояние. Список основных:
# Pods
kubectl run nginx --image=nginx:1.27
kubectl run probe --image=busybox -- sleep 3600
# Deployments
kubectl create deployment web --image=nginx --replicas=3
kubectl scale deployment web --replicas=5
kubectl set image deployment/web nginx=nginx:1.28
kubectl rollout undo deployment/web
# Services
kubectl expose deployment web --port=80 --target-port=80 --type=ClusterIP
# Labels, annotations
kubectl label pod nginx env=prod
kubectl annotate pod nginx description="frontend"
# ConfigMaps, Secrets
kubectl create configmap app-config --from-literal=KEY=value
kubectl create secret generic db-creds --from-literal=password=s3cret
Каждая такая команда — это HTTP POST/PATCH на apiserver. kubectl сам формирует JSON-тело, валидирует против OpenAPI schema (опционально), отправляет. Если ресурс уже существует — create упадёт с AlreadyExists, run — тоже (он внутри делает create).
Преимущества imperative:
- Быстро для одиночных действий.
- Не нужен файл — всё в одной строке.
- Идеально для отладки, экспериментов, ad-hoc операций.
Минусы:
- Нет source of truth — после команды состояние есть в кластере, в git его нет.
- Не повторяется: запустить «то же самое» дважды — ошибка.
- Drift с реальной конфигурацией: если другой человек поправил руками — вы не узнаете.
В production imperative используют только для read-операций (get, describe, logs) и аварийных правок (kubectl edit, kubectl patch). Всё остальное — declarative из git.
Declarative: opisanie желаемого состояния
Declarative — это kubectl apply -f file.yaml (или -k dir/ для kustomize). Вы пишете YAML с желаемым состоянием, apiserver сохраняет его в etcd, дальше controllers приводят реальность к описанию.
# nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
containers:
- name: nginx
image: nginx:1.27
kubectl apply -f nginx.yaml
# deployment.apps/web created (первый раз)
# deployment.apps/web configured (второй раз, если что-то изменилось)
# deployment.apps/web unchanged (если ничего не поменялось)
apply — это upsert: создаёт, если не существует, обновляет, если существует. Поэтому он идемпотентен — вы можете запускать его сколько угодно раз без побочных эффектов.
3-way merge: суть apply
Когда вы делаете apply, kubectl сравнивает три версии:
- desired — ваш текущий YAML.
- last-applied — то, что было в YAML на прошлый apply (хранится в annotation
kubectl.kubernetes.io/last-applied-configuration). - actual — то, что сейчас лежит в etcd.
По этим трём строится патч:
- Поле есть в desired, нет в last-applied → добавить.
- Поле было в last-applied, нет в desired → удалить (вы убрали поле из YAML).
- Поле есть и там, и там, но значение изменилось → обновить.
- Поле есть только в actual (не в last-applied и не в desired) → не трогать (это поле controller-а или webhook-а, не ваше).
Последний пункт — ключевой. HPA может менять spec.replicas в Deployment, и apply не сбросит его обратно, потому что в last-applied этого поля не было. Это то, что делает декларативный подход дружелюбным к controllers.
Можно посмотреть, что лежит в last-applied: kubectl get deploy web -o jsonpath='{.metadata.annotations.kubectl\.kubernetes\.io/last-applied-configuration}' | jq. Это полный YAML, который вы apply-нули в прошлый раз, в JSON-форме.
kubectl create vs kubectl apply
create -f | apply -f | |
|---|---|---|
| Объект не существует | Создаёт | Создаёт |
| Объект существует | Ошибка AlreadyExists | Обновляет через 3-way merge |
| Идемпотентен | Нет | Да |
| Хранит last-applied | Нет | Да |
| Подходит для CI/CD | Нет (одна попытка) | Да (можно перезапускать) |
| Поведение с replace | Атомарная замена | 3-way merge |
Опасный сценарий: вы сделали kubectl create -f deploy.yaml, потом — kubectl apply -f deploy.yaml. У объекта нет annotation last-applied, поэтому apply ведёт себя странно: считает, что last-applied == пустой объект, и при первой возможности захочет «удалить» все поля, которые добавил create.
# С v1.18+ есть исправление: apply --save-config умеет восстановить annotation
kubectl apply --save-config -f deploy.yaml
# Эквивалент: kubectl apply -f deploy.yaml + добавить annotation last-applied = текущий YAML
Правило: либо весь жизненный цикл объекта через apply, либо весь через create+replace. Не мешать. Сейчас в новых командах kubectl create остаётся в основном для генерации манифестов (с --dry-run=client), а боевое применение — через apply или server-side apply.
—dry-run: валидация и генерация YAML
--dry-run имеет три значения:
# 1. none (default) — реально применить
kubectl create deployment web --image=nginx
# 2. client — НЕ слать запрос, только показать что бы отправили
kubectl create deployment web --image=nginx --dry-run=client -o yaml
# 3. server — отправить запрос с заголовком dry-run, apiserver валидирует
# и применит admission webhooks, но НЕ сохранит в etcd
kubectl create deployment web --image=nginx --dry-run=server -o yaml
Разница между client и server:
- client: kubectl формирует YAML локально по собственным правилам. Не видит admission webhooks, не видит defaults apiserver (resource limits из LimitRange, securityContext из PodSecurity), не валидирует против OpenAPI schema cluster.
- server: запрос реально отправляется с заголовком
?dryRun=All. Apiserver прогоняет authentication, authorization, admission (webhooks!), validation, defaulting — и возвращает финальный объект. Только запись в etcd не происходит.
server точнее, но требует rights и доступности cluster. client — быстрее и работает offline.
# CKAD-style: получить YAML заготовку быстро
kubectl run nginx --image=nginx --dry-run=client -o yaml > pod.yaml
kubectl create deployment web --image=nginx --replicas=3 --dry-run=client -o yaml > web.yaml
kubectl create service clusterip web --tcp=80:80 --dry-run=client -o yaml > svc.yaml
kubectl create configmap app --from-literal=K=V --dry-run=client -o yaml > cm.yaml
kubectl create secret generic db --from-literal=PASS=x --dry-run=client -o yaml > sec.yaml
# Дальше — vim, доработать, apply
Это и есть CKAD-стратегия скорости: не пишите YAML с нуля. Сгенерируйте заготовку imperative-командой, доточите её (volumeMounts, env, resources), apply.
kubectl replace -f: полная замена
replace — это PUT, а не PATCH. Он отправляет полный объект, и apiserver заменяет всё, что есть в etcd, новой версией.
kubectl replace -f deploy.yaml
# Эквивалент: GET текущий, заменить в YAML resourceVersion, PUT.
Опасность: если controller (HPA, etcd-defaulter) добавил поля, которых нет в вашем YAML, — они исчезнут. После replace HPA увидит, что replicas сброшен на исходный, и снова поднимет — это race condition, который проявляется как «pod-ы прыгают между 3 и 12».
replace -f --force ещё агрессивнее: удаляет объект и создаёт заново. Все running Pod-ы перезапустятся, IP сменятся, PVC отвяжутся-привяжутся — это полная рекреация. Используйте только когда apply не справляется (например, immutable field изменился — selector в Deployment).
| Сценарий | Команда |
|---|---|
| Регулярные обновления | apply |
| Создать новый объект | apply или create |
| Полностью перезаписать (snapshot-style) | replace |
| Принудительно пересоздать (immutable field) | replace --force или delete + apply |
| Точечная правка одного поля | patch или edit |
CKAD-стратегия: imperative + dry-run + apply
На экзамене 2 часа, 15-20 задач, средняя задача — создать или модифицировать ресурс. Писать YAML с нуля в каждой — провал по времени. Стратегия чемпионов:
# 1. Получить заготовку imperative-командой + dry-run
kubectl run web --image=nginx:1.27 \
--port=80 \
--labels="app=web,tier=frontend" \
--dry-run=client -o yaml > pod.yaml
# 2. Открыть в vim, доточить spec (volumes, env, resources, probes)
vim pod.yaml
# 3. Применить
kubectl apply -f pod.yaml
# 4. Проверить
kubectl get pod web -o wide
kubectl describe pod web
Алиасы, которые нужно настроить в начале экзамена (есть отдельный урок):
alias k=kubectl
export do='--dry-run=client -o yaml' # quick YAML
export now='--grace-period=0 --force' # quick delete
# Тогда CKAD-комбо выглядит так:
k run web --image=nginx $do > pod.yaml
k delete pod web $now
Не запоминайте все флаги kubectl run / create deployment наизусть — на экзамене разрешена документация. Запомните паттерн: «нужен YAML — generate с dry-run, доточи, apply». Этого достаточно для 80% задач.
Server-Side Apply: новая модель
С v1.22 GA, в v1.35 — основной способ apply для controllers и CI/CD. SSA добавляет kubectl apply --server-side:
kubectl apply --server-side -f deploy.yaml --field-manager=ci-bot
Идея: вместо annotation last-applied, apiserver сам отслеживает, какой клиент владеет каким полем (managedFields в metadata). Если CI-bot владеет spec.replicas, а HPA — spec.replicas, при конфликте apiserver вернёт ошибку 409 с подробностями. Это лучше, чем тихий race condition с client-side apply.
На CKAD пока спрашивают классический apply (client-side). SSA полезен в реальной работе, особенно когда несколько controllers/CI редактируют один объект.
Что дальше
В следующем уроке — контексты, namespaces и переключение между ними. Это критично для CKAD: каждая задача даёт вам kubectl config use-context <ctx> — если забыли выполнить, отвечаете в чужом кластере и получаете 0 баллов за задачу.