Съдържание на курса

Урок 0 — Какво е Kubernetes и какво НЕ е kind

От · основател и главен изпълнителен директор на Hábil ·

Петък, 11:40 вечерта. Програмата revisor работи вече три седмици на един-единствен сървър, стартирана с един docker run --restart always, който някой е написал един следобед и никога не е записал никъде. В този час сървърът угасва: захранващият блок отказва. Контейнерът умира с него, а с него умира и daemon-ът на Docker — единственият, който знаеше как да го рестартира. В понеделник в девет екипът по експлоатация има нужда от отчета за състоянията за седмичната среща, а страницата е празна. Някой трябва да се събуди, да намери друга машина, да си спомни точните променливи на средата, порта, тома, версията на образа… а всичко това е било в историята на терминала на сървъра, който вече не се включва.

Проблемът не беше контейнерът. Беше това, че никой, освен един заспал човек, не знаеше в какво състояние трябва да бъде услугата. Този урок е за инструмента, който съществува, за да знае това системата, а не ти в три през нощта.

Време: 90 мин

Какво изграждаш: засега нищо. Това е урок за четене: тук се изгражда мисловната карта, с която останалата част от курса става предвидима

Какво научаваш: какъв проблем решава Kubernetes, който самият Docker не решава; контролната равнина и възлите, част по част; да декларираш желаното състояние и да оставиш клъстера да го достигне (контролният цикъл); картата от Docker към Kubernetes; какво е kind, какво доказва един клъстер на kind и какво не доказва; и какво защитава един локален клъстер и какво не

Откъде идваш. От курса по Docker от същата поредица или от еквивалентна практика: знаеш как да изградиш образ, да го пуснеш с docker run, да му подадеш променливи на средата, порт и том, и да стартираш няколко заедно с Compose. Носиш освен това и revisor, вече пакетиран: сървър на Go, който слуша на порт 8080, отговаря на GET /salud и GET /api/estados и се конфигурира чрез променливи на средата. Онова, което не носиш, е картата на начина, по който мисли един клъстер, която е различна от тази на Docker и обяснява защо командите в следващите уроци са такива, каквито са.

Какво не прави този урок. Не инсталира нищо (това е Урок 1) и не разгръща revisor (това започва в Урок 2). Съдържа реална обиколка из един клъстер на kind, с нейните изходи, за да видиш със собствените си очи онова, което урокът твърди. Не е нужно да разбираш всеки ред от всеки изход още сега: важното е какво разказват. Ако искаш да повториш обиколката, направи го след Урок 1, когато вече имаш kubectl и kind.

За изпълненията. Изходите в този урок са получени на 21 октомври 2026 г. с kind v0.33.0, kubectl v1.37.1 и образа на възел kindest/node:v1.37.0, фиксиран чрез неговия отпечатък, върху Docker Engine 29.2.1. Машината беше Mac с процесор ARM, с Docker вътре във виртуална машина с Linux (Colima), а не Linux Mint, който е референтната система на курса. Какво се променя на твоя Mint: колоната KERNEL-VERSION ще показва твоето ядро и amd64 вместо arm64, IP адресите и портовете ще са други, а имената на обектите Pod ще носят други случайни наставки. Всичко останало трябва да съвпада. Имаше и още една разлика, и тя е поучителна: на тази машина клъстер от три възела не стартира, защото лимитът за наблюдение на файлове на системата беше на 128 и се изчерпа (раздел 0.6.3 го разказва). Затова обиколката използва един-единствен възел; клъстерът от три възела на курса идва в Урок 1, който първо настройва този лимит. Пълната обиколка е в programas/00-que-es-kubernetes/: recorrido.sh я изпълнява от начало до край за около две минути, а recorrido.salida.txt е нейният изход; ejercicio3.sh и ejercicio3.salida.txt правят същото за Упражнение 3.

След урока ще можеш да

  • Обясняваш с пример защо docker run --restart always не е достатъчен, за да поддържа жива една услуга, когато машината, на която тя работи, падне.
  • Назоваваш частите на контролната равнина (kube-apiserver, etcd, kube-scheduler, kube-controller-manager) и тези на всеки възел (kubelet, kube-proxy, runtime за контейнери) и казваш какво прави всяка с едно изречение.
  • Описваш стъпка по стъпка какво се случва в клъстера между един kubectl apply и един работещ контейнер.
  • Различаваш заповед (kubectl scale) от декларация (kubectl apply -f) и предвиждаш какво става, когато се смесят.
  • Превеждаш пет понятия от Docker към техния еквивалент в Kubernetes и назоваваш едно, което няма еквивалент.
  • Казваш какво е kind, какво доказва един клъстер на kind и какво не доказва.
  • Четеш грешката The connection to the server localhost:8080 was refused и казваш какво означава.

Защо, преди как

С Docker се научи да отговаряш на въпроса „как да пусна тази програма?“. Отговорът е един образ и един docker run. Това е чудесен отговор и Kubernetes не го променя: образите, които изгради в курса по Docker, са точно тези, които ще изпълнява клъстерът.

Но има и друг въпрос, който се появява веднага щом една услуга стане важна: „кой се грижи тя да продължава да работи?“. С Docker отговорът е daemon-ът на тази машина, и то само докато тази машина е жива. Документацията на Docker го казва ясно: политиките за рестартиране, като always или unless-stopped, се прилагат за контейнерите на този daemon (Docker, „Start containers automatically“). Ако машината изчезне, не остава никой, който да помни, че там е работило нещо.

Kubernetes отговаря на втория въпрос. Казваш му в какво състояние трябва да бъде твоята услуга —„три копия на revisor, с този образ, с тази конфигурация“— и той се грижи тя да бъде такава, на машините, с които разполага, дори някоя да падне. Това променя работата ти: спираш да даваш заповеди („пусни това“, „рестартирай онова“) и започваш да пишеш описания на състоянието, което искаш, а клъстерът го преследва неуморно.

Има и втора причина да започнем с четене, а не с инсталация. Kubernetes има много части със странни имена и съобщенията, които ще ти дава в следващите уроци (Pending, ImagePullBackOff, CrashLoopBackOff), споменават тези части. Ако не знаеш кой е scheduler-ът, един Pod в състояние Pending е загадка; ако го знаеш, е следа. И трета: в този курс ще използваш kind — клъстер, който живее вътре в Docker на твоята машина. Това е отличен инструмент за учене, но ако не знаеш какво представлява, ще си мислиш, че локалният ти клъстер доказва неща, които всъщност не доказва.

Урокът има осем части, в този ред:

  1. Проблемът, който решава Kubernetes (0.1).
  2. Деклариране вместо заповядване: контролният цикъл (0.2), с реален клъстер, който поправя онова, което чупиш.
  3. Частите: контролната равнина и възлите (0.3), погледнати отвътре.
  4. Заповед срещу декларация (0.4): какво става, когато ги смесиш.
  5. Картата от Docker към Kubernetes (0.5).
  6. Какво е kind и какво НЕ е (0.6): какво доказва локалният ти клъстер и какво не.
  7. Версиите (0.7): кои използва курсът и защо.
  8. Какво предстои в останалата част от курса (0.8).

С тази цел наум, ето какво ще видиш в раздел 0.2. Един от три обекта Pod беше изтрит и секунда по-късно клъстерът вече беше създал друг на негово място, без никой да го е молил. Това е централната идея на целия урок:

$ kubectl get pods -l app=espera
NAME                      READY   STATUS    RESTARTS   AGE
espera-68b9d575f7-cprg7   1/1     Running   0          3s
espera-68b9d575f7-fjx8v   1/1     Running   0          1s
espera-68b9d575f7-gsrlf   1/1     Running   0          3s

Понятията

0.1 Проблемът, който решава Kubernetes

0.1.1 От „пускане на контейнер“ до „поддържане на услуга“

Контейнерът е процес със собствена файлова система и собствена мрежа, на една машина. Услуга, която хората използват, се нуждае от повече от това:

  • Да има няколко копия, така че падането на едно да не остави никого без услуга.
  • Тези копия да са на различни машини, така че падането на една машина също да не го направи.
  • Ако едно копие умре, някой да го замени, без да чака човек.
  • Останалите програми да го намират по име, макар копията му да сменят машината и адреса си.
  • Конфигурацията му да може да се променя без повторно изграждане на образа.
  • Нова версия да заменя старата без прекъсване на услугата. Kubernetes дава механизма (сменя копията постепенно); дали наистина няма прекъсвания зависи и от това приложението да съобщава кога е готово и да се изключва подредено (Уроци 5 и 9).

