Съдържание на курса
Урок 2 — Pods и Deployments
От Dorian Chávez · основател и главен изпълнителен директор на Hábil ·
Понеделник, 8:50 сутринта. В 9:30 екипът има демонстрация на новия клъстер, а твоята част е най-простата: revisor да се появи работещ, с три копия. Образът е на лаптопа ти от петък, изграден и изпробван с docker run. Пишеш манифеста, прилагаш го и kubectl отговаря deployment.apps/revisor created. Отдъхваш си. Искаш списъка с обектите Pod и трите казват едно и също: ImagePullBackOff. Някой от екипа, без да вдигне поглед, ти казва: „изтрий го и го приложи пак“. Правиш го. Същото състояние. Правиш го още веднъж. Часът е 9:05, а образът си е там, в твоя docker image ls, на два сантиметра от клъстера, който твърди, че не може да го намери.
Изтриването и повторното прилагане нямаше да помогне с нищо в тази ситуация, защото клъстерът не беше объркан: казваше ти точно какво му липсва. Този урок те учи да го чуваш, да му даваш каквото иска и да разбираш кои обекти се раждат, когато приложиш един Deployment. Така, когато нещо се провали в 9:05, ще знаеш къде да погледнеш.
Време: 2 × 45 мин
Какво изграждаш: revisor, работещ в клъстера на курса, с три реплики, които се раждат отново, когато умрат, със защита (hardening) още от първия манифест
Какво научаваш: защо твоят Docker и възлите на клъстера не споделят образи и как kind load docker-image ги пренася; грешката ErrImagePull, предизвикана нарочно, и как се разчита; какво е Pod, ReplicaSet и Deployment и кой на кого е собственик; етикети и селектори; какво е rollout и как изглежда историята му; и securityContext, който носи всеки контейнер в този курс
Откъде идваш. От Урок 1: имаш kubectl и kind, инсталирани с проверена контролна сума, настроен лимит на inotify и умееш да създаваш, разглеждаш и изтриваш клъстера на курса от три възела. От Урок 0 носиш картата: kube-apiserver пази онова, което декларираш, контролерите го преследват, kube-scheduler избира възел, а kubelet стартира контейнери. А от курса по Docker носиш завършения образ на revisor, curso-docker-revisor:1.6.0.
Какво не прави този урок. revisor остава да работи, но все още никой не може да говори с него: няма стабилно име, по което да бъде намерен, нито начин да се стигне до него от твоята машина. Това е Урок 3, с обектите Service и DNS на клъстера. Още не се конфигурира и отвън: списъкът с услуги, които проверява, е записан в манифеста, а Урок 4 го изнася в ConfigMap. И клъстерът още не знае дали revisor е изправен или само жив: probes идват в Урок 5.
За изпълненията. Всички изходи в този урок са получени на 21 октомври 2026 г. с kind v0.33.0, kubectl v1.37.1 и образа на възел kindest/node:v1.37.0, фиксиран чрез отпечатъка си, в клъстера от три възела от Урок 1, който тук се нарича curso-k8s-l2. Машината беше Mac с процесор ARM и Docker Engine 29.2.1 във виртуална машина с Linux (Colima), а не Linux Mint, който е референтната система на курса. Какво се променя в твоя Mint: архитектурата (amd64 вместо arm64), IP адресите на обектите Pod, случайните наставки в имената на обектите Pod и ReplicaSet, възрастите и на кой възел попада всяка реплика. Командите са същите. Цялата обиколка е в programas/02-pods-deployments/: correr.sh създава клъстера без образа на revisor, минава през урока по ред, изпълнява Упражнения 1 до 3 и изтрива клъстера; отне 2 минути и 25 секунди, измерени с часовника на терминала; скриптът сам записва колко е отнела всяка дълга стъпка: създаването на клъстера и изчакването да е готов — 34 секунди; зареждането на образа — 2; изтриването му — 1. Изходът му е correr.salida.txt. Там частта за навика за сигурност се появява преди тази за rollout (2.8), защото скриптът ги изпълнява в този ред.
След урока ще можеш да
- Проверяваш с
crictl imagesдали даден образ е вътре във възел на kind и да го зареждаш сkind load docker-image. - Предизвикваш
ErrImagePull, разчиташ го сkubectl describeиkubectl get eventsи казваш какво означава всеки ред. - Пишеш по памет скелета на един Deployment (
replicas,selector,template) и обясняваш защо селекторът и етикетите на шаблона трябва да съвпадат. - Проследяваш с
ownerReferencesверигата Deployment → ReplicaSet → Pod и предвиждаш какво става при изтриване на всяка брънка. - Филтрираш обекти със селектори по етикет (
-l app=revisor,-l 'app in (…)') и обясняваш какво е етикетътpod-template-hash. - Разчиташ историята на един Deployment с
kubectl rollout historyи предизвикваш подредено рестартиране сkubectl rollout restart. - Обосноваваш всеки ред от
securityContextнаrevisorи разпознаваш грешкатаCreateContainerConfigError, която предизвиква противоречие в него.
Защо, преди как
В Урок 0 видя един Deployment, който поддържаше три копия на контейнер, който не правеше нищо. Това беше чистата идея: декларираш „искам три“ и клъстерът преследва това число. Сега е ред на версията с истинска програма и се появяват три въпроса, които играчката криеше.
Първият: откъде клъстерът взима образа? Този от Урок 0 вече беше вътре във възела, защото самият Kubernetes го използва. Образът на revisor го изгради ти и той живее в твоя Docker. Един възел на kind изглежда, че е „на твоята машина“, но отвътре има собствено хранилище за образи, което не е това на твоя Docker. Това е точната причина за сцената от понеделник и е първото, което ще видиш да се проваля.
Вторият: какви обекти съществуват и кой за кого се грижи? Един Deployment не изпълнява контейнери. Той създава ReplicaSet, който създава обекти Pod, а те са онези, които работят. Ако не виждаш тази верига, „изтриване на Pod“ и „изтриване на Deployment“ изглеждат като една и съща операция, а не са: едната се поправя сама за секунда, а другата спира услугата.
Третият: какво може да направи контейнерът, ако някой го атакува? С docker run се научи да му отнемаш привилегии с --user, --read-only и --cap-drop ALL. В Kubernetes същите решения се записват в манифеста, в блок, наречен securityContext, и този курс ги слага още от първия Deployment. Не чакат Урок 7. Сигурността, която се добавя накрая, остава в списък с неща за вършене; онази, която се ражда с манифеста, пътува с него до всеки клъстер.
Урокът има осем части с понятия и след тях обичайните раздели:
- Две хранилища за образи (2.1): твоят Docker и това на всеки възел.
- Манифестът на
revisor, поле по поле (2.2). - Прилагане и ненамерен образ (2.3), нарочната грешка.
kind load docker-image(2.4) и клъстерът, който се възстановява сам.- Веригата на собствениците (2.5): Deployment, ReplicaSet и Pod.
- Етикети и селектори (2.6).
- Самостоятелен Pod срещу Pod на Deployment (2.7).
- Rollout (2.8): ревизии, история и рестартиране.
Това е целта. В края на 2.4 revisor работи с три реплики в клъстер, който половин минута по-рано нямаше неговия образ:
$ kubectl get deployment,replicaset -l app=revisor
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/revisor 3/3 3 3 32s
NAME DESIRED CURRENT READY AGE
replicaset.apps/revisor-697fb84f97 3 3 3 32s
Ако нямаш образа от курса по Docker
Този урок използва образа, който предава Урок 10 от курса по Docker, curso-docker-revisor:1.6.0. Преди да продължиш, провери, че го имаш:
$ docker image ls curso-docker-revisor:1.6.0 --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.Size}}'
curso-docker-revisor:1.6.0 bd1b2e7d91b8 16.2MB
Ако го нямаш, изгради го от хранилището на онзи курс, с Dockerfile от неговия Урок 10 върху папката programas/revisor и с --build-arg VERSION=1.6.0, като му дадеш точно тага curso-docker-revisor:1.6.0. Ако използваш друг образ на твоя собствена услуга, ето какво изисква курсът от него, и си струва да го прочетеш, защото всяка точка обяснява едно решение в манифеста:
- Слуша HTTP на висок порт, 8080, и отговаря на
GET /salud, без да пита никой друг. - Работи с цифров потребител, който не е
root: 65532, потребителятnonrootот образите distroless. Манифестът го изисква сrunAsNonRoot: true. - Работи с файлова система само за четене, с изключение на една папка за данни (
/datos), която манифестът монтира отделно. - Носи собствена проверка на състоянието (health check) като подкоманда (
/revisor healthcheck), защото образът няма shell, нитоcurl. - Има фиксиран таг, никога
:latest. Раздел 2.2.3 обяснява защо това има значение в kind. - Конфигурира се чрез променливи на средата.
revisorизискваREVISOR_SERVICIOSсъс списъка от услуги, които проверява, и разбираREVISOR_PUERTO,REVISOR_REPORTEиREVISOR_TOKEN_ARCHIVO. Променлива, която не чете, се пренебрегва безшумно, така че не измисляй други.
Понятията
2.1 Две хранилища за образи
Този урок използва собствен клъстер, curso-k8s-l2, с конфигурацията от три възела от Урок 1, току-що създаден и без да му се зарежда нищо. Създай го така, от корена на хранилището на курса, със същото внимание, което научи в Урок 1:
# Un kubeconfig temporal y propio: tu ~/.kube/config ni se lee ni se toca.
export KUBECONFIG="$(mktemp -d /tmp/curso-k8s-l2.XXXX)/kubeconfig"
# curso-k8s-l2 NO debe aparecer aquí. Si aparece y no es tuyo, detente.
kind get clusters
kind create cluster --name curso-k8s-l2 --config programas/02-pods-deployments/kind-curso.yaml
# Debe decir kind-curso-k8s-l2. Si dice otra cosa, no sigas.
kubectl config current-context
export важи само в този терминал. Ако отвориш друг, повтори export със същия път, иначе kubectl ще говори с друг клъстер. Изходите в този раздел са от изпълнението на correr.sh, който прави точно това:
$ kubectl config current-context
kind-curso-k8s-l2
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
curso-k8s-l2-control-plane Ready control-plane 26s v1.37.0
curso-k8s-l2-worker Ready <none> 16s v1.37.0
curso-k8s-l2-worker2 Ready <none> 16s v1.37.0
Първата команда не е украса. Това е навикът от Урок 1: преди да промениш нещо, питаш с кой клъстер говориш. В този урок ще създаваш и изтриваш обекти и kind-curso-k8s-l2 е единственият отговор, който ти позволява да продължиш. Повтаряй го преди всеки apply, delete, label или rollout restart в урока, а не само тук. Скриптът го прави сам: преди всяко kubectl сравнява контекста и спира, ако не е kind-curso-k8s-l2.
Сега въпросът от сцената. Твоят Docker има образа; има ли го възелът? Спомни си от Урок 0, че възелът на kind е контейнер, който носи вътре собствен containerd, а този containerd има собствено хранилище за образи. За да попиташ него, влизаш във възела и използваш crictl, инструмента от командния ред, който говори с runtime-а на възела. Ръководството за начало на kind го препоръчва точно за това (kind, „Quick Start“, раздел „Loading an Image Into Your Cluster“):
$ docker exec curso-k8s-l2-worker crictl images | grep -c curso-docker-revisor
0
[código de salida: 1]
Нула. (Изходният код 1 е на grep, който така съобщава, че не е намерил нищо; не е повреда на възела.) Образът е на твоята машина и не е във възела. Това са две различни хранилища, макар и двете да живеят на един и същ компютър, и нищо не ги синхронизира.
В истински клъстер това се решава с регистър: качваш образа на сървър за образи (Docker Hub, регистъра на твоята фирма или този на облачния ти доставчик) и всеки възел го изтегля оттам. В kind, който е лаборатория на твоята машина, има пряк път: да копираш образа направо от твоя Docker във всеки възел. Този пряк път е kind load docker-image и идва в 2.4. Първо ще видиш какво става без него, защото съобщението за грешка е от онези, които ще срещаш най-често в живота си с Kubernetes.
2.2 Манифестът на revisor, поле по поле
Това е манифестът, който урокът изгражда. Той е програма: прилага се сам и живее в programas/02-pods-deployments/revisor-deployment.yaml.
# revisor-deployment.yaml — Lección 2: el revisor con tres réplicas que renacen.
apiVersion: apps/v1
kind: Deployment
metadata:
name: revisor
labels:
app: revisor
spec:
replicas: 3
selector:
matchLabels:
app: revisor
template:
metadata:
labels:
app: revisor
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: revisor
image: curso-docker-revisor:1.6.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
env:
- name: REVISOR_SERVICIOS
value: "prueba http://prueba:9000/ 2s"
# La imagen de Docker trae REVISOR_REPORTE=/datos/reporte.json: con el
# sistema de archivos de solo lectura, /datos necesita un volumen. emptyDir
# vive y muere con el Pod (la Lección 8 lo contrasta con un PVC).
volumeMounts:
- name: datos
mountPath: /datos
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumes:
- name: datos
emptyDir:
sizeLimit: 10Mi
Изглежда много. Чете се на слоеве, отвън навътре, и всеки слой отговаря на един въпрос.
2.2.1 Какво е и как се казва
Първите три ключа ги носи всеки обект в Kubernetes. apiVersion: apps/v1 казва в коя група на API живее типът: обектите Deployment живеят в групата apps, версия v1, която е стабилна. kind: Deployment казва типа. А metadata носи името (revisor) и етикетите на самия Deployment (app: revisor). Името е уникално в своето пространство от имена; етикетите, както ще видиш в 2.6, не са, и точно в това е ценността им.
2.2.2 Колко, кои и с какъв шаблон
spec е желаното състояние и в един Deployment има три части, които вървят заедно:
replicas: 3: колко копия искаш. Това е числото, което клъстерът преследва.selector: как Deployment разпознава своите обекти Pod. Тук казва: „моите са онези, които носят етикетаapp: revisor“.template: шаблонът на всеки Pod. Той носи собствени етикети (app: revisor) и собственspec, който е този на един Pod: контейнери, томове и сигурност.
Селекторът и етикетите на шаблона трябва да съвпадат. Ако шаблонът произвеждаше обекти Pod с app: otro, а селекторът търсеше app: revisor, Deployment щеше да създава обекти Pod, които никога нямаше да разпознае като свои, и щеше да продължава да създава още, без край. Затова kube-apiserver отхвърля Deployment, чийто селектор не съвпада с етикетите на шаблона му, преди да го запише. Документацията за Deployments добавя едно предупреждение, което е добре да имаш предвид още отсега: Kubernetes не пречи на два контролера да имат селектори, които се припокриват, и ако ги имат, те „могат да влязат в конфликт и да се държат неочаквано“. Упражнение 2 ти показва една версия на този проблем.
2.2.3 Контейнерът
Вътре в шаблона containers е списък; в този курс той винаги съдържа един-единствен контейнер. Неговите полета:
name: revisor: името на контейнера вътре в обекта Pod. Ще го виждаш в събитията катоspec.containers{revisor}.image: curso-docker-revisor:1.6.0: образът, с фиксирания си таг.imagePullPolicy: IfNotPresent: кога да се изтегля образът.IfNotPresentозначава „изтегли го само ако вече не е във възела“. Документацията за образите определя същоAlways(да се пита регистърът всеки път, когато стартира контейнер; runtime-ът изтегля само слоевете, които му липсват, но ако регистърът не отговаря или няма образа, се проваля, макар възелът вече да го има) иNever(никога да не се опитва изтегляне). Ако пропуснеш полето, Kubernetes решава според тага: с таг, който не е:latest, слагаIfNotPresent, а с:latestили без таг слагаAlways. Това е, което прави:latestкапан в kind: сAlwaysвъзелът би се опитал да отиде до Docker Hub, макар вече да си му заредил образа, и би се провалил. Ръководството на kind предупреждава за това: с:latestили без таг политиката по подразбиране еAlways, и съветва да не се използва:latestили да се напишеimagePullPolicy: IfNotPresent(илиNever) (kind, „Quick Start“). Манифестът пишеIfNotPresent, макар това да е стойността по подразбиране, за да не се налага онзи, който го чете, да знае правилото.ports: декларира, че контейнерът слуша на 8080, и му дава име,http. Декларирането не публикува нищо и не отваря никакъв порт навън: това е информация за онзи, който чете манифеста, и за обектите, които го използват по-късно. В Урок 3 обектът Service ще сочи порта по името му,http, а не по номера му.env: променливите на средата. Тук има само една,REVISOR_SERVICIOS, онази, коятоrevisorизисква, с услуга на имеpruebaнаhttp://prueba:9000/и времево ограничение от 2 секунди. Тази услуга още не съществува в клъстера и това няма значение:revisorстартира въпреки това и ще я отчете със състояниеFALLA, когато някой му поиска отчета. В Урок 4 този списък излиза от манифеста и отива вConfigMap.
2.2.4 Томът datos
Накрая има един том, datos, от тип emptyDir, монтиран в /datos. Той е там по конкретна причина. Образът от курса по Docker определя REVISOR_REPORTE=/datos/reporte.json: всеки път, когато някой му поиска отчета, revisor го записва в този файл. Но securityContext на контейнера (2.2.5) прави цялата му файлова система само за четене. Без том revisor би стартирал, без да се оплаче, и би се провалил по-късно, първия път, когато се опита да запише отчет: в кода му този случай отговаря с грешка 500 и съобщението no pude guardar el reporte.
emptyDir е най-простият начин да се даде място за писане. Документацията за томовете го описва така: създава се, когато обектът Pod бъде разпределен към възел, започва празен и „когато един Pod бъде премахнат от възел по каквато и да е причина, данните в emptyDir се изтриват завинаги“. Ако контейнерът падне и се рестартира, данните оцеляват, защото обектът Pod остава на възела. sizeLimit: 10Mi му слага таван: един отчет тежи няколко килобайта, а таванът не позволява една грешка да напълни диска на възела. Урок 8 съпоставя този emptyDir с постоянен том и измерва какво се губи при всеки от тях.
2.2.5 Двата securityContext
Има два блока securityContext: един на ниво Pod (template.spec.securityContext) и друг на ниво контейнер. Този на Pod важи за всички негови контейнери; този на контейнера — само за него. Разделът „Навикът за сигурност от урока“ ги разглобява ред по ред, като клъстерът показва какво прави всеки. Засега стига идеята: контейнерът работи като потребител 65532, не може да стане root, няма никаква специална способност (capability) на ядрото, не може да пише в собствената си файлова система и е под филтъра на системните извиквания, който носи runtime-ът.
2.3 Прилагане и ненамерен образ
С току-що създадения клъстер и без зареден образ манифестът беше приложен:
$ kubectl apply -f revisor-deployment.yaml
deployment.apps/revisor created
Спомни си от Урок 0 какво означава този отговор: обектът е записан. Не казва нищо за контейнери. Двадесет секунди по-късно:
$ kubectl get pods -l app=revisor
NAME READY STATUS RESTARTS AGE
revisor-697fb84f97-gn68x 0/1 ImagePullBackOff 0 20s
revisor-697fb84f97-qvs7x 0/1 ImagePullBackOff 0 20s
revisor-697fb84f97-vq4ld 0/1 ImagePullBackOff 0 20s
Трите обекта Pod съществуват, всеки вече има възел и нито един не работи: 0/1 в READY означава „нула от един контейнер е готов“. Състоянието, ImagePullBackOff, е сцената от понеделник. Обърни внимание на това, което все пак сработи: Deployment създаде своя ReplicaSet, ReplicaSet създаде три обекта Pod и kube-scheduler ги разпредели. Цялата контролна равнина свърши работата си. Онова, което се провали, е в края на веригата, в kubelet на всеки възел, който не успя да се сдобие с образа.
Разделът „Грешката, която ще видиш“ разчита тази грешка ред по ред. Кратката версия, за да продължим: kubelet потърси образа във възела, не го намери и, тъй като политиката е IfNotPresent, се опита да го изтегли. Образът не казва от кой регистър идва, така че отиде в Docker Hub, където не съществува. Опитва отново, като всеки път чака малко повече. Изтриването и повторното прилагане не променят нищо от това: новият Pod попада на възел, който също няма образа, и стига до същото място.
2.4 kind load docker-image: пренасяне на образа до всеки възел
Лекарството е да дадеш на възела каквото иска:
$ kind load docker-image curso-docker-revisor:1.6.0 --name curso-k8s-l2
Image: "curso-docker-revisor:1.6.0" with ID "sha256:bd1b2e7d91b822f31b25e7ca52c4d0ccd5b81807695860bc13a7acf8169b2e59" not yet present on node "curso-k8s-l2-worker", loading...
Image: "curso-docker-revisor:1.6.0" with ID "sha256:bd1b2e7d91b822f31b25e7ca52c4d0ccd5b81807695860bc13a7acf8169b2e59" not yet present on node "curso-k8s-l2-worker2", loading...
Image: "curso-docker-revisor:1.6.0" with ID "sha256:bd1b2e7d91b822f31b25e7ca52c4d0ccd5b81807695860bc13a7acf8169b2e59" not yet present on node "curso-k8s-l2-control-plane", loading...
kind извади образа от твоя Docker и го копира вътре във всеки възел на клъстера, по един на ред: „все още не е на еди-кой си възел, зарежда се“. Появява се и контролният възел, макар там да не работи нито един Pod на revisor. kind не знае къде ще използваш образа и го слага на всички. Отне 2 секунди, защото образът тежи около 16 MB. --name curso-k8s-l2 е задължителен, когато клъстерът не се казва kind, което е името по подразбиране. Без него kind би търсил клъстер, който не съществува, а на машина с няколко клъстера това е начинът да си сигурен, че зареждаш образа в своя, а не в друг.
Сега възелът вече го има:
$ docker exec curso-k8s-l2-worker crictl images | grep curso-docker-revisor
docker.io/library/curso-docker-revisor 1.6.0 70e09194319be 3.68MB
Виж името, с което е записан: docker.io/library/curso-docker-revisor. Това е пълното име, което отговаря на curso-docker-revisor: когато образ не казва от кой регистър идва, се приема, че е от Docker Hub (docker.io), а ако не казва и от кой потребител, от пространството му с официални образи (library). Документацията за образите го обяснява с примера на busybox, който е равен на docker.io/library/busybox:latest. Това е същото име, което се появяваше в грешката от 2.3, и затова сега kubelet го намира. (Числата изглеждат различни, но си имат обяснение. crictl казва 3.68MB. С Docker Engine 29 docker image ls curso-docker-revisor:1.6.0 показва две колони: DISK USAGE 16.2MB, колкото заема разкомпресиран на диска, и CONTENT SIZE 3.68MB, компресираното съдържание; crictl отчита второто. Ако твоят Docker е по-стар, docker image ls показва едно-единствено число и то може да не съвпада. Идентификаторът на crictl, 70e09194319be, също не е sha256:bd1b2e7d… от kind load: всеки инструмент идентифицира образа с различен отпечатък. Че е един и същ, го доказват името и тагът и това, че kubelet го намира в следващата стъпка.)
Не е нужно да прилагаш нищо отново. kubelet продължаваше да опитва сам; при следващия опит намери образа и стартира контейнера:
$ kubectl rollout status deployment/revisor --timeout=180s
Waiting for deployment "revisor" rollout to finish: 0 of 3 updated replicas are available...
Waiting for deployment "revisor" rollout to finish: 1 of 3 updated replicas are available...
Waiting for deployment "revisor" rollout to finish: 2 of 3 updated replicas are available...
deployment "revisor" successfully rolled out
$ kubectl get pods -l app=revisor -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
revisor-697fb84f97-gn68x 1/1 Running 0 33s 10.244.1.3 curso-k8s-l2-worker2 <none> <none>
revisor-697fb84f97-qvs7x 1/1 Running 0 33s 10.244.1.2 curso-k8s-l2-worker2 <none> <none>
revisor-697fb84f97-vq4ld 1/1 Running 0 33s 10.244.2.2 curso-k8s-l2-worker <none> <none>
Това е дълбокият урок от сцената: клъстерът се възстанови сам, защото желаното състояние никога не се промени. То продължаваше да казва „три реплики с този образ“. Единственото, което липсваше, беше образът, и щом пристигна, всяка част довърши работата си. Изтриването и повторното прилагане щяха да бъдат в най-добрия случай безполезни.
Погледни колоната NODE. Две реплики останаха на curso-k8s-l2-worker2 и една на curso-k8s-l2-worker. Deployment се грижи за това колко реплики има, а не къде отиват. Урок 0 го предвиди: scheduler-ът се старае да разпределя, но не го гарантира, и при друго изпълнение може да попаднат различно. Ако worker2 се изключи, две от трите реплики ще умрат едновременно. Гарантираното разпределение се декларира със собствени правила и курсът го прави, когато revisor трябва да преживее изключен възел (Урок 9). Обърни внимание и на колоната IP: всеки Pod има свой адрес и той се сменя всеки път, когато Pod се роди отново. Затова никой не бива да говори с Pod по неговия IP адрес и затова съществува Урок 3.
Журналът на revisor потвърждава, че програмата е стартирала:
$ kubectl logs deployment/revisor
Found 3 pods, using pod/revisor-697fb84f97-gn68x
revisor 1.6.0 escuchando en :8080 (1 servicio(s))
kubectl logs deployment/revisor е пряк път: избира един от обектите Pod на Deployment, показва ти неговия журнал и ти казва кой е избрал. Това е същият ред, който се отпечатваше с docker run в курса по Docker: образът е същият, смени се само кой го изпълнява.
2.5 Веригата на собствениците: Deployment, ReplicaSet и Pod
В Урок 0 видя веригата нарисувана. Сега можеш да я прочетеш в самия клъстер, защото всеки обект казва кой е собственикът му в поле, наречено metadata.ownerReferences. Документацията за собствениците и зависимите обекти го обяснява така: зависимите обекти имат това поле, което сочи към собственика им, и Kubernetes го попълва сам за обектите, които създават контролерите, като обектите ReplicaSet.
$ kubectl get pod revisor-697fb84f97-gn68x -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
ReplicaSet/revisor-697fb84f97
$ kubectl get replicaset revisor-697fb84f97 -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
Deployment/revisor
$ kubectl get deployment revisor -o jsonpath='{.metadata.ownerReferences}{"\n"}'
Обектът Pod принадлежи на ReplicaSet, ReplicaSet принадлежи на Deployment, а Deployment не принадлежи на никого: отговорът му е празен ред. Deployment го написа ти и собственикът си ти.
Опцията -o jsonpath=… заслужава малко внимание, защото курсът я използва много. kubectl get може да върне целия обект (-o yaml), но почти винаги искаш само една стойност. jsonpath ти позволява да поискаш поле по неговия път: {.metadata.ownerReferences[0].kind} означава „в обекта влез в metadata, после в ownerReferences, вземи първия елемент и ми дай неговия kind“. {"\n"} накрая само добавя нов ред.
Всяка брънка има различна работа:
| Обект | Създава го | Работата му | Ако го изтриеш |
|---|---|---|---|
| Deployment | ти | да пази желаното състояние и да управлява версиите: когато промениш шаблона, създава нов ReplicaSet и прехвърля репликите от стария към новия | изтриват се неговите обекти ReplicaSet и Pod; услугата спира |
| ReplicaSet | Deployment | да брои: да има толкова обекти Pod с точно неговия шаблон, колкото казва replicas |
Deployment създава друг същия, но kubectl delete изтрива каскадно неговите обекти Pod: измерено, трите обекта Pod минаха в Terminating едновременно и заместникът стартира от нулата, с прекъсване; затова не се пипат ръчно |
| Pod | ReplicaSet | да изпълнява контейнера | ReplicaSet създава друг |
Документацията за ReplicaSet определя предназначението му с едно изречение: „да поддържа стабилен набор от реплики на Pod, работещи по всяко време“. И препоръчва да не се използват директно: „препоръчваме да използвате Deployments вместо директно ReplicaSets“, освен в специални случаи. Документацията за Deployments е още по-категорична: „не управлявайте ReplicaSets, които принадлежат на Deployment“. В този курс никога няма да напишеш ReplicaSet. Ще ги виждаш, защото разчитането им обяснява какво прави Deployment.
А обектът Pod? Документацията за Pods го казва без заобикалки: „рядко ще създавате отделни обекти Pod директно в Kubernetes, дори единични Pod“, защото обектите Pod са проектирани като „сравнително ефимерни, за еднократна употреба“ същности. Раздел 2.7 ти показва какво означава „за еднократна употреба“ с един Pod без собственик.
2.6 Етикети и селектори
Дотук си писал -l app=revisor много пъти. Време е да видиш какво стои зад това.
Етикетът (label) е двойка ключ и стойност, закачена за обект. Документацията за етикетите ги определя като двойки, които „служат за задаване на идентифициращи атрибути на обектите, значими за потребителите“, и добавя онова, което ги прави полезни: „за разлика от имената и UID, етикетите не са уникални“. Много обекти носят един и същ етикет. Със селектор по етикети избираш набор от обекти и според същата страница селекторът е „основният примитив за групиране в Kubernetes“.
Ето как изглеждат етикетите на обектите Pod на revisor:
$ kubectl get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
revisor-697fb84f97-gn68x 1/1 Running 0 33s app=revisor,pod-template-hash=697fb84f97
revisor-697fb84f97-qvs7x 1/1 Running 0 33s app=revisor,pod-template-hash=697fb84f97
revisor-697fb84f97-vq4ld 1/1 Running 0 33s app=revisor,pod-template-hash=697fb84f97
app=revisor го сложи ти, в шаблона. Но има още един, който не си написал: pod-template-hash=697fb84f97. Сложи го Deployment, а числото е същата наставка, която носи името на ReplicaSet. Погледни селектора на ReplicaSet:
$ kubectl get replicaset revisor-697fb84f97 -o jsonpath='{.spec.selector}{"\n"}'
{"matchLabels":{"app":"revisor","pod-template-hash":"697fb84f97"}}
ReplicaSet не търси само app: revisor: търси app: revisor и pod-template-hash: 697fb84f97. Документацията за Deployments обяснява защо: контролерът за Deployments добавя този етикет към всеки ReplicaSet, който създава, изчислен с hash на шаблона на Pod, и „този етикет гарантира, че дъщерните ReplicaSets на един Deployment не се припокриват“. Когато промениш шаблона, в 2.8, ще се появи друг ReplicaSet с друг hash. Всеки ще брои само собствените си обекти Pod, макар всички да носят app=revisor. Същата страница има предупреждение в червено: „не променяйте този етикет“.
Селекторите имат две форми. Тази по равенство е онази, която използваше: app=revisor, или app!=revisor за обратното. Тази по множества пита за принадлежност:
$ kubectl get pods -l 'app in (revisor,prueba)' -o name
pod/revisor-697fb84f97-gn68x
pod/revisor-697fb84f97-qvs7x
pod/revisor-697fb84f97-vq4ld
$ kubectl get pods -l app=prueba
No resources found in default namespace.
Първата иска обектите Pod, чийто етикет app е revisor или prueba, и тъй като все още няма нито един prueba, връща само тези на revisor. Втората го потвърждава. Две правила от документацията, за да не сбъркаш: няколко изисквания, разделени със запетая, се комбинират с „и“ (app=revisor,pod-template-hash=697fb84f97 изисква и двете), а оператор „или“ между различни изисквания не съществува. За „или“ върху един и същ ключ има in.
No resources found не е грешка. Това е отговор: няма нищо, което да съвпада. Да различаваш „няма нищо“ от „нещо се провали“ е част от разчитането на това, което казва клъстерът.
Защо курсът винаги търси по етикет и никога по име на Pod? Защото името се сменя всеки път, когато обектът Pod се роди отново, а етикетът — не. Команда, в която пише revisor-697fb84f97-gn68x, престава да върши работа в мига, в който този Pod умре. Такава, в която пише -l app=revisor, продължава да работи през заместванията, в рамките на същото пространство от имена и докато никой друг Pod не използва този етикет (Упражнение 2 показва какво става, когато някой го използва). Обектите Service от Урок 3 избират своите обекти Pod по същия начин: по етикет. Това прави важно онова, което показва Упражнение 2.
2.7 Самостоятелен Pod срещу Pod на Deployment
2.7.1 Самостоятелният Pod
Един Pod може да се създаде сам, без Deployment. Този манифест е програма (fig02_01.pod.yaml). Това е същият шаблон от Deployment, но написан като Pod, с фиксирано име и без етикета app: revisor, за да не го сбърка никой ReplicaSet с някой от своите:
# fig02_01.pod.yaml — un Pod suelto: el revisor sin Deployment que lo cuide.
# Sirve para ver qué hace un Pod por sí solo, y qué NO hace: si lo borras, nadie
# lo repone. No lleva etiqueta app: revisor a propósito, para que ningún
# ReplicaSet lo confunda con uno de los suyos.
apiVersion: v1
kind: Pod
metadata:
name: revisor-suelto
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: revisor
image: curso-docker-revisor:1.6.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
env:
- name: REVISOR_SERVICIOS
value: "prueba http://prueba:9000/ 2s"
volumeMounts:
- name: datos
mountPath: /datos
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumes:
- name: datos
emptyDir:
sizeLimit: 10Mi
Обърни внимание на разликата с Deployment: apiVersion: v1 (обектите Pod живеят в основната група на API, която няма име) и kind: Pod. Онова, което в Deployment беше вътре в template.spec, тук е директно в spec. Един Deployment до голяма степен е шаблон на Pod с брояч отгоре.
$ kubectl apply -f fig02_01.pod.yaml
pod/revisor-suelto created
$ kubectl get pod revisor-suelto -o jsonpath='{.metadata.name} dueños={.metadata.ownerReferences}{"\n"}'
revisor-suelto dueños=
$ kubectl delete pod revisor-suelto
pod "revisor-suelto" deleted from default namespace
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
revisor-697fb84f97-gn68x 1/1 Running 0 40s
revisor-697fb84f97-qvs7x 1/1 Running 0 40s
revisor-697fb84f97-vq4ld 1/1 Running 0 40s
dueños= празно: никой не е собственик на този Pod. Беше изтрит, скриптът изчака пет секунди и revisor-suelto вече го няма. Никой не го възстанови, защото нито един контролер нямаше „искам един revisor-suelto“ в желаното си състояние.
Внимавай с онова, което самостоятелният Pod все пак прави. Ако неговият процес падне, kubelet го стартира отново в същия Pod, защото полето restartPolicy по подразбиране е Always (kubernetes.io, „Pod Lifecycle“). Прилича на docker run --restart always на една-единствена машина: издържа програмата да се срине, а колоната RESTARTS го отброява. Онова, което не издържа, е загубата на самия Pod. Документацията за Pods казва, че един Pod остава на своя възел, „докато не приключи, не бъде изтрит обектът Pod, не бъде изгонен поради липса на ресурси или възелът не откаже“, а тази за жизнения му цикъл добавя, че един Pod никога не се „препланира“ към друг възел: вместо това някой може да създаде нов, почти идентичен. При самостоятелния Pod този някой не съществува. Ако го изтрият, изгонят или се изключи възелът му, край.
2.7.2 Обектът Pod на Deployment
Сега същото с Pod, който има собственик:
$ kubectl delete pod revisor-697fb84f97-gn68x
pod "revisor-697fb84f97-gn68x" deleted from default namespace
$ kubectl get pods -l app=revisor
NAME READY STATUS RESTARTS AGE
revisor-697fb84f97-nrwht 1/1 Running 0 1s
revisor-697fb84f97-qvs7x 1/1 Running 0 41s
revisor-697fb84f97-vq4ld 1/1 Running 0 41s
$ kubectl get events --field-selector involvedObject.kind=ReplicaSet,reason=SuccessfulCreate -o custom-columns=OBJETO:.involvedObject.name,MENSAJE:.message
OBJETO MENSAJE
revisor-697fb84f97 Created pod: revisor-697fb84f97-vq4ld
revisor-697fb84f97 Created pod: revisor-697fb84f97-gn68x
revisor-697fb84f97 Created pod: revisor-697fb84f97-qvs7x
revisor-697fb84f97 Created pod: revisor-697fb84f97-nrwht
Секунда по-късно има нов Pod, nrwht, а събитията разказват историята: три създавания в началото и четвърто — възстановяването. Това е термостатът от Урок 0, сега с твоята програма.
-o custom-columns=… е сестрата на jsonpath за таблици: казваш му кои колони искаш и от кое поле идва всяка. Използва се тук, за да се побере всяко събитие на един ред.
Сравнението е най-важната идея в урока: обектът Pod работи, но онзи, който го възстановява, е собственикът му. Pod без собственик рестартира контейнера си, ако процесът падне, но самият Pod не се ражда отново: ако бъде изтрит, изгонен или загуби възела си, край. Затова в този курс всичко, което трябва да продължи да работи, живее вътре в Deployment. Самостоятелни обекти Pod се появяват само за отстраняване на грешки или за да се демонстрира нещо и се изтриват накрая.
2.8 Rollout: ревизии, история и рестартиране
2.8.1 Какво е ревизия
Rollout е преминаването от една версия на обектите Pod към друга. Документацията за Deployments казва кога се случва, и правилото е точно: rollout се задейства „ако и само ако шаблонът на Pod в Deployment (.spec.template) се промени“, например етикетите или образът му. Други промени, като мащабирането от три на пет реплики, не го задействат. Всеки rollout оставя ревизия в историята, а всяка ревизия е един ReplicaSet.
$ kubectl rollout history deployment/revisor
deployment.apps/revisor
REVISION CHANGE-CAUSE
1 <none>
Една ревизия: тази, която се роди с apply. Колоната CHANGE-CAUSE казва <none>, защото никой не е записал причината. Може да се попълни с анотация на Deployment; курсът не го прави, защото причината за всяка промяна живее в историята на Git на твоите манифести.
2.8.2 Рестартиране, без да се променя нищо
Понякога искаш всички обекти Pod да бъдат заменени, без да се променя образът или конфигурацията: например, за да прочетат отново нещо, което се е променило извън тях. Ръчното им изтриване върши работа, но ги изтриваш всичките наведнъж и услугата остава за момент без реплики. За това съществува kubectl rollout restart:
$ kubectl rollout restart deployment/revisor
deployment.apps/revisor restarted
$ kubectl rollout status deployment/revisor --timeout=180s
Waiting for deployment "revisor" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "revisor" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "revisor" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "revisor" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "revisor" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "revisor" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "revisor" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "revisor" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "revisor" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "revisor" rollout to finish: 1 old replicas are pending termination...
deployment "revisor" successfully rolled out
$ kubectl get replicaset -l app=revisor
NAME DESIRED CURRENT READY AGE
revisor-55cc969586 3 3 3 2s
revisor-697fb84f97 0 0 0 55s
Прочети напредъка на rollout status: един от три нови, два от три и накрая „една стара реплика чака да приключи“. Обектите Pod не бяха заменени изведнъж: създаваше се един нов и се махаше един стар, на смени. Това е стратегията по подразбиране на Deployment, RollingUpdate. Двата ѝ регулатора са 25 %, ако не ги пипаш: maxSurge — колко обекта Pod в повече може да има по време на промяната (закръгля се нагоре), и maxUnavailable — колко може да липсват (закръгля се надолу). При три реплики 25 % са 0.75: maxSurge става 1, а maxUnavailable — 0. Казано просто: по време на промяната може да има до четири обекта Pod и контролерът не маха нито един стар, докато новият не е готов, така че планът му никога не пада под три налични. Това е обещание на контролера, не на вселената: ако в този момент падне възел или умре процес, бройката намалява по причини извън него. А при друг брой реплики закръглянето се променя: при четири 25 % вече е 1 и maxUnavailable би бил 1. Урок 9 се връща към това с истинско натоварване и измерва дали се губи някоя заявка.
И резултатът: нов ReplicaSet, revisor-55cc969586, с трите реплики, и старият, revisor-697fb84f97, на 0 0 0. Старият не беше изтрит: остана празен, като запис на ревизия 1. Документацията казва, че по подразбиране се пазят 10 стари ReplicaSet (revisionHistoryLimit) и че изтриването на един означава да загубиш възможността да се върнеш към тази ревизия. Урок 9 използва тази възможност с rollout undo.
Защо имаше rollout, ако не си променил нищо? Нещо все пак се промени:
$ kubectl rollout history deployment/revisor
deployment.apps/revisor
REVISION CHANGE-CAUSE
1 <none>
2 <none>
$ kubectl get deployment revisor -o jsonpath='{.spec.template.metadata.annotations}{"\n"}'
{"kubectl.kubernetes.io/restartedAt":"2026-10-21T03:55:53-06:00"}
rollout restart добави към шаблона анотация с часа на рестартирането. Шаблонът се промени и правилото „ако и само ако“ се изпълни: нов шаблон, нов hash, нов ReplicaSet, ревизия 2. Няма магия, само същото правило, приложено към поле, което си сложил, без да го въвеждаш.
Тази анотация не е в твоя файл и това има последица. Ако утре приложиш revisor-deployment.yaml отново, kubectl apply няма да я махне, защото управлява само полетата, които файлът декларира (Урок 0 обясни анотацията last-applied-configuration, чрез която води тази сметка). Крайното състояние на този урок се проверява с kubectl diff, който не намира разлики.
Грешката, която ще видиш
Грешката в този урок е тази от 2.3: ErrImagePull, която после става ImagePullBackOff. Предизвикана е нарочно и се разчита по ред, защото този ред служи за всеки Pod, който не стартира.
Първо, състоянието. kubectl get pods ти даде ImagePullBackOff и 0/1. Колоната STATUS ти казва на кой етап е заседнал обектът Pod. Самото Pending обикновено означава „все още няма възел“ (scheduler-ът не е намерил място; ще го видиш в Урок 5). Когато има нещо по-конкретно, kubectl показва него вместо фазата: ImagePullBackOff е „вече има възел, а kubelet на този възел не успява да се сдобие с образа“, а CrashLoopBackOff би било „образът стартира, а програмата пада“. Един нюанс: фазата на Pod, която е друго поле, остава Pending, докато образът се изтегля. Документацията за жизнения цикъл го казва така: Pending включва времето на изчакване да бъде разпределен възел и „времето за изтегляне на образите по мрежата“ (kubernetes.io, „Pod Lifecycle“). Фазата се разчита с kubectl get pod <pod> -o jsonpath='{.status.phase}'. Само от колоната STATUS вече знаеш коя част от Урок 0 да питаш.
Второ, събитията. Клъстерът оставя следа от всеки опит:
$ kubectl get events --field-selector reason=Failed -o custom-columns=OBJETO:.involvedObject.name,MENSAJE:.message | head -4
OBJETO MENSAJE
revisor-697fb84f97-gn68x Failed to pull image "curso-docker-revisor:1.6.0": failed to pull and unpack image "docker.io/library/curso-docker-revisor:1.6.0": failed to resolve reference "docker.io/library/curso-docker-revisor:1.6.0": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
revisor-697fb84f97-gn68x Error: ErrImagePull
revisor-697fb84f97-gn68x Error: ImagePullBackOff
Първото съобщение е дълго и се чете като верига от „защото“, отляво надясно:
Failed to pull image "curso-docker-revisor:1.6.0": не успях да изтегля образа, който поиска…failed to pull and unpack image "docker.io/library/curso-docker-revisor:1.6.0": …който, с пълното си име, е този от Docker Hub…failed to resolve reference: …защото не успях да намеря тази препратка…pull access denied, repository does not exist or may require authorization: …защото Docker Hub ми отказа достъп: хранилището не съществува или изисква идентификационни данни.
Решаващата следа е docker.io/library/…. Възелът е отишъл да търси твоя образ в интернет. Никога не е погледнал в твоя Docker, защото не може: това не е неговото хранилище. Docker Hub казва „не съществува или ти трябва разрешение“ и не уточнява кое от двете. Така се държат регистрите, за да не издават кои частни хранилища съществуват. Тук е първото: не съществува.
Трето, describe. kubectl get events смесва събитията на цялото пространство от имена. kubectl describe pod събира всичко за един Pod, а събитията му — накрая. Внимание: повтарящо се събитие се групира в един ред и се подрежда по последния път, когато се е случило, така че Pulling (x2 over 20s) се появява след BackOff, макар да се е случило първо:
$ kubectl describe pod revisor-697fb84f97-gn68x | sed -n '/^Events:/,$p'
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 20s default-scheduler Successfully assigned default/revisor-697fb84f97-gn68x to curso-k8s-l2-worker2
Normal BackOff 19s kubelet spec.containers{revisor}: Back-off pulling image "curso-docker-revisor:1.6.0"
Warning Failed 19s kubelet spec.containers{revisor}: Error: ImagePullBackOff
Normal Pulling 6s (x2 over 20s) kubelet spec.containers{revisor}: Pulling image "curso-docker-revisor:1.6.0"
Warning Failed 6s (x2 over 19s) kubelet spec.containers{revisor}: Failed to pull image "curso-docker-revisor:1.6.0": failed to pull and unpack image "docker.io/library/curso-docker-revisor:1.6.0": failed to resolve reference "docker.io/library/curso-docker-revisor:1.6.0": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
Warning Failed 6s (x2 over 19s) kubelet spec.containers{revisor}: Error: ErrImagePull
(sed реже изхода, за да покаже само от Events: нататък; над това describe показва образа, възела, томовете, securityContext и условията на обекта Pod.) Колоната From казва кой говори и тя те учи най-много. В логически ред:
default-scheduler,Scheduled: scheduler-ът разпредели обекта Pod къмcurso-k8s-l2-worker2. Неговата част мина добре.kubelet,Pulling:kubeletна този възел се опита да изтегли образа.(x2 over 20s)означава, че същото събитие се е случило два пъти за 20 секунди; клъстерът групира повторенията, вместо да записва всички.kubelet,FailedсErrImagePull: опитът се провали.kubelet,BackOffиFailedсImagePullBackOff:kubeletреши да изчака преди следващия опит.
ErrImagePull и ImagePullBackOff са един и същ проблем в два момента. ErrImagePull е провалът на един опит. ImagePullBackOff е изчакването между опитите. Документацията за образите го обяснява: частта BackOff показва, че Kubernetes ще продължи да опитва, с изчакване, което расте при всеки опит, до фиксиран лимит от 300 секунди (5 минути). Затова обектът Pod редува двете състояния, докато не му дадеш образа. И затова възстановяването в 2.4 не беше мигновено за всеки Pod, който е бил по средата на дълго изчакване: kubelet взима образа при следващия си опит, не по-рано. Ако един Pod чака отдавна и не искаш да изчакваш, изтриването му след като си поправил причината наистина помага: ReplicaSet създава друг, който опитва веднага. Изтриването преди да поправиш е онова, което направи екипът от сцената.
Редът, за всеки Pod, който не стартира: състоянието (get pods), describe на обекта Pod (събитията му и колоната From) и, ако контейнерът е успял да стартира, неговите logs. Първо се разчита и после се променя нещо. А онова, което се променя, е причината, не обектът Pod.
Навикът за сигурност от урока: securityContext още от първия манифест
Таблицата с навици на курса възлага на този урок securityContext на първия Deployment: без root, без ескалация на привилегии, без capabilities, файлова система само за четене и профил seccomp. Ето как беше приложен в клъстера:
$ kubectl get pod -l app=revisor -o jsonpath='{.items[0].spec.securityContext}{"\n"}{.items[0].spec.containers[0].securityContext}{"\n"}'
{"runAsGroup":65532,"runAsNonRoot":true,"runAsUser":65532,"seccompProfile":{"type":"RuntimeDefault"}}
{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true}
Първият ред е този на обекта Pod; вторият — на контейнера. Ред по ред, с онова, което предотвратява всеки от тях:
| Ред | Какво прави | Какво предотвратява | Еквивалентът му в Docker |
|---|---|---|---|
runAsNonRoot: true |
kubelet отказва да стартира контейнера, ако той би работил като потребител 0 |
зле направен образ или зле написан ред да работи като root, без никой да забележи |
няма: Docker не проверява |
runAsUser: 65532, runAsGroup: 65532 |
фиксира цифровите потребител и група, с които работи процесът | зависимостта от това, което казва образът; числото остава записано в манифеста | --user 65532:65532 |
seccompProfile: RuntimeDefault |
прилага филтъра на системните извиквания, който носи runtime-ът | процесът да използва извиквания към ядрото, от които един HTTP сървър никога не се нуждае. Без този ред Kubernetes изпълнява контейнера без филтър (Unconfined), освен ако kubelet няма включена опцията seccompDefault |
Docker прилага своя профил по подразбиране, без да го искаш; Kubernetes — не |
allowPrivilegeEscalation: false |
един процес не може да получи повече привилегии от родителя си, например като изпълни двоичен файл setuid | някой, който влезе в контейнера, да стане root отвътре |
--security-opt no-new-privileges |
capabilities: drop: ["ALL"] |
премахва всички специални способности (capabilities) на ядрото | процесът да може например да сменя собственика на файлове (CHOWN) или да отваря сурови sockets, за да изработва мрежови пакети (NET_RAW) |
--cap-drop ALL |
readOnlyRootFilesystem: true |
монтира файловата система на контейнера само за четене | нападател да остави инструменти или да промени програмата | --read-only |
Определенията идват от официалното ръководство „Configure a Security Context for a Pod or Container“. За allowPrivilegeEscalation например то казва, че „контролира дали един процес може да получи повече привилегии от родителския си процес“ и че включва флага no_new_privs на ядрото. А ръководството за seccomp обяснява разликата с Docker, която отбелязва таблицата: ако опцията seccompDefault е включена, обектите Pod използват RuntimeDefault, когато не искат друг профил; „в противен случай стойността по подразбиране е Unconfined“ (kubernetes.io, „Restrict a Container's Syscalls with seccomp“). Затова редът се пише винаги.
Има една отправна точка, която е по-важна от всяко ръководство: Pod Security Standards, трите нива на сигурност, които Kubernetes определя (kubernetes.io, „Pod Security Standards“). Най-строгото, restricted, е предназначено да „налага текущите най-добри практики за подсилване на защитата на обектите Pod“. То изисква четири от тези редове: работа като не-root (runAsNonRoot: true), забрана на ескалацията на привилегии, премахване на всички capabilities (позволява да се добави обратно само NET_BIND_SERVICE) и изрично фиксиране на профила seccomp („както профилът Unconfined, така и липсата на профил са забранени“). Освен това restricted забранява да се иска runAsUser: 0 (точно каквото прави fig02_02.raiz.yaml по-долу) и позволява само определени типове томове, сред тях emptyDir. Манифестът на revisor изпълнява всичко това още от днес. Когато Урок 7 постави пространството от имена на ниво restricted, revisor вече ще е готов и няма да се налага да се преписва нищо в последния момент.
readOnlyRootFilesystem не се изисква от restricted. Курсът го слага, защото revisor го понася: единственият му файл за писане отива в /datos, а за това съществува emptyDir.
Какво се вижда отвътре. Образът на revisor няма shell. Беше проверено:
$ kubectl exec deployment/revisor -- /revisor healthcheck
sano
$ kubectl exec deployment/revisor -- /revisor version
1.6.0
$ kubectl exec deployment/revisor -- sh -c 'id'
error: Internal error occurred: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "d8ccff765090d70932ea2f15a41fa34693c57f56ad0106eeb012a0a816fa701d": OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
[código de salida: 1]
kubectl exec изпълнява програма вътре в контейнер, който вече работи, като docker exec. С deployment/revisor избира един от неговите обекти Pod. Двете подкоманди на самия revisor работят: healthcheck проверява неговия /salud отвътре и отговаря sano, а version казва 1.6.0. Но sh не съществува: executable file not found in $PATH. Това не е дефект, а защита. Който успее да изпълни нещо вътре в контейнера, не намира нито shell, нито curl, нито мениджър на пакети. Начинът за отстраняване на грешки без shell идва в Урок 3: отделен Pod, направен за това.
Монтиранията на контейнера потвърждават тома за данни и разкриват още нещо:
$ kubectl get pod -l app=revisor -o jsonpath='{.items[0].spec.containers[0].volumeMounts}{"\n"}'
[{"mountPath":"/datos","name":"datos"},{"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount","name":"kube-api-access-vxlgn","readOnly":true}]
/datos е твоят emptyDir. Второто монтиране не си го написал ти: Kubernetes монтира в обекта Pod идентификационни данни за разговор с kube-apiserver, тези на сервизния акаунт (service account) по подразбиране на пространството от имена. revisor никога не говори със сървъра на API, така че не се нуждае от тях. Остава записано като известен дълг, със своя урок: Урок 7 дава на revisor собствен сервизен акаунт, без права, и оттам нататък всички акаунти в курса носят automountServiceAccountToken: false, за да не се монтират тези идентификационни данни.
Предпазната мрежа в действие. runAsNonRoot изглежда излишно, ако вече си написал runAsUser: 65532. Стойността му се проявява, когато някой сгреши. Този Pod (fig02_02.raiz.yaml, програма, която се проваля нарочно) иска две неща едновременно: да не е root и да е потребител 0, който е root:
# fig02_02.raiz.yaml — FALLA A PROPÓSITO: pide correr como root (runAsUser: 0)
# y a la vez exige no ser root (runAsNonRoot: true). El kubelet se niega a
# arrancar el contenedor. Es la red de seguridad de runAsNonRoot en acción.
apiVersion: v1
kind: Pod
metadata:
name: revisor-raiz
spec:
securityContext:
runAsNonRoot: true
runAsUser: 0
seccompProfile:
type: RuntimeDefault
containers:
- name: revisor
image: curso-docker-revisor:1.6.0
imagePullPolicy: IfNotPresent
env:
- name: REVISOR_SERVICIOS
value: "prueba http://prueba:9000/ 2s"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
$ kubectl apply -f fig02_02.raiz.yaml
pod/revisor-raiz created
$ kubectl get pod revisor-raiz
NAME READY STATUS RESTARTS AGE
revisor-raiz 0/1 CreateContainerConfigError 0 10s
$ kubectl get events --field-selector involvedObject.name=revisor-raiz,reason=Failed -o custom-columns=MENSAJE:.message | head -2
MENSAJE
Error: container's runAsUser breaks non-root policy (pod: "revisor-raiz_default(49586352-b84d-462b-9ded-e41bb2d2123a)", container: revisor)
kube-apiserver прие обекта Pod (created), защото във формата му няма нищо невалидно. Онзи, който отказа, беше kubelet, когато тръгна да създава контейнера: CreateContainerConfigError, „конфигурацията на контейнера не може да се използва“, а събитието казва защо: „runAsUser на контейнера нарушава политиката за не-root“. Образът беше във възела и програмата така и не стигна до изпълнение. Това е функцията на runAsNonRoot: втори ключ, който не зависи от това дали първият е написан правилно. Обектът Pod беше изтрит накрая.
Какво защитава и какво не. securityContext ограничава онова, което може да направи процесът, ако някой го компрометира: да не е root, да не получава привилегии, да не променя файловете си. Той не защитава revisor от злонамерени заявки (това е работа на кода му), не решава кой може да говори с него (това е мрежата, Урок 7) и не защитава твоята машина от възела на kind, който, както видя в Урок 0, е привилегирован контейнер.
Какво се прави погрешно
Да изтриеш и приложиш отново при грешка. Това е сцената от понеделник. Ако причината не се е променила, новият Pod стига до същата грешка. Първо се разчита describe, после се поправя причината.
Да използваш :latest в kind. С :latest или без таг политиката за изтегляне по подразбиране е Always и възелът се опитва да отиде до регистъра, макар вече да си му заредил образа. Освен това :latest не казва коя версия работи: два възела могат да имат два различни образа с един и същ таг. Фиксиран таг и IfNotPresent.
Да забравиш --name в kind load docker-image. Без него kind търси клъстера с име kind. Ако не съществува, се проваля. Ако съществува и не е твоят, си заредил образа на грешното място, а твоят клъстер продължава да е без него.
Да създаваш самостоятелни обекти Pod за нещо, което трябва да продължи да работи. Pod без собственик не се ражда отново. За услуга — винаги Deployment.
Да слагаш на самостоятелен Pod етикетите на Deployment. Ще го видиш в Упражнение 2: Deployment не го осиновява, но всичко, което търси по този етикет, го намира. Това е Pod, който се появява в преброяванията и в трафика, без никой да се грижи за него.
Да редактираш или изтриваш ръчно ReplicaSet на Deployment. Deployment е негов собственик и го изгражда наново. Документацията го казва без заобикалки: не управлявай ReplicaSets, които принадлежат на Deployment.
Да пипаш етикета pod-template-hash. Слага го Deployment, за да не се застъпват неговите ReplicaSets. Промяната му разваля това разделение.
Да махаш securityContext, „за да тръгне“. Ако контейнер не стартира със securityContext на курса, проблемът почти винаги е в образа (работи като root, пише където не бива) и се поправя в образа или с том, а не като се отнемат защити от манифеста.
Да се доверяваш на Running. Без emptyDir revisor също щеше да е Running и 1/1, и щеше да се провали при първата заявка, която го накара да запише отчета. Running означава, че процесът е жив, а не че си върши добре работата. Урок 5 учи клъстера да прави разлика.
Упражнения
Първите три се изпълняват в клъстера на урока, по ред и от състоянието, в което остана в края на 2.8. Преди всяко упражнение kubectl config current-context трябва да казва kind-curso-k8s-l2; ако казва нещо друго, не продължавай. correr.sh ги изпълнява по същия начин, а изходът му е в correr.salida.txt. Преди да изпълниш всяко, запиши в дневника какво мислиш, че ще стане. Четвъртото се пише със собствени думи.
Упражнение 1 — Да извадиш Pod от стадото
Избери един от обектите Pod на revisor и смени етикета му app на друга стойност:
kubectl label pod <un-pod-del-revisor> app=fuera --overwrite
Предвиди: колко обекта Pod има пет секунди по-късно? Обектът Pod, който промени, продължава ли да работи? Кой е собственикът му сега? Провери го с kubectl get pods -L app (опцията -L добавя колона със стойността на този етикет) и с jsonpath от 2.7.1, -o jsonpath='dueños={.metadata.ownerReferences}{"\n"}', който върши работа и когато списъкът със собственици е празен ([0] от 2.5 върху празен списък отпечатва само една наклонена черта, /, и излиза с код 0, без да предупреди, че няма нищо). Накрая изтрий обекта Pod, който извади.
Упражнение 2 — Обектът Pod, който се промъква (проект)
fig02_03.adoptado.yaml е самостоятелен Pod, revisor-extra, идентичен с тези на revisor и с етикета app: revisor:
# fig02_03.adoptado.yaml — Ejercicio 2: un Pod suelto que SÍ lleva app: revisor.
# Pregunta: el Deployment selecciona app: revisor. ¿Se queda con este Pod? ¿Borra
# uno de los suyos para seguir en tres? ¿Y qué ve quien busca por app=revisor?
apiVersion: v1
kind: Pod
metadata:
name: revisor-extra
labels:
app: revisor
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: revisor
image: curso-docker-revisor:1.6.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
env:
- name: REVISOR_SERVICIOS
value: "prueba http://prueba:9000/ 2s"
volumeMounts:
- name: datos
mountPath: /datos
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumes:
- name: datos
emptyDir:
sizeLimit: 10Mi
Преди да го приложиш, отговори на трите въпроса от коментара му. После го приложи и провери с kubectl get pods -l app=revisor, kubectl get deployment revisor и селектора на обектите ReplicaSet. Накрая обясни какъв проблем би причинил този Pod, когато в Урок 3 един Service избира своите обекти Pod по app: revisor. Изтрий го накрая.
Упражнение 3 — Таг, който не съществува
fig02_04.etiqueta-mala.yaml е същият Deployment с една-единствена разлика: иска образа curso-docker-revisor:1.6.9, който не съществува нито в твоя Docker, нито в клъстера. Преди да го приложиш, предвиди: колко обекта Pod на revisor остават да работят, докато rollout е заседнал? Колко нови обекта Pod се опитва да създаде? (Подсказка: числата за maxSurge и maxUnavailable от 2.8.2.) После го приложи, изчакай с kubectl rollout status deployment/revisor --timeout=40s и погледни kubectl get pods -l app=revisor и kubectl get deployment revisor. За да излезеш от задръстването, приложи отново revisor-deployment.yaml и обясни защо rollout приключва мигновено.
Упражнение 4 — Защити securityContext
Колега ти предлага да махнеш readOnlyRootFilesystem: true и тома datos, „защото са две неща за поддръжка, а revisor стартира и така“. В най-много пет изречения му отговори: какво предотвратява този ред, защо томът съществува именно заради него, какво би станало с отчета, ако махнеш само тома и оставиш реда, и дали махането му нарушава нивото restricted.
Решения
Решение 1
Измерено в correr.salida.txt:
$ kubectl label pod revisor-55cc969586-6m76h app=fuera --overwrite
pod/revisor-55cc969586-6m76h labeled
$ kubectl get pods -L app
NAME READY STATUS RESTARTS AGE APP
revisor-55cc969586-6m76h 1/1 Running 0 8s fuera
revisor-55cc969586-7tmfc 1/1 Running 0 7s revisor
revisor-55cc969586-f8dk6 1/1 Running 0 6s revisor
revisor-55cc969586-x6vfq 1/1 Running 0 5s revisor
$ kubectl get pod revisor-55cc969586-6m76h -o jsonpath='dueños={.metadata.ownerReferences}{"\n"}'
dueños=
Четири обекта Pod. Онзи, който промени, продължава да е Running: смяната на етикет не го спира. Но той престана да съвпада със селектора на ReplicaSet, така че ReplicaSet го пусна (неговият ownerReferences остана празен) и, тъй като сега броеше два там, където трябваше да има три, създаде нов. Остават трите от стадото плюс един самостоятелен, за когото никой не се грижи. Документацията за ReplicaSet описва тази техника: можеш да извадиш обекти Pod от ReplicaSet, като смениш етикетите им, например „за отстраняване на грешки или възстановяване на данни“, а извадените така се заместват сами. Полезно е: оставяш настрана Pod, който се държи зле, за да го изучиш, без услугата да губи реплика. Но не го забравяй там: скриптът го изтри накрая.
Решение 2
Измерено в correr.salida.txt. (fig02_03.adoptado.yaml декларира порта http, също като шаблона на Deployment. Полето е информативно и не променя нито един от тези изходи, но има значение за трафика в Урок 3: затова обектът Pod е идентичен с тези на Deployment.)
$ kubectl apply -f fig02_03.adoptado.yaml
pod/revisor-extra created
$ kubectl get pods -l app=revisor
NAME READY STATUS RESTARTS AGE
revisor-55cc969586-7tmfc 1/1 Running 0 13s
revisor-55cc969586-f8dk6 1/1 Running 0 12s
revisor-55cc969586-x6vfq 1/1 Running 0 11s
revisor-extra 1/1 Running 0 5s
$ kubectl get deployment revisor
NAME READY UP-TO-DATE AVAILABLE AGE
revisor 3/3 3 3 67s
$ kubectl get pod revisor-extra -o jsonpath='dueños={.metadata.ownerReferences}{"\n"}'
dueños=
$ kubectl get replicaset -l app=revisor -o custom-columns=NOMBRE:.metadata.name,DESEADOS:.spec.replicas,SELECTOR:.spec.selector.matchLabels
NOMBRE DESEADOS SELECTOR
revisor-55cc969586 3 map[app:revisor pod-template-hash:55cc969586]
revisor-697fb84f97 0 map[app:revisor pod-template-hash:697fb84f97]
Трите отговора:
- Не го взима.
revisor-extraпродължава да е без собственик. Причината е в селектора на ReplicaSet: той не търси самоapp: revisor, търси иpod-template-hash: 55cc969586, аrevisor-extraне носи този етикет. Документацията за ReplicaSet казва, че ReplicaSet веднага осиновява Pod без собственик, който съвпада с неговия селектор, и че затова е добре самостоятелните обекти Pod да не носят етикети, които съвпадат със селектора на някой ReplicaSet. Тук етикетътpod-template-hashпредотврати осиновяването. - Не изтрива нито един от своите. Deployment казва
3/3: за него съществуват само неговите три. - Който търси по
app=revisor, вижда четири.kubectl get pods -l app=revisorизброяваrevisor-extra, сякаш е още една реплика.
Тази трета точка е проблемът. Един Service, в Урок 3, избира своите обекти Pod по етикет, и то по този от манифеста, app: revisor, а не по pod-template-hash, и сочи към порта с име http. Този самостоятелен Pod носи и двете неща: етикета и порта с това име. Така че този Pod, който никой не възстановява, ако умре, и който никой не обновява, когато смениш образа, би получавал своя дял от трафика, сякаш е още една реплика. Точно това има предвид документацията, когато предупреждава, че припокриващите се селектори водят до неочаквано поведение. Скриптът го изтри накрая.
Решение 3
Измерено в correr.salida.txt:
$ kubectl apply -f fig02_04.etiqueta-mala.yaml
deployment.apps/revisor configured
$ kubectl rollout status deployment/revisor --timeout=40s
Waiting for deployment "revisor" rollout to finish: 1 out of 3 new replicas have been updated...
error: timed out waiting for the condition
[código de salida: 1]
$ kubectl get pods -l app=revisor
NAME READY STATUS RESTARTS AGE
revisor-55cc969586-7tmfc 1/1 Running 0 55s
revisor-55cc969586-f8dk6 1/1 Running 0 54s
revisor-55cc969586-x6vfq 1/1 Running 0 53s
revisor-b8b699487-fc4gn 0/1 ErrImagePull 0 41s
$ kubectl get deployment revisor
NAME READY UP-TO-DATE AVAILABLE AGE
revisor 3/3 1 3 109s
Три стари обекта Pod продължават да работят и има само един нов Pod, заседнал в ErrImagePull. При три реплики maxSurge е 1 (може да има един Pod в повече), а maxUnavailable е 0 (не може да липсва нито един). Deployment създаде нов Pod и, за да махне един стар, изчака новият да стане наличен. Той така и не стана, така че не махна нито един. kubectl get deployment го обобщава: READY 3/3 (услугата остава пълна), UP-TO-DATE 1 (само една реплика има новия шаблон). Тук rollout със счупен образ не сваля услугата: заседва с непокътната услуга, защото при три реплики maxUnavailable е 0. Това не е закон: при четири реплики 25 % вече са 1 и Deployment наистина би махнал една изправна реплика, преди да знае дали новата стартира, а ако стар Pod падне, докато rollout е заседнал, капацитетът намалява. rollout status се отказа след 40 секунди, защото ти го поиска; Deployment, сам по себе си, би чакал 600 секунди (progressDeadlineSeconds, чиято стойност по подразбиране според документацията е 600), преди да отбележи rollout като неуспешен с условието ProgressDeadlineExceeded.
При повторното прилагане на добрия файл:
$ kubectl apply -f revisor-deployment.yaml
deployment.apps/revisor configured
$ kubectl rollout status deployment/revisor --timeout=180s
deployment "revisor" successfully rolled out
$ kubectl get deployment,replicaset -l app=revisor
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/revisor 3/3 3 3 109s
NAME DESIRED CURRENT READY AGE
replicaset.apps/revisor-55cc969586 3 3 3 56s
replicaset.apps/revisor-697fb84f97 0 0 0 109s
replicaset.apps/revisor-b8b699487 0 0 0 41s
Приключи мигновено, защото шаблонът отново стана идентичен с този на ReplicaSet revisor-55cc969586: същият образ и същата анотация restartedAt, която apply не маха, защото файлът не я декларира. Същият шаблон, същият hash, същият ReplicaSet. Deployment не създаде нов: свали на нула счупения, revisor-b8b699487, и тъй като добрият вече имаше своите три реплики, нямаше какво повече да се прави. Връщането към предишната версия с kubectl rollout undo е краткият начин да се направи това и идва в Урок 9. Тук беше направено по начина, на който учи курсът: файлът е истината и се прилага отново.
Решение 4
Един възможен отговор: „readOnlyRootFilesystem пречи на някой, който влезе в контейнера, да промени програмата или да остави инструменти, и е евтино, защото revisor пише само един файл. Именно заради този ред съществува томът datos: това е единственото място, където revisor може да запише отчета си. Ако махнеш тома и оставиш реда, revisor стартира и изглежда Running, но първия път, когато се опита да запише отчет, отговаря с 500 и no pude guardar el reporte. Махането на реда не нарушава нивото restricted, което не го изисква, но се отказваш от защита, която не струва нищо. А „стартира и така“ не е доказателство: важното е дали продължава да работи, когато някой го атакува и когато някой му възложи работа.“
Изтриване на клъстера
Когато приключиш с упражненията, изтрий клъстера на урока. Не оставяй нищо живо на машината:
$ kind delete cluster --name curso-k8s-l2
Deleting cluster "curso-k8s-l2" ...
Deleted nodes: ["curso-k8s-l2-worker" "curso-k8s-l2-worker2" "curso-k8s-l2-control-plane"]
$ kind get clusters | grep -c curso-k8s-l2
0
--name е задължителен и тук: без него kind би изтрил клъстера с име kind, който може да е на друг човек или от друг проект. След това изтрий временната папка на kubeconfig (онази, която създаде mktemp, с префикса /tmp/curso-k8s-l2.) или затвори този терминал. Клъстерът за следващия урок се създава от нулата, точно като този.
Как разбирам, че съм успял
- При
ImagePullBackOffказваш коя част се е провалила (kubeletна възела) и защо съобщението споменаваdocker.io/library/…, без да го търсиш. - Проверяваш с
docker exec <nodo> crictl imagesдали даден образ е във възел на kind и го зареждаш сkind load docker-image <imagen> --name <clúster>. - Пишеш по памет скелета на един Deployment и обясняваш защо
selectorиtemplate.metadata.labelsтрябва да съвпадат. - Предвиждаш какво става при изтриване на самостоятелен Pod, на Pod на Deployment и на целия Deployment.
- Обясняваш за какво служи етикетът
pod-template-hashи защо обектът Pod от Упражнение 2 не беше осиновен. - Обосноваваш всеки ред от
securityContextнаrevisorи казваш кои изисква нивотоrestricted. - Изпълняваш
programas/02-pods-deployments/correr.sh > /tmp/mi-corrida-l2.txt 2>&1и сравняваш/tmp/mi-corrida-l2.txtсprogramas/02-pods-deployments/correr.salida.txt(не пренасочвай към този файл: той е еталонът) по онова, което не се променя между изпълненията: три възелаReadyнаv1.37.0;0образа наrevisorвъв възела преди зареждането; трите обекта Pod вImagePullBackOffи събитиетоpull access deniedсdocker.io/library/curso-docker-revisor:1.6.0; трите редаnot yet present on node … loading..., по един за всеки възел, включително контролния;deployment.apps/revisor 3/3;revisor 1.6.0 escuchando en :8080 (1 servicio(s));sanoи1.6.0;executable file not found in $PATH;securityContextв JSON;CreateContainerConfigErrorсbreaks non-root policy; две ревизии вrollout historyи старият ReplicaSet на0 0 0; и накраяsin diferencias con el archivo. Наставките в имената, IP адресите, на кой възел попада всеки Pod, възрастите, редът на редовете отkind loadи колко пъти се повтаря всеки ред отrollout statusсе променят при всяко изпълнение: наблюдават се, не се сравняват. - Накрая
kind get clustersвече не показваcurso-k8s-l2.
Обобщение
Отговори, без да поглеждаш урока:
- Защо възел на kind не вижда образите на твоя Docker и какво прави
kind load docker-image? - Каква е разликата между
ErrImagePullиImagePullBackOff? Колко най-много чакаkubeletмежду опитите? - Кои три части има
specна един Deployment и какво става, ако селекторът не съвпада с етикетите на шаблона? - Кой на кого е собственик във веригата Deployment, ReplicaSet и Pod и в кое поле се разчита това?
- Какво прави ReplicaSet с Pod, от който махнеш етикета на неговия селектор?
- Кога се задейства rollout? Защо
rollout restartзадейства такъв, ако не си променил нищо? - При три реплики и стойностите по подразбиране колко обекта Pod може да има по време на rollout и колко може да липсват?
- Какво предотвратява
runAsNonRoot: true, коетоrunAsUser: 65532вече не предотвратява? - Защо
revisorсе нуждае отemptyDirв/datos?
Източници
Всички са консултирани на 21 октомври 2026 г.
- kind, „Quick Start“, раздел „Loading an Image Into Your Cluster“:
kind load docker-image, опцията--nameза клъстери с име,crictl imagesвътре във възела и предупреждението за:latestи политиката за изтегляне. - Kubernetes, „Images“: политиките
IfNotPresent,AlwaysиNever; политиката по подразбиране според тага; Docker Hub като регистър по подразбиране (docker.io/library/…); иImagePullBackOff, с растящото изчакване до 300 секунди. - Kubernetes, „Deployments“: кога се задейства rollout;
maxSurgeиmaxUnavailable(25 %, с тяхното закръгляне);revisionHistoryLimit(10);progressDeadlineSeconds(600) иProgressDeadlineExceeded; етикетътpod-template-hash; предупреждението да не се управляват обектите ReplicaSet на Deployment и това за припокриващите се селектори. - Kubernetes, „ReplicaSet“: предназначението му; осиновяването на обекти Pod без собственик, които съвпадат със селектора му; препоръката да се използват Deployments; и как се изваждат обекти Pod от ReplicaSet чрез смяна на етикетите им.
- Kubernetes, „Pods“: защо рядко се създават отделни обекти Pod и докога един Pod остава на своя възел.
- Kubernetes, „Pod Lifecycle“:
restartPolicyеAlwaysпо подразбиране; фазатаPendingвключва изтеглянето на образите; един Pod никога не се препланира към друг възел. - Kubernetes, „Restrict a Container's Syscalls with seccomp“: без
seccompDefaultпрофилът по подразбиране еUnconfined. - Kubernetes, „Owners and Dependents“: полето
ownerReferences. - Kubernetes, „Labels and Selectors“: какво е етикет, че не е уникален, селекторите по равенство и по множества и липсата на оператор „или“.
- Kubernetes, „Volumes“, раздел
emptyDir: кога се създава, кога се изтрива иsizeLimit. - Kubernetes, „Configure a Security Context for a Pod or Container“: всяко поле на
securityContextи какво контролира. - Kubernetes, „Pod Security Standards“: нивото
restrictedи неговите правила за не-root, ескалация на привилегии, capabilities иseccomp.
Предпочитате имейл? Пишете ни на hola@habil.mx