10 типовых ошибок на CKAD — и как их избежать
Каждая из этих ошибок стоила кому-то сертификата. Я собрал их из post-mortem от провалившихся, retake-кандидатов и собственного опыта. Прочитайте — и не повторяйте.
Disk emergency: чеклист при критической проблеме
MISTAKE 1: Забыли переключить context
Каждая задача на экзамене начинается со строки вроде:
kubectl config use-context k8s
Это первое что нужно выполнить перед любым действием в задаче. Если не переключили:
- Создаёте Pod в кластере
mk8sвместоk8s. - Проверяющий скрипт ищет Pod в
k8s— не находит. - Результат — 0 баллов за задачу.
Также часто требуется namespace switch:
kubectl config set-context --current --namespace=target-ns
Это самая частая ошибка. Caждая задача — copy-paste context command в первую очередь. Перед submit задачи — kubectl config current-context для проверки.
MISTAKE 2: Wrong probe handler syntax
Probes имеют 3 типа handlers, у каждого свой синтаксис:
# exec — это ARRAY команды, не string
livenessProbe:
exec:
command:
- sh
- -c
- "cat /tmp/healthy"
# НЕПРАВИЛЬНО: command: "sh -c cat /tmp/healthy"
# httpGet — path обычно /health или /healthz
readinessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: X-Custom
value: value
# tcpSocket
startupProbe:
tcpSocket:
port: 8080
exec.command — массив строк, не строка с пробелами. command: "sh -c cmd" создаст Pod, но probe будет fail-иться (kubectl передаёт строку как один argument). Правильно: command: ["sh", "-c", "cmd"].
MISTAKE 3: SecurityContext поля не на том уровне
В SecurityContext есть Pod-level и Container-level. Не все поля доступны на обоих уровнях:
apiVersion: v1
kind: Pod
spec:
# Pod-level: runAsUser, runAsGroup, fsGroup, supplementalGroups, seccompProfile
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: app
# Container-level ONLY: capabilities, allowPrivilegeEscalation,
# readOnlyRootFilesystem, privileged
securityContext:
capabilities:
add: ["NET_ADMIN"]
drop: ["ALL"]
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
privileged: false
runAsUser: 2000 # OVERRIDE Pod-level
Ключевые моменты:
fsGroup— только на Pod-level. Применяется ко всем volumes Pod-а.capabilities— только на Container-level. Это привилегия процесса, не Pod.- Container может override
runAsUser/runAsGroupот Pod-level.
MISTAKE 4: NetworkPolicy без явного policyTypes
# НЕПРАВИЛЬНО: без policyTypes
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-external
spec:
podSelector:
matchLabels:
app: web
egress:
- to:
- podSelector:
matchLabels:
app: db
Что произойдёт: К8s применит default policyTypes: [Ingress]. Egress rules игнорируются. Pod может выходить куда угодно — задача не выполнена.
Правильно — явно указать policyTypes:
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
egress:
- to:
- podSelector:
matchLabels:
app: db
Всегда явно указывайте policyTypes даже если кажется очевидным. Default behavior зависит от того, какие rules вы указали — это запутанно и приводит к ошибкам.
MISTAKE 5: ConfigMap mount с subPath не auto-updates
ConfigMap mount имеет 2 варианта:
# Без subPath — kubelet обновляет автоматически (через ~1 минуту)
containers:
- name: app
volumeMounts:
- name: config
mountPath: /etc/config
volumes:
- name: config
configMap:
name: app-config
# С subPath — FIXED при mount, БЕЗ auto-update
containers:
- name: app
volumeMounts:
- name: config
mountPath: /etc/app.conf
subPath: app.conf
volumes:
- name: config
configMap:
name: app-config
Если задача “ConfigMap должен auto-обновляться без restart Pod” — НЕ используйте subPath. Если задача “конкретный файл из ConfigMap в существующий конфиг dir” — subPath неизбежен, но потеряете auto-update.
MISTAKE 6: Imperative create + apply конфликт
Конфликт между imperative и declarative подходом:
# Step 1: imperative
kubectl create -f pod.yaml
# Step 2: правите pod.yaml, apply
kubectl apply -f pod.yaml # CAN FAIL
Что произошло: при kubectl create -f объект создан БЕЗ kubectl.kubernetes.io/last-applied-configuration annotation. kubectl apply использует эту аннотацию для 3-way merge. Без неё — apply путается, либо warning, либо конфликт.
Best practice: всегда использовать kubectl apply -f от начала. Не миксовать.
# Правильно
kubectl apply -f pod.yaml # creates with annotation
# edit pod.yaml
kubectl apply -f pod.yaml # 3-way merge works
MISTAKE 7: PVC с RWO и Deployment с replicas > 1
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 2 # ПРОБЛЕМА
template:
spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: my-pvc # RWO PVC
Где PVC:
apiVersion: v1
kind: PersistentVolumeClaim
spec:
accessModes:
- ReadWriteOnce # RWO — только один node может mount
Что произойдёт:
- Первый Pod успешно создан и mount-ит PVC на ноде A.
- Второй Pod планируется на ноду B (или A — зависит от scheduler).
- Если на ноду B: kubelet пытается mount PVC, fails —
MultiAttachError. Pod stuck Pending. - Если на ту же ноду A: некоторые CSI драйверы поддерживают, но не все.
Решения:
replicas: 1для Deployment с RWO PVC.- Использовать
ReadWriteMany(NFS, CephFS, Azure Files, EFS). - Использовать StatefulSet с
volumeClaimTemplates— каждый Pod получает свой PVC.
MISTAKE 8: kubectl exec без -it для interactive
# НЕПРАВИЛЬНО для interactive shell
kubectl exec my-pod -- sh
# Запускает sh, но без stdin — мгновенно завершается
# ПРАВИЛЬНО
kubectl exec -it my-pod -- sh
# -i = interactive (stdin attached)
# -t = tty allocated
Также частая ошибка: kubectl exec -it my-pod -c mycontainer -- bash — забыли -c для multi-container Pod, kubectl откажется без default container.
MISTAKE 9: Probes без startupProbe для медленных приложений
# ПРОБЛЕМА: liveness падает на 30s default
livenessProbe:
httpGet:
path: /health
port: 8080
# initialDelaySeconds: 30 по умолчанию
# periodSeconds: 10
# failureThreshold: 3
# = 30 + 10*3 = 60 сек до считается dead
Если приложение стартует 90 секунд (Spring Boot, JVM warmup) — liveness fail-ится → kubelet restart → loop.
Решение — startupProbe:
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 30
periodSeconds: 10
# = до 5 минут на startup; liveness не запускается пока startup passes
livenessProbe:
httpGet:
path: /health
port: 8080
Правило: если приложение стартует > 30 секунд — добавьте startupProbe. Это disable-ит liveness/readiness до тех пор пока startup не passed. Старт защищён, liveness защищает running.
MISTAKE 10: Не использовать kubectl explain
Самая большая трата времени на экзамене — переключение в браузер и поиск по kubernetes.io. Когда забыли точное имя поля YAML, есть быстрее способ:
# Забыли как настроить capabilities?
k explain pod.spec.containers.securityContext.capabilities
# Все поля resource limits?
k explain pod.spec.containers.resources --recursive
# Структура NetworkPolicy?
k explain networkpolicy.spec --recursive
Это:
- Мгновенно — без переключения вкладок.
- Точно — выдаёт ровно те поля что доступны для вашей kubectl/cluster version.
- С описанием —
--recursiveопускает описания (только schema), без--recursive— каждое поле с docs string.
Тренируйтесь использовать kubectl explain дома. Через 2 недели у вас будет рефлекс — “забыл поле → explain”. Это экономит 30 секунд каждый раз. За экзамен — 5+ минут.
Bonus: 5 микро-ошибок
- YAML с табами вместо пробелов. Vim без
set et— катастрофа. Setexpandtabобязательно. kubectl applyбез-f.kubectl apply pod.yaml— error. Нужноkubectl apply -f pod.yaml.- Resource без namespace в задаче где требуется. Создали в default — задача провалена.
- Не проверили статус. Создали ресурс, перешли к следующей задаче — а он Pending.
kubectl getдля подтверждения. - Запутались в kind.
podvsPod(kind case sensitive в YAML),podsvspodв kubectl (оба работают как resource).
Killer-моменты ошибок
- Context switch обязателен для каждой задачи.
exec.command— массив, не строка.fsGroupтолько на Pod-level,capabilitiesтолько на Container-level.policyTypesявно в NetworkPolicy.- subPath blockирует auto-update ConfigMap.
- Не миксовать
create -fиapply -f. - RWO PVC + replicas > 1 = Pending.
exec -itдля interactive shell.- startupProbe для slow apps.
kubectl explainбыстрее docs.