С Docker и Compose можеш да решиш някои от тези неща на една-единствена машина. Нито едно — на няколко. Kubernetes е инструментът, който решава и шестте, в група от машини, която нарича клъстер.

Официалната дефиниция е следната: Kubernetes е „преносима, разширяема платформа с отворен код за управление на контейнеризирани работни натоварвания и услуги, която улеснява както декларативната конфигурация, така и автоматизацията“ (kubernetes.io, „Overview“). Обърни внимание на думата декларативна: тя е важната и раздел 0.2 я обяснява.

Два факта, за да не те изненадат. Името идва от гръцки и означава „кормчия“ или „пилот“, а съкращението K8s идва от преброяването на осемте букви между „K“ и „s“. Google пусна проекта като отворен код през 2014 г. (и трите неща са на същата официална страница).

0.1.2 Какво не е Kubernetes

Същата официална страница посвещава раздел на онова, което Kubernetes не прави, и е добре да се прочете преди началото, защото спестява фалшиви очаквания. Наред с други неща, Kubernetes не изгражда приложението ти, нито разгръща изходен код (образа го носиш ти, както в курса по Docker), не включва в себе си бази данни или опашки за съобщения и не налага решение за дневници (логове) или за наблюдение. И изречението, което си струва най-много: Kubernetes „не е просто система за оркестрация“. Да оркестрираш, в технически смисъл, означава да изпълниш определен поток: „първо А, после Б, после В“. Kubernetes, напротив, е „набор от независими и комбинируеми контролни процеси, които непрекъснато придвижват текущото състояние към желаното състояние“.

Това последно изречение е резюмето на този урок. Останалата част го разглобява.

0.2 Деклариране вместо заповядване: контролният цикъл

0.2.1 Термостатът

Документацията на Kubernetes обяснява централната си идея с уред, който познаваш: термостат (kubernetes.io, „Controllers“). Когато настроиш температурата на 22 градуса, казваш на термостата своето желано състояние. Температурата в стаята е текущото състояние. Термостатът включва или изключва климатика, за да доближи текущото до желаното. Не му казваш „включи климатика за десет минути“; казваш му „искам 22“ и той решава какво да прави, отново и отново, докато е включен.

Това е контролен цикъл: цикъл, който не свършва, който наблюдава, сравнява и коригира. В Kubernetes всеки от тези цикли се нарича контролер: „контролерите са контролни цикли, които следят състоянието на твоя клъстер и след това правят или заявяват промени там, където е необходимо. Всеки контролер се опитва да доближи текущото състояние на клъстера до желаното състояние“.

Практическата последица е онази, която видя в началната сцена, но наопаки. С Docker знанието за това в какво състояние трябва да бъде revisor живееше в паметта на един човек. С Kubernetes то живее в клъстера, записано, а една програма го сравнява с реалността през цялото време.

0.2.2 Желано състояние, записано във файл

В Kubernetes желаното състояние се записва в манифест: YAML файл, който описва даден обект. Това е първият манифест в курса. Той иска три копия на един контейнер:

# fig00_02.deployment.yaml — un estado deseado: «quiero tres copias de este contenedor».
# La imagen es `pause`, un contenedor que no hace nada más que esperar; viene ya
# dentro del nodo de kind, así que no se descarga nada. El `revisor` llega en la
# Lección 2; aquí solo importa ver qué hace el clúster con lo que declaras.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: espera
spec:
  replicas: 3
  selector:
    matchLabels:
      app: espera
  template:
    metadata:
      labels:
        app: espera
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65535
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: espera
        image: registry.k8s.io/pause:3.10
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop: ["ALL"]

Не е нужно да го разбираш целия; Урок 2 го разглобява поле по поле. Засега — три неща. kind: Deployment казва какъв тип обект е това: един Deployment е нормалният начин да поискаш „поддържай работещи N копия на този контейнер“. replicas: 3 е желаното състояние: три копия. А template е калъпът на всяко копие: кой образ използва и с какъв етикет (app: espera) се разпознава. Двата блока securityContext казват, че контейнерът работи без да е root, без да може да придобива привилегии и с файлова система само за четене; тук са, защото в този курс всеки манифест ги носи още от първия, а Урок 2 обяснява всеки от тях.

Образът, registry.k8s.io/pause, е миниатюрен контейнер, който не прави нищо друго, освен да чака. Избран е, защото вече идва вътре във възела на kind (самият Kubernetes го използва отвътре), така че примерът не зависи от изтегляне на каквото и да било от интернет.

0.2.3 Какво прави клъстерът с твоята декларация

Манифестът се предава на клъстера с kubectl apply -f. Ето какво отговори клъстерът от обиколката:

$ kubectl apply -f fig00_02.deployment.yaml
deployment.apps/espera created

$ kubectl rollout status deployment/espera --timeout=90s
Waiting for deployment "espera" rollout to finish: 0 of 3 updated replicas are available...
Waiting for deployment "espera" rollout to finish: 1 of 3 updated replicas are available...
Waiting for deployment "espera" rollout to finish: 2 of 3 updated replicas are available...
deployment "espera" successfully rolled out

Прочети първия отговор внимателно: deployment.apps/espera created. Не казва „три контейнера са стартирани“. Казва, че е записан обектът Deployment. В този миг не е имало нито един работещ контейнер; имало е описание. Втората команда, rollout status, остава да чака и брои: нула от три налични, един, два и накрая „завършено“. Между първия и последния ред клъстерът е работил, за да може реалността да достигне онова, което си декларирал.

Сега виж какво е останало:

$ kubectl get deployment espera
NAME     READY   UP-TO-DATE   AVAILABLE   AGE
espera   3/3     3            3           1s

$ kubectl get replicaset -l app=espera
NAME                DESIRED   CURRENT   READY   AGE
espera-68b9d575f7   3         3         3       1s

$ kubectl get pods -l app=espera
NAME                      READY   STATUS    RESTARTS   AGE
espera-68b9d575f7-9gbtr   1/1     Running   0          1s
espera-68b9d575f7-cprg7   1/1     Running   0          1s
espera-68b9d575f7-gsrlf   1/1     Running   0          1s

Поиска един обект, а има три типа обекти. Това е верига и е добре да я виждаш така от първия ден:

Deployment espera            «quiero 3 copias de este molde»
   └── ReplicaSet espera-68b9d575f7       «hay que tener 3 Pods con este molde exacto»
         ├── Pod espera-68b9d575f7-9gbtr
         ├── Pod espera-68b9d575f7-cprg7
         └── Pod espera-68b9d575f7-gsrlf

Обектът Deployment е онова, което ти пишеш. Обектът ReplicaSet се създава от самия Deployment и единствената му работа е да брои: да има толкова обекти Pod, колкото казва replicas. Наставката 68b9d575f7 идентифицира калъпа; когато смениш образа в Урок 2, ще се появи друг ReplicaSet с друга наставка и така Kubernetes сменя версията постепенно, като старата обслужва, докато новата стартира. А Pod е единицата, която наистина работи: документацията го определя като „най-малката разгръщаема единица“ в Kubernetes, „група от един или повече контейнери със споделено хранилище и мрежа“ (kubernetes.io, „Pods“). Почти винаги един Pod има само един контейнер; в този курс — винаги.

Обърни внимание и на имената на обектите Pod: крайната наставка (9gbtr, cprg7…) е случайна. При твоето изпълнение те ще са други. Затова в този курс не се разчита на ръчно въвеждане на името на един Pod: търси се по неговия етикет, с -l app=espera. Когато една команда се нуждае от конкретно име, то се получава от това търсене, както прави recorrido.sh с kubectl get pods -l app=espera -o jsonpath='{.items[0].metadata.name}'.

0.2.4 Да счупим нещо нарочно

Сега тестът с термостата. Един от трите обекта Pod беше изтрит, сякаш е умрял. Скриптът получи името му чрез търсенето по етикет от по-горе; тук то вече е заместено. В твоя клъстер наставката ще е друга, така че да копираш командата както е, би дало NotFound: вземи името от собствения си kubectl get pods -l app=espera:

$ kubectl delete pod espera-68b9d575f7-9gbtr
pod "espera-68b9d575f7-9gbtr" deleted from default namespace

$ kubectl get pods -l app=espera
NAME                      READY   STATUS    RESTARTS   AGE
espera-68b9d575f7-cprg7   1/1     Running   0          3s
espera-68b9d575f7-fjx8v   1/1     Running   0          1s
espera-68b9d575f7-gsrlf   1/1     Running   0          3s

Продължават да са три. 9gbtr вече го няма, а има нов, fjx8v, на възраст една секунда. Никой не го е искал. Обектът ReplicaSet е видял, че текущото състояние (два обекта Pod) не съвпада с желаното (три), и е създал един. Клъстерът оставя следа за онова, което прави, в своите събития, и там се вижда пълната история:

$ kubectl get events --field-selector involvedObject.kind=ReplicaSet --sort-by=.lastTimestamp
LAST SEEN   TYPE     REASON             OBJECT                         MESSAGE
3s          Normal   SuccessfulCreate   replicaset/espera-68b9d575f7   Created pod: espera-68b9d575f7-9gbtr
3s          Normal   SuccessfulCreate   replicaset/espera-68b9d575f7   Created pod: espera-68b9d575f7-gsrlf
3s          Normal   SuccessfulCreate   replicaset/espera-68b9d575f7   Created pod: espera-68b9d575f7-cprg7
1s          Normal   SuccessfulCreate   replicaset/espera-68b9d575f7   Created pod: espera-68b9d575f7-fjx8v

Четири създавания: трите от началото и замяната. Събитията са първото нещо, което се гледа, когато нещо не излезе както си очаквал, и курсът ги използва във всеки урок.

Това има последица, която променя начина на работа: когато един Deployment управлява обекта Pod, изтриването на Pod не изключва нищо. Ако искаш да има по-малко копия, променяш желаното състояние. Ако искаш да няма нито едно, изтриваш обекта Deployment. Изтриването на Pod само предизвиква раждането на друг. Условието има значение: документацията препоръчва да не се създават самостоятелни обекти Pod, а самостоятелният Pod (онзи, който създаваш директно, без Deployment или друг контролер) няма кой да го наглежда, така че ако го изтриеш, той не се връща. Затова курсът създава обектите Pod чрез Deployment (или чрез Job, в Урок 8) (kubernetes.io, „Pods“).

0.2.5 А когато това, което пада, е машина

Сцената от началото не беше контейнер, който умира: беше машина, която угасва. Kubernetes покрива и това, но е добре да знаеш колко време отнема, защото не е мигновено. Когато един възел спре да отговаря, клъстерът го отбелязва като недостъпен, а обектите Pod, които са работили на него, остават разпределени там за известно време, преди да бъдат възстановени на друг възел. Това време е записано във всеки Pod и документацията казва колко е: Kubernetes автоматично добавя един толеранс за node.kubernetes.io/not-ready и друг за node.kubernetes.io/unreachable, и двата с tolerationSeconds=300, а това означава, че обектите Pod „остават прикрепени към възлите в продължение на 5 минути след откриването на някой от тези проблеми“ (kubernetes.io, „Taints and Tolerations“). Това са 300 секунди. Изчакването съществува, за да не се мести всичко заради десетсекундно прекъсване на мрежата.

Когато вече имаш разгърнат revisor, в Урок 2, можеш да го видиш в някой от своите обекти Pod (замести името с едно от твоите, което излиза от kubectl get pods). Това е реалният изход, измерен на 21 октомври 2026 г. в клъстер на kind с три възела (kindest/node:v1.37.0), върху Pod на revisor, създаден от Deployment, като тези от обиколката на курса (командата беше kubectl get pod -n revisor -l app=revisor -o jsonpath='{.items[0].spec.tolerations}', която избира първия Pod по етикет, вместо да иска името му):

$ kubectl get pod <nombre-del-pod> -o jsonpath='{.spec.tolerations}'
[{"effect":"NoExecute","key":"node.kubernetes.io/not-ready","operator":"Exists","tolerationSeconds":300},{"effect":"NoExecute","key":"node.kubernetes.io/unreachable","operator":"Exists","tolerationSeconds":300}]

Това са двата толеранса, за които говори документацията, по един за всеки ключ (not-ready и unreachable), и двата с "effect":"NoExecute", "operator":"Exists" и "tolerationSeconds":300. Нито един от двата не е в манифеста на revisor: Kubernetes ги добави сам при създаването на обекта Pod. Това измерване е в programas/08-datos-y-tareas/correr.salida.txt, в края на Упражнение 3.

Значи ли това, че услугата е паднала за пет минути? Не, ако си направил онова, на което учи този курс: да имаш няколко реплики, разпределени на различни възли. Но не е и „без никакво прекъсване“, защото има два различни срока и е добре да не ги бъркаш. Първият е откриването: контролната равнина не разбира в същата секунда, че възелът е умрял; тя изчаква --node-monitor-grace-period, който по подразбиране е 50 секунди (kubernetes.io, „kube-controller-manager“), преди да го обяви за неизправен. През това време обектът Service може да продължи да праща заявки към репликата на падналия възел и тези заявки се провалят; онези, които попаднат в репликите на другите възли, се обслужват нормално. Вторият срок са 300-те секунди от по-горе, които решават само кога се възстановява обектът Pod на друга машина, а не кога спира да получава трафик. Практическа последица: повече разпределени реплики намаляват дела на изгубените заявки, а повторните опити в клиента смекчават останалото. Курсът измерва този прозорец по-нататък, с наистина изключен възел. Внимавай с едно удобно предположение: един Deployment се грижи за това колко реплики има, а не къде отиват. По подразбиране kube-scheduler се старае да ги разпредели между възлите, но ако не може, ги струпва на един (документацията казва, че действа така, сякаш има правилото whenUnsatisfiable: ScheduleAnyway, „планирай въпреки това“). За да изискаш разпределението, манифестът трябва да го декларира с topologySpreadConstraints или с анти-афинитет (kubernetes.io, „Pod Topology Spread Constraints“); манифестът от този урок не го прави и курсът се връща към това, когато revisor трябва да преживее изключен възел. Това е една от целите на курса: да спреш работен възел и revisor да продължи да отговаря.

0.3 Частите: контролната равнина и възлите

0.3.1 Два вида машини

Един клъстер има два вида машини, които Kubernetes нарича възли:

  • Контролната равнина (control plane): машините, където живее „мозъкът“. Те пазят желаното състояние, решават къде работи всяко нещо и изпълняват контролерите.
  • Работните възли (worker nodes): машините, където работят твоите контейнери.

В продукционен клъстер контролната равнина обикновено е на три отделни машини, за да не я спре падането на една от тях. В клъстера от този урок има един-единствен възел, който прави и двете неща; в този от Урок 1 ще има един контролен и два работни.

0.3.2 Частите, една по една

Дефинициите идват от официалната страница за компонентите (kubernetes.io, „Kubernetes Components“); дясната колона е преводът към онова, което виждаш в курса.

Част Къде живее Какво прави според документацията Какво означава за теб
kube-apiserver контролна равнина „сървърът, който предоставя HTTP API на Kubernetes“ това е единствената входна точка: kubectl говори с него, както и останалите части
etcd контролна равнина „консистентно и високодостъпно хранилище от тип ключ-стойност за всички данни на API сървъра“ тук остава записан твоят манифест; ако се загуби etcd, се губи желаното състояние
kube-scheduler контролна равнина „търси обекти Pod, които все още не са разпределени към възел, и разпределя всеки към подходящ възел“ решава къде работи всеки Pod; когато не намери място, обектът Pod остава Pending (Урок 5)
kube-controller-manager контролна равнина „изпълнява контролери за реализиране на поведението на API на Kubernetes“ тук живеят термостатите: този за Deployments, този за ReplicaSets, този за възлите…
cloud-controller-manager контролна равнина, незадължителен „интегрира се с подлежащите облачни доставчици“ в kind не съществува; затова един LoadBalancer се нуждае от помощ (Урок 6)
kubelet всеки възел „грижи се обектите Pod да работят, включително техните контейнери“ агентът на всяка машина: получава обекти Pod и ги превръща в реалност
kube-proxy всеки възел, незадължителен „поддържа мрежови правила във възлите за реализиране на обектите Service“ насочва трафика, който пристига на адреса и порта на един Service, към обектите Pod, които го обслужват в този момент; името на обекта Service се разрешава от DNS на клъстера, а не от него (Урок 3)
runtime за контейнери всеки възел „софтуерът, отговорен за изпълнението на контейнери“ в kind това е containerd, а не Docker

0.3.3 Да ги видим в клъстера

Всичко това не е теория: може да се види. Компонентите на контролната равнина работят като обекти Pod в запазено пространство от имена, kube-system:

$ kubectl get pods -n kube-system
NAME                                                 READY   STATUS    RESTARTS   AGE
coredns-559f6c778d-gg7jc                             1/1     Running   0          15s
coredns-559f6c778d-wcmct                             1/1     Running   0          15s
etcd-curso-k8s-l0-control-plane                      1/1     Running   0          23s
kindnet-2cfgd                                        1/1     Running   0          15s
kube-apiserver-curso-k8s-l0-control-plane            1/1     Running   0          22s
kube-controller-manager-curso-k8s-l0-control-plane   1/1     Running   0          22s
kube-proxy-cnp6z                                     1/1     Running   0          15s
kube-scheduler-curso-k8s-l0-control-plane            1/1     Running   0          22s

Ето четири от частите от таблицата: etcd, kube-apiserver, kube-controller-manager и kube-scheduler, всяка с името на възела, залепено накрая. Тук е и kube-proxy. И има три, които таблицата не споменава: coredns (две копия), който е вътрешният DNS на клъстера и благодарение на който един Pod намира друг по име (Урок 3); и kindnet, мрежата между обектите Pod, която kind инсталира по подразбиране. Kubernetes няма собствена мрежа: нея я осигурява приставка, наречена CNI, и всяка инсталация избира своята. Ръководството за конфигурация на kind нарича своята kindnetd и обяснява как да се смени; Урок 7 се връща към това, защото от мрежата зависи дали една NetworkPolicy се прилага, или не.

Липсва една част в списъка: kubelet. Той не е Pod. Той е нормална програма на възела и се вижда, като се погледнат процесите вътре във възела:

$ docker exec curso-k8s-l0-control-plane sh -c "ps -eo comm | grep -E '^(kubelet|containerd|kube-|etcd)' | sort"
containerd
containerd-shim
containerd-shim
containerd-shim
containerd-shim
containerd-shim
containerd-shim
containerd-shim
containerd-shim
containerd-shim
etcd
kube-apiserver
kube-controller
kube-proxy
kube-scheduler
kubelet

kubelet и containerd са процеси на възела. Останалите (etcd, kube-apiserver и т.н.) са контейнери, които containerd е стартирал, а всеки containerd-shim е малък помощен процес, който наглежда контейнери. В това изпълнение са девет, по един на Pod, защото shim-ът на runc групира в един и същ процес контейнерите от един и същ Pod; но тази връзка се решава от runtime-а, така че не брой обектите Pod чрез броене на shim-ове (containerd, „Runtime v2“). Ако преброиш, списъкът на kube-system съдържа осем обекта Pod, а shim-овете са девет. Деветият Pod не е в това пространство от имена: kind инсталира и local-path-provisioner, в local-path-storage (0.6.2; Урок 8 го показва с kubectl get pods -n local-path-storage). Това е извод, защото тази обиколка не изброи kubectl get pods -A; с тази команда в твоя клъстер сметката се вижда цялата. (Името kube-controller се вижда отрязано, защото ps показва само първите петнадесет знака от името на програмата.)

0.3.4 Яйцето и кокошката

Тук има една загадка. Почти всички обекти Pod се раждат по един и същ начин: един контролер (този за ReplicaSets например) ги записва в kube-apiserver, kube-scheduler намира онези, които все още нямат възел, и избира по един за всеки Pod, а kubelet на този възел ги стартира. kube-scheduler не създава обекти Pod: той ги разпределя (kubernetes.io, „Kubernetes Scheduler“). Но самият kube-apiserver е Pod. Кой е стартирал обекта Pod на API сървъра, преди да съществува API сървърът?

Отговорът е в една папка на възела:

$ docker exec curso-k8s-l0-control-plane ls /etc/kubernetes/manifests
etcd.yaml
kube-apiserver.yaml
kube-controller-manager.yaml
kube-scheduler.yaml

Това са четири манифеста, по един за всяка част на контролната равнина. kubelet чете тази папка директно, без да пита никого, и стартира каквото намери. Тези обекти Pod се наричат статични обекти Pod (static Pods): управлява ги kubelet въз основа на файлове, а не API сървърът (kubernetes.io, „Create static Pods“). Онова, което виждаш с kubectl get pods -n kube-system, е тяхно отражение, което kubelet публикува в API, за да можеш да ги виждаш. Затова имената им завършват с името на възела и затова нямат случайната наставка на обектите Pod, които създава един ReplicaSet.

Няма да пипаш тази папка в този курс. Но вече знаеш защо контролната равнина „стартира сама“ и какво да попиташ, ако някой ден не го направи.

0.3.5 Пълното пътуване на един kubectl apply

С частите пред очите ни, ето го пътя на онова, което се случи в 0.2.3, от край до край:

  1. kubectl чете fig00_02.deployment.yaml и го изпраща на kube-apiserver по HTTPS.
  2. kube-apiserver проверява кой си и дали имаш права, валидира обекта и го записва в etcd. Отговаря deployment.apps/espera created. Дотук няма нито един контейнер.
  3. Контролерът за Deployments, вътре в kube-controller-manager, вижда нов Deployment без ReplicaSet и създава един.
  4. Контролерът за ReplicaSets вижда ReplicaSet, който иска три обекта Pod, а няма нито един, и създава три обекта Pod, все още без разпределен възел.
  5. kube-scheduler вижда три обекта Pod без възел, избира по един за всеки и го отбелязва в обекта Pod.
  6. kubelet на избрания възел вижда, че има разпределени към него обекти Pod, иска от containerd да стартира контейнерите и докладва състоянието им на kube-apiserver.
  7. kubectl rollout status, който е питал през това време, вижда трите налични обекта Pod и изписва successfully rolled out.

Забележи, че никоя част не дава заповеди на друга. Всяка следи обектите, които я интересуват, и действа, когато текущото състояние не съвпада с желаното. Ако някоя част падне за малко, като се върне, намира висящото и го свършва. Затова дизайнът издържа на сривове: няма верига от извиквания, която да се скъса по средата.

0.4 Заповед срещу декларация

0.4.1 Двата начина да говориш на клъстера

kubectl ти позволява да работиш по два начина и документацията ги разграничава внимателно (kubernetes.io, „Kubernetes Object Management“):

  • Заповеди (imperative commands): казваш на клъстера какво да направи, с глагол. kubectl create deployment, kubectl scale, kubectl delete. Те са бързи и документацията ги препоръчва „за начало или за еднократна задача“. Проблемът им: не оставят запис. Единственото нещо, което знае в какво състояние е обектът, е самият клъстер.
  • Декларации (declarative object configuration): пишеш файл с желаното състояние и го предаваш с kubectl apply -f. Файлът се пази в Git, преглежда се преди да се приложи, а ако клъстерът се загуби, се прилага отново и всичко остава същото.

Този курс използва декларации за всичко, което има значение. Заповедите се появяват само за гледане (get, describe, logs) и за експерименти, които след това се изхвърлят.

0.4.2 Какво се случва, когато се смесят

Документацията има кратко и сериозно предупреждение: „един обект на Kubernetes трябва да се управлява само с една техника; смесването на техники върху един и същ обект води до недефинирано поведение“. В обиколката те бяха смесени нарочно, за да видиш едно от тези поведения. Първо, една заповед: да се увеличат копията от три на пет.

$ kubectl scale deployment espera --replicas=5
deployment.apps/espera scaled

$ kubectl get deployment espera
NAME     READY   UP-TO-DATE   AVAILABLE   AGE
espera   3/5     5            3           3s

Клъстерът се подчини: сега иска пет (3/5 означава „три готови от пет желани“; другите две стартират). Но файлът fig00_02.deployment.yaml продължава да казва replicas: 3. Сега клъстерът и файлът не съвпадат и нищо не те предупреждава. За да се види това, съществува kubectl diff, който сравнява файла с онова, което е в клъстера, без да променя нищо:

$ kubectl diff -f fig00_02.deployment.yaml
@@ -6,14 +6,14 @@
     kubectl.kubernetes.io/last-applied-configuration: |
       {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"espera","namespace":"default"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"espera"}},"template":{"metadata":{"labels":{"app":"espera"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.10","name":"espera","securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true}}],"securityContext":{"runAsNonRoot":true,"runAsUser":65535,"seccompProfile":{"type":"RuntimeDefault"}}}}}}
   creationTimestamp: "2026-10-21T07:52:14Z"
-  generation: 2
+  generation: 3
   name: espera
   namespace: default
   resourceVersion: "587"
   uid: 0e26d2f6-3277-4f4f-9dc0-592b883974f8
 spec:
   progressDeadlineSeconds: 600
-  replicas: 5
+  replicas: 3
   revisionHistoryLimit: 10
   selector:
     matchLabels:

(Пропуснати са първите три реда от изхода, които само назовават два временни файла с пътища, променящи се при всяко изпълнение; пълни са в recorrido.salida.txt.)

Това е diff като този в Git: редът с - е онова, което е в клъстера, а редът с + е онова, което би останало, ако приложиш файла. Данните, които имат значение, са - replicas: 5 / + replicas: 3: ако приложиш файла, се губят двете копия, които заповедта добави. Редът generation е брояч, който се увеличава всеки път, когато се промени желаното състояние; а дългата анотация, last-applied-configuration, е копието, което kubectl apply пази от последния приложен от теб файл, за да знае кои полета са „негови“. (Една полезна подробност: kubectl diff завършва с код 1, когато намери разлики, според собствената му помощна информация; това не е грешка, а отговорът.)

Файлът беше приложен:

$ kubectl apply -f fig00_02.deployment.yaml
deployment.apps/espera configured

$ kubectl get deployment espera
NAME     READY   UP-TO-DATE   AVAILABLE   AGE
espera   3/3     3            3           4s

Обратно на три. Победи файлът, защото replicas е поле, което файлът декларира. Поуката не е „заповедите са лоши“; тя е, че ако имаш файл, файлът е истината, а заповед, която му противоречи, трае до следващия apply. Представи си, че тази заповед я е дал друг човек през нощта, за да издържи пик в натоварването, а на сутринта ти прилагаш файла, без да погледнеш. Затова в този курс, преди да се приложи върху нещо, което вече съществува, се гледа kubectl diff.

0.5 Картата от Docker към Kubernetes

Почти всичко, което знаеш от Docker, има еквивалент в Kubernetes. Не е идентично, но картата ти спестява половината курс:

В Docker В Kubernetes Къде се среща в курса
един образ същият образ Урок 2 (kind load docker-image)
docker run на контейнер един Pod (и почти никога сам: вътре в Deployment) Урок 2
--restart always обектът Deployment и неговият ReplicaSet, които възстановяват липсващите обекти Pod на възел, където се побират Урок 2
docker compose up --scale revisor=3 replicas: 3 в обекта Deployment Урок 2
-p 8080:8080 един Service и kubectl port-forward за тестване от твоята машина Урок 3
името на услугата в мрежата на Compose DNS на клъстера: revisor.<пространство>.svc.cluster.local (в рамките на същото пространство стига revisor) Урок 3
-e и --env-file ConfigMap и Secret Урок 4
HEALTHCHECK в Dockerfile проби (probes) за стартиране, за готовност и за живот (startup, readiness, liveness) Урок 5
--memory и --cpus (таванът); --memory-reservation и --cpu-shares (резервация и относително тегло, които не гарантират нищо) resources.limits (таванът) и resources.requests (онова, което scheduler-ът резервира при избора на възел) Урок 5
--user, --read-only, --cap-drop ALL securityContext Урок 2
именуван том един PersistentVolumeClaim Урок 8
контейнер, който се изпълнява и завършва един Job; планиран по график — един CronJob Урок 8
файлът compose.yaml YAML манифести, обединени с Kustomize Уроци 4 и 10

И има неща, които нямат еквивалент в Docker, защото имат смисъл само когато има няколко машини и няколко души: scheduler-ът, който избира машина; обектите Namespace, които разделят екипи или проекти вътре в един и същ клъстер; правата по сервизен акаунт (RBAC); мрежовите политики между обектите Pod; и автоматичното мащабиране според натоварването. Това са Уроци 7 и 9.

Един въпрос, който винаги изниква: щом възлите не използват Docker, стават ли образите, които си изградил с Docker? Да. Kubernetes престана да говори директно с Docker Engine във версия 1.24 (kubernetes.io, исторически контекст за dockershim), но това не засегна образите. Официалният блог го обяснява така: „образът, който Docker произвежда, всъщност не е специфичен за Docker образ: той е OCI образ“, а containerd знае как да го изтегли и изпълни (kubernetes.io, „Don't Panic: Kubernetes and Docker“). Обиколката го потвърждава: колоната CONTAINER-RUNTIME на възела казва containerd://2.3.4.

0.6 Какво е kind и какво НЕ е

0.6.1 Kubernetes вътре в Docker

kind е съкращение от Kubernetes IN Docker. Страницата му го описва с едно изречение: „инструмент за изпълнение на локални клъстери на Kubernetes с помощта на „възли“, които са контейнери на Docker“, и добавя, че „е проектиран основно за тестване на самия Kubernetes, но може да се използва за локална разработка или непрекъсната интеграция“ (kind.sigs.k8s.io).

Това за „възли, които са контейнери“ се вижда без трикове. Погледнато от Kubernetes, клъстерът има един възел:

$ kubectl get nodes -o wide
NAME                         STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                       KERNEL-VERSION              CONTAINER-RUNTIME
curso-k8s-l0-control-plane   Ready    control-plane   25s   v1.37.0   172.18.0.5    <none>        Debian GNU/Linux 13 (trixie)   6.8.0-100-generic (arm64)   containerd://2.3.4

А погледнато от Docker, същият този възел е контейнер:

$ docker ps --filter label=io.x-k8s.kind.cluster=curso-k8s-l0 --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES                        IMAGE                  PORTS
curso-k8s-l0-control-plane   kindest/node:v1.37.0   127.0.0.1:60004->6443/tcp

Същото име, curso-k8s-l0-control-plane. Образът kindest/node носи вътре пълна система Debian, със systemd, kubelet и containerd: това е „машина“ наужким, направена като контейнер. Вътре в нея containerd изпълнява контейнерите на твоите обекти Pod. Това са контейнери вътре в контейнер. (Филтърът --filter label=... беше използван, за да се изброят само контейнерите на този клъстер; ако на твоята машина има други контейнери, те не се показват.)

Обърни внимание на две колони от get nodes. OS-IMAGE казва Debian, макар твоята система да е Mint: това е системата на образа на възела. Но KERNEL-VERSION показва ядрото на машината, на която работи Docker, защото контейнерите не носят собствено ядро: всички използват това на хоста. Тази колона е първата следа за онова, което kind не е.

0.6.2 Какво доказва един клъстер на kind и какво не

kind изпълнява истинския Kubernetes: същия kube-apiserver, същия etcd, същия scheduler, инсталирани с kubeadm — официалния инструмент (дизайн на kind). Сред проектните му цели е да преминава всички тестове за съответствие на Kubernetes. Всичко, което научиш за API —Deployments, Services, ConfigMaps, проби (probes), права—, важи по същия начин и в продукционен клъстер.

Онова, което не е същото, е какво стои отдолу. Тази таблица е добре да ти е подръка по време на курса:

Какво изпробваш в kind Доказва ли го? Защо
Че манифестите ти са валидни и правят това, което мислиш Да това е същият API и същият код на Kubernetes
Че твоите обекти Pod се раждат наново, когато умрат Да контролерът е същият
Че правата ти (RBAC) и нивото restricted работят Да прилага ги kube-apiserver, точно както в продукция
Че услугата ти преживява падането на машина Отчасти docker stop на един възел си прилича с това, но трите „възела“ споделят твоята машина, твоя диск и твоето ядро: ако падне компютърът ти, пада всичко
Че твоята NetworkPolicy спира трафика Зависи прилага я мрежата (CNI), а не Kubernetes; Урок 7 го измерва на живо
Че услугата ти издържа дадено натоварване Не възлите си поделят твоя процесор и твоята памет; числата не се пренасят към друга машина
Че един LoadBalancer ти дава публичен IP адрес Не kind няма вграден балансьор на натоварването; трябва отделна част. Ръководството на kind предлага Cloud Provider KIND; този курс инсталира MetalLB вътре в клъстера, а Урок 6 го проверява и обяснява защо не използва другото
Че данните ти са в безопасност Не хранилището по подразбиране на kind (снабдител, наречен local-path-provisioner, който kind инсталира при създаване на клъстера) пази данните в самия възел, който е контейнер на твоята машина; Урок 8 измерва какво се губи и кога
Че контролната равнина издържа на сривове Не в този курс kind позволява няколко контролни възела, но на една-единствена машина това тества конфигурацията, а не реалната устойчивост

Честно обобщение: kind доказва, че разбираш Kubernetes; не доказва, че системата ти е готова за продукция. Урок 10 завършва тъкмо със списъка на онова, което липсва.

0.6.3 Реален пример за „споделят твоята машина“

Това, че „възлите споделят твоето ядро“, не е академична подробност и подготовката на този урок го доказа неволно. Първия път беше направен опит да се създаде клъстерът с три възела, като този от Урок 1. kind каза, че всичко е минало добре, включително реда ✓ Joining worker nodes. Но kubectl get nodes показваше само един възел, а обектът Pod на kube-proxy беше в състояние Error. В лога му пишеше:

"command failed" err="failed complete: fsnotify watcher init: too many open files"

А kubelet на работните възли, вътре в своя контейнер, спираше с:

"Failed to start cAdvisor" err="inotify_init: too many open files"

Причината: програмите в Linux, които следят за промени във файлове, използват ресурс на ядрото, наречен inotify, а ядрото има лимит за това колко наблюдатели може да отвори всеки потребител. На тази машина той беше 128 (fs.inotify.max_user_instances = 128). Всеки възел на kind стартира свой собствен kubelet и свой собствен kube-proxy, всички срещу едно и също ядро, и трите заедно изчерпаха лимита. С един-единствен възел стигна.

Ръководството за известни проблеми на kind описва точно това: Ubuntu идва с този лимит на 128, „което не е достатъчно за създаване на клъстер с много възли“, и препоръчва да се увеличи на 512 (kind, „Known Issues“). Linux Mint 22 е базиран на Ubuntu 24.04 (Linux Mint, бележки към версия 22), така че на твоята машина е вероятно да се случи същото. Затова Урок 1 настройва този лимит преди да създаде клъстера за курса. И затова този урок използва един-единствен възел.

Доказателството, че това беше причината: на същата машина лимитът беше увеличен на 512 и клъстерът от три възела беше създаден отново, без да се променя нищо друго. Този път и трите станаха готови, всеки със своя работещ kube-proxy (скриптът е tres-nodos.sh, а пълният му изход — tres-nodos.salida.txt):

$ kubectl get nodes
NAME                         STATUS   ROLES           AGE   VERSION
curso-k8s-l0-control-plane   Ready    control-plane   30s   v1.37.0
curso-k8s-l0-worker          Ready    <none>          19s   v1.37.0
curso-k8s-l0-worker2         Ready    <none>          19s   v1.37.0

Две поуки излизат оттук. Първата: три „машини“ на kind са три процеса на една-единствена машина и споделят нейните лимити. Втората, по-обща: команда, която казва „✓“, не доказва, че системата е останала в добро състояние. Гледа се състоянието (kubectl get nodes, kubectl get pods -A), преди да се приеме нещо за даденост.

0.7 Версиите

Kubernetes пуска минорна версия приблизително три пъти годишно (kubernetes.io, „Kubernetes Release Cycle“) и поддържа едновременно трите най-скорошни, всяка с около една година корекции (kubernetes.io, „Releases“). Ето какво публикуваше тази страница на 21 октомври 2026 г.:

Минорна версия Последна кръпка Край на поддръжката
1.37 1.37.1 28 октомври 2027 г.
1.36 1.36.5 28 юни 2027 г.
1.35 1.35.9 28 февруари 2027 г.
1.34 1.34.12 27 октомври 2026 г.

(Същата страница казва, че се поддържат „трите най-скорошни“, и същевременно изброява 1.34 с крайната ѝ дата; до тази дата оставаха шест дни.)

Курсът използва Kubernetes 1.37, най-скорошната. Има две различни числа, които е добре да не се бъркат:

  • Клъстерът работи с 1.37.0. Това е версията, която носи образът на възел по подразбиране на kind v0.33.0. В kubectl get nodes се появява в колоната VERSION: v1.37.0.
  • kubectl е 1.37.1. Това е инструментът на твоята машина. Официалната политика казва, че kubectl работи с API сървър с до една минорна версия по-стар или по-нов (kubernetes.io, „Version Skew Policy“): с клъстер 1.37 вършат работа kubectl 1.36, 1.37 и 1.38. Разликата в кръпката няма значение.

И още едно нещо: образът на възела се фиксира чрез неговия отпечатък. Бележките към версия v0.33.0 на kind публикуват за всяка версия на Kubernetes образа с неговия отпечатък sha256 и казват, че трябва да се използва той, за да се получи образът, изграден за тази версия на kind (kind v0.33.0). Затова конфигурационният файл на обиколката го изписва изцяло:

# fig00_01.kind.yaml — el clúster de prueba de la Lección 0.
# Un solo nodo, que hace a la vez de plano de control y de nodo de trabajo.
# La imagen del nodo va fijada por su huella (sha256), copiada de las notas de la
# versión v0.33.0 de kind. El clúster del curso (tres nodos) llega en la Lección 1.
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  image: kindest/node:v1.37.0@sha256:a1ed56cfb0e7b93589bdf97c8cd566405a265939e3620fc4f5de89adff580ae5

Таг като v1.37.0 може да бъде преместен от онзи, който публикува образа; отпечатъкът — не. Ако някой подмени образа с друг, отпечатъкът ще престане да съвпада и изтеглянето ще се провали. Това е същият навик, който Урок 1 прилага към двоичните файлове на kubectl и kind.

0.8 Какво предстои в останалата част от курса

Всичко казано дотук се прилага на практика в следващите уроци. Всеки от тях добавя по една част към revisor в клъстера:

Урок Какво се добавя Коя идея от този урок влиза в действие
1 Инсталираш kubectl и kind и създаваш клъстера от три възела настройката на inotify от 0.6.3; контексти и навикът да се проверяват
2 revisor като Deployment веригата Deployment, ReplicaSet и Pod от 0.2.3
3 Един Service и неговото име в DNS kube-proxy и coredns от 0.3.3
4 Конфигурация без повторно изграждане картата от -e към ConfigMap от 0.5
5 Проби (probes), ресурси и подредено изключване наблюдаващият kubelet и scheduler-ът, който оставя един Pod в Pending
6 Публикуване навън обектът LoadBalancer, който kind няма вграден (0.6.2)
7 Пространства, права и мрежа мрежата, която зависи от CNI (0.3.3 и 0.6.2)
8 Данни и задачи хранилището, което живее в самия възел (0.6.2)
9 Мащабиране и обновяване с измерване на всяко прекъсване друг контролер — онзи, който нагласява replicas вместо теб
10 Всичко в един-единствен kubectl apply -k файлът като истина (0.4)

Грешката, която ще видиш

Този урок има една основна грешка, която ще видиш веднага щом нещо не е свързано, и една грешка без съобщение, която е по-опасна.

Основната грешка: The connection to the server localhost:8080 was refused. В края на обиколката клъстерът беше изтрит и беше направен опит да се използва:

$ kubectl config current-context
error: current-context is not set

$ kubectl get nodes
E1021 01:52:19.535319   95580 memcache.go:381] "Couldn't get current server API group list" err="Get \"http://localhost:8080/api?timeout=32s\": dial tcp [::1]:8080: connect: connection refused"
E1021 01:52:19.535953   95580 memcache.go:381] "Couldn't get current server API group list" err="Get \"http://localhost:8080/api?timeout=32s\": dial tcp [::1]:8080: connect: connection refused"
E1021 01:52:19.536970   95580 memcache.go:381] "Couldn't get current server API group list" err="Get \"http://localhost:8080/api?timeout=32s\": dial tcp [::1]:8080: connect: connection refused"
E1021 01:52:19.537419   95580 memcache.go:381] "Couldn't get current server API group list" err="Get \"http://localhost:8080/api?timeout=32s\": dial tcp [::1]:8080: connect: connection refused"
E1021 01:52:19.538619   95580 memcache.go:381] "Couldn't get current server API group list" err="Get \"http://localhost:8080/api?timeout=32s\": dial tcp [::1]:8080: connect: connection refused"
The connection to the server localhost:8080 was refused - did you specify the right host or port?

Чете се отдолу нагоре, защото последният ред е резюмето: „връзката със сървъра localhost:8080 беше отказана; посочихте ли правилния хост или порт?“. Петте реда с E1021 са повторни опити, по един за всеки опит да се открие какви типове обекти съществуват; казват едно и също и могат да се пренебрегнат.

Следата е в първата команда: няма активен контекст. Един контекст е записът в конфигурационния файл на kubectl (файла kubeconfig), който казва с кой клъстер да се говори и с какво удостоверение. kind е добавил един при създаването на клъстера и го е премахнал при изтриването му. Без контекст kubectl не знае къде да отиде и пробва адрес по подразбиране, localhost:8080, където няма никого. Съобщението не означава „клъстерът ти падна“: то означава „не знам кой е твоят клъстер“. Поправя се, като му се зададе контекст, който в този курс винаги е онзи, създаден от kind (Урок 1).

Грешката без съобщение: грешният контекст. Сега си представи обратното: контекст има, но не е онзи, който мислиш. Компютърът ти има конфигуриран клъстер от работата, от училището или от предишен проект и точно той е активният. Набираш kubectl delete deployment revisor с мисълта за твоя kind. Клъстерът отговаря deployment.apps "revisor" deleted, без грешка, без предупреждение. Изтрил си го на друго място.

Тази грешка не може да се прочете, защото не произвежда съобщение. Тя само се предотвратява, с един навик: преди всяка команда, която променя нещо, kubectl config current-context. В обиколката отговорът беше този и едва след като беше видян, се продължи нататък:

$ kubectl config current-context
kind-curso-k8s-l0

Скриптът recorrido.sh го прави сам: преди всеки kubectl сравнява активния контекст с kind-curso-k8s-l0 и ако не съвпада, спира. Освен това дава на kind собствен и временен файл kubeconfig, така че обичайната ти конфигурация изобщо не се пипа. И изтрива само онова, което сам е създал: ако при стартирането вече съществува клъстер на име curso-k8s-l0, спира, без да го пипа, а ако се провали по средата на пътя, на излизане изтрива единствено клъстера, който това изпълнение е успяло да създаде. Урок 1 те учи да правиш същото ръчно.

Навикът за сигурност от урока: какво защитава един локален клъстер и какво не

Таблицата с навиците на курса определя за този урок само един: да знаеш какво защитава един локален клъстер и какво не. Обиколката разгледа три неща.

Първо: API сървърът слуша само на твоята собствена машина.

$ docker inspect curso-k8s-l0-control-plane --format 'privilegiado={{.HostConfig.Privileged}}  puerto_api={{json .NetworkSettings.Ports}}'
privilegiado=true  puerto_api={"6443/tcp":[{"HostIp":"127.0.0.1","HostPort":"60004"}]}

Порт 6443 на възела, където обслужва kube-apiserver, се публикува на 127.0.0.1: адреса, който е достъпен само от твоя собствен компютър. Никой в мрежата ти не може да говори с твоя клъстер. Ръководството за конфигурация на kind казва, че това може да се промени, и категорично съветва да не се прави: препоръчва да се остави 127.0.0.1 „от съображения за сигурност“ и „да не се излага kind на нищо друго освен на локалния loopback интерфейс“ (kind, „Configuration“). Какво защитава: достъпа отвън.

Второ: твоят kubeconfig е главният ключ.

$ kubectl auth whoami
ATTRIBUTE                                           VALUE
Username                                            kubernetes-admin
Groups                                              [kubeadm:cluster-admins system:authenticated]

(Редът Extra с отпечатъка на удостоверението е пропуснат тук; има го в recorrido.salida.txt.) Потребителят, с когото работиш, е kubernetes-admin, от групата kubeadm:cluster-admins: пълен администратор на клъстера. Удостоверението живее в kubeconfig, в полетата client-certificate-data и client-key-data, които kubectl config view скрива с DATA+OMITTED тъкмо защото са тайни. В локален клъстер възможната щета е по-малка, но kubeconfig си остава администраторско удостоверение, а в истински клъстер се равнява на администраторската парола: и в двата случая не се копира в чат и не се качва в хранилище. Урок 7 създава акаунт с минимални права за revisor, защото revisor не бива да има този ключ.

Трето: възелът е привилегирован контейнер. Същият docker inspect казва privilegiado=true. За да може един контейнер да се преструва на цяла машина, със свой systemd и свой собствен containerd, той се нуждае от почти всички правомощия на хоста. Това означава, че kind не е граница за сигурност: един клъстер на kind не изолира от компютъра ти онова, което работи вътре в него. Какво не защитава: твоята машина от онова, което сложиш в клъстера. Затова в този курс в kind работят само образи, които познаваш: този на revisor, този на тестовата услуга и официалните на Kubernetes.

Трите заедно дават правилото: един клъстер на kind е добре защитен навън и не защитава нищо навътре. Той е лаборатория, а не сейф.

Какво се прави погрешно

Да изтриеш Pod, за да „изключиш“ нещо. Ако го управлява Deployment, обектът ReplicaSet го възстановява за секунда, както видя в 0.2.4. За изключване се променя желаното състояние (replicas: 0) или се изтрива обектът Deployment.

Да даваш заповеди върху нещо, което се управлява с файлове. Един kubectl scale или kubectl edit върху обект, който живее в Git, трае до следващия apply, а следващият човек, който приложи файла, няма да знае, че е съществувал. Ако промяната си струва, прави се във файла.

Да прилагаш, без да погледнеш kubectl diff. Това е единственият начин да разбереш какво ще се промени, преди да се е променило. Струва една команда.

Да не проверяваш контекста. Това е грешката без съобщение от „Грешката, която ще видиш“. На машина с един-единствен клъстер изглежда излишно; щом станат два, това е разликата между експеримент и инцидент.

Да вярваш, че kind доказва продукция. Това, че услугата ти издържа в kind падането на един „възел“, не казва нищо за това как издържа падането на истинска машина, нито реалното натоварване, нито истински диск. Kind доказва, че разбираш API.

Да се доверяваш на зелената отметка. kind create cluster каза ✓ Joining worker nodes с два възела, които така и не успяха да се присъединят (0.6.3). След като се създаде нещо, се гледа неговото състояние.

Да разчиташ на името на един Pod. Наставката е случайна и се променя всеки път, когато обектът Pod се роди наново. Търси се по етикет (-l app=...) и ако някоя команда иска името, то се взима от това търсене.

Да копираш ръководство, без да погледнеш датата. Kubernetes се променя три пъти годишно. Онова, което е премахнато, остава в интернет: ръководства, които инсталират Docker вътре във възлите (dockershim беше премахнат в 1.24) или които използват ingress-nginx, който проектът Kubernetes изведе от поддръжка през март 2026 г.: без нови версии и без корекции за сигурност (kubernetes.io, „Ingress NGINX Retirement“). Урок 6 обяснява какво се използва на негово място.

Упражнения

Упражненията в този урок не изискват клъстер. Първото и второто се отговарят, като се четат изходите от урока; третото се отговаря преди да пуснеш каквото и да е и се проверява след Урок 1 с ejercicio3.sh; четвъртото се пише със свои думи. Запиши отговорите си в дневника, преди да погледнеш решенията.

Упражнение 1 — Петък вечер (проект)

Върни се към сцената от началото. За всяко от тези неща, които се загубиха със сървъра, кажи кой обект или част от Kubernetes би го запазил или решил, като използваш картата от 0.5:

  1. Да има три копия на revisor, а не едно.
  2. Когато машината падне, някой да стартира копията на друга.
  3. Променливите на средата със списъка от услуги за проверка.
  4. Портът, през който се говори с него.
  5. „Как е бил стартиран“, което живееше в историята на един терминал.

Упражнение 2 — Кой го направи?

За всеки от тези факти от обиколката кажи коя част от 0.3 е отговорна:

  1. Обектът Deployment espera беше записан и kubectl отговори created.
  2. Появи се обектът ReplicaSet espera-68b9d575f7.
  3. Всеки от трите обекта Pod беше разпределен към възела curso-k8s-l0-control-plane.
  4. Контейнерите на обектите Pod стартираха вътре във възела.
  5. При изтриването на espera-68b9d575f7-9gbtr се появи espera-68b9d575f7-fjx8v.
  6. Обектът Pod kube-apiserver-curso-k8s-l0-control-plane съществуваше преди да има API сървър, който да го създаде.

Упражнение 3 — Предвиди, преди да пуснеш

Без да пускаш нищо, предвиди какво отговаря клъстерът във всеки случай. След Урок 1 го провери с programas/00-que-es-kubernetes/ejercicio3.sh, който създава тестов клъстер, изпълнява четирите случая и го изтрива.

  1. Обектът Deployment espera има replicas: 3. Изтриваш и трите обекта Pod с kubectl delete pods -l app=espera. Колко обекта Pod има десет секунди по-късно?
  2. Изпълняваш kubectl scale deployment espera --replicas=1, а след това kubectl apply -f fig00_02.deployment.yaml. Колко реплики остават?
  3. Изпълняваш kubectl delete deployment espera. Какво става с обекта ReplicaSet и с обектите Pod?
  4. Изтриваш клъстера с kind delete cluster --name curso-k8s-l0 и изпълняваш kubectl get pods. Какво съобщение очакваш?

Упражнение 4 — Kind в пет изречения

Колежка те пита: „ако на лаптопа ми revisor в kind преживява това да изключа един възел, мога ли вече да кажа, че и в продукция ще преживее?“. Отговори ѝ с най-много пет изречения, като използваш думите контейнер, ядро, API и продукция. Без да гледаш урока.

Решения

Решение 1

  1. Три копия: полето replicas: 3 на един Deployment; обектът ReplicaSet, който той създава, е този, който брои да са три.
  2. Стартирането им на друга машина: обектът ReplicaSet възстановява липсващите обекти Pod, kube-scheduler избира жив възел за всеки от тях, а kubelet на този възел ги стартира. С изчакването от 300 секунди от 0.2.5, освен че има реплики на други възли, които продължават да обслужват междувременно.
  3. Променливите на средата: един ConfigMap (и един Secret, ако някоя е поверителна), Урок 4.
  4. Портът: един Service, Урок 3.
  5. „Как е бил стартиран“: манифестът, запазен във файл и в Git и приложен с kubectl apply. Това е отговорът, който има най-голямо значение: знанието престава да живее в човек или в терминал и остава записано. Клъстерът го пази в etcd, а ти — в своето хранилище, така че може да се възстанови дори да се загуби целият клъстер.

Решение 2

  1. kube-apiserver, който валидира обекта и го записва в etcd.
  2. Контролерът за Deployments, вътре в kube-controller-manager.
  3. kube-scheduler. (В клъстер с един-единствен възел няма много за избиране, но решението пак го взима той.)
  4. kubelet на възела, който го иска от runtime-а, containerd.
  5. Контролерът за ReplicaSets, вътре в kube-controller-manager. Остави го записано в събитието SuccessfulCreate.
  6. kubelet, въз основа на файла /etc/kubernetes/manifests/kube-apiserver.yaml: това е статичен Pod (0.3.4).

Решение 3

Четирите бяха измерени с ejercicio3.sh; пълният му изход е в ejercicio3.salida.txt.

  1. Три. Обектът ReplicaSet вижда нула обекта Pod там, където трябва да има три, и създава три нови, с нови наставки. Изтриването на всички не променя желаното състояние. При изпълнението трите изтрити бяха khb2n, phxml и srfng, а десет секунди по-късно имаше три различни: kcvgl, qbnbx и qz6pc.
  2. Три. Заповедта ги намали до една, но във файла пише replicas: 3 и apply го налага отново, точно както в 0.4.2 с петте. Ако преди apply изпълниш kubectl diff -f fig00_02.deployment.yaml, виждаш - replicas: 1 / + replicas: 3.
  3. Изтриват се и трите нива. Обектът ReplicaSet принадлежи на обекта Deployment, а обектите Pod — на обекта ReplicaSet; при изтриване на собственика Kubernetes изтрива онова, което зависи от него. Пет секунди по-късно kubectl get replicaset,pods -l app=espera отговори No resources found in default namespace. Тук услугата наистина се изключва, защото изчезва желаното състояние.
  4. The connection to the server localhost:8080 was refused - did you specify the right host or port?, предшествано от няколко реда E… от повторни опити. Kind е премахнал контекста при изтриването на клъстера, а без контекст kubectl пробва localhost:8080. Внимание: скриптът използва собствен kubeconfig, в който е съществувал само клъстерът от курса. Ако в твоя има конфигурирани други клъстери, отговорът зависи от това кой остане активен и би могъл да говориш на друг, без да знаеш: затова се проверява с kubectl config current-context, а не се предполага.

Решение 4

Един възможен отговор: „Все още не. В kind всеки възел е контейнер на твоя лаптоп, така че трите възела споделят твоя диск, твоята памет и твоето ядро: изключването на един прилича на загуба на машина, но ако падне лаптопът ти, пада всичко. Онова, което наистина си доказала, е, че конфигурацията ти на API е правилна: репликите, разпределението им и възстановяването им работят еднакво във всеки Kubernetes. За да го твърдиш за продукция, остава да го изпробваш на наистина отделни машини, с тяхната мрежа, техните дискове и тяхното натоварване. Kind доказва, че го разбираш; продукцията се доказва в продукция“.

Как разбирам, че съм успял

  • Обясняваш, със сцената от петък, защо --restart always не е достатъчен, когато машината падне.
  • Рисуваш по памет контролната равнина и един възел, със седемте им части, и казваш какво прави всяка с едно изречение.
  • Разказваш седемте стъпки от 0.3.5, от kubectl apply до работещия контейнер.
  • Знаеш защо kube-apiserver може да бъде Pod, без никой да иска от API да го създаде.
  • При kubectl scale, последван от kubectl apply, предвиждаш какъв брой реплики остава.
  • Превеждаш поне пет понятия от Docker към Kubernetes и назоваваш едно без еквивалент.
  • Казваш две неща, които един клъстер на kind доказва, и три, които не доказва.
  • При The connection to the server localhost:8080 was refused казваш какво означава, без да го търсиш.
  • След Урок 1 изпълняваш programas/00-que-es-kubernetes/recorrido.sh и сверяваш спрямо recorrido.salida.txt онова, което не се променя между изпълненията: един възел Ready; обектът Deployment в 3/3 и неговият ReplicaSet с 3 желани, 3 текущи и 3 готови; три обекта Pod в Running след изтриването на един; изходът от diff, който показва replicas: 5 срещу replicas: 3; потребителят kubernetes-admin; и крайната грешка localhost:8080. Генерираните имена, възрастите, IP адресите, портовете, събитията, напредъкът на kind и на rollout и временните пътища се променят при всяко изпълнение: те се наблюдават, не се сравняват.

Обобщение

Отговори, без да гледаш урока:

  1. На кой въпрос отговаря Docker и на кой въпрос отговаря Kubernetes?
  2. Какво е контролен цикъл? Обясни го с термостата.
  3. Каква е разликата между Deployment, ReplicaSet и Pod?
  4. Какво отговаря kubectl apply, когато създава Deployment, и защо този отговор не означава, че има работещи контейнери?
  5. Колко изчаква Kubernetes, преди да възстанови обектите Pod от възел, който е спрял да отговаря? Защо не чака нула?
  6. Какво е статичен Pod и кой го стартира?
  7. Какво става, ако дадеш заповед, която противоречи на твоя файл, и след това приложиш файла?
  8. Какво общо има ядрото на твоята машина с това, че един тривъзлов клъстер на kind не стартира?
  9. Какво защитава един клъстер на kind и какво не?

Източници

Всички са посетени на 21 октомври 2026 г.

Да обсъдим вашия проект

Предпочитате имейл? Пишете ни на hola@habil.mx