Kubernetes

Deployment — декларативне управління Pod

Від ручного управління Pod до автоматизованої оркестрації — self-healing, масштабування та декларативні оновлення

Deployment — декларативне управління Pod

Проблема: чому Pod не підходить для production

У попередніх статтях ми детально вивчили Pod — атомарну одиницю Kubernetes. Ми навчилися створювати Pod, налаштовувати контейнери, використовувати volumes, init-контейнери та sidecar-патерни. Але наприкінці статті про Pod ми зробили важливий висновок: Pod не слід створювати напряму у production.

Чому? Давайте розглянемо типовий сценарій розгортання веб-застосунку.

Сценарій: розгортання ASP.NET Core Web API

Уявіть, що ви розгортаєте ASP.NET Core Web API у Kubernetes. Ваші вимоги:

  1. Три репліки для балансування навантаження та відмовостійкості
  2. Автоматичне відновлення — якщо одна репліка падає, система має створити нову автоматично
  3. Оновлення без downtime — нова версія застосунку має розгортатись поступово, без зупинки сервісу
  4. Можливість rollback — якщо нова версія має критичний баг, потрібно швидко повернутись до попередньої версії
  5. Масштабування — можливість швидко збільшити або зменшити кількість реплік залежно від навантаження

Якби ви використовували Pod напряму, вам довелося б:

Ручне управління Pod
# Створити 3 Pod вручну
$ kubectl apply -f api-pod-1.yaml
$ kubectl apply -f api-pod-2.yaml
$ kubectl apply -f api-pod-3.yaml
# Постійно моніторити їхній стан
$ kubectl get pods -w
# Якщо один Pod впав — вручну створити новий
$ kubectl apply -f api-pod-1.yaml
# Для оновлення — видалити старі та створити нові (з downtime!)
$ kubectl delete pod api-pod-1 api-pod-2 api-pod-3
$ kubectl apply -f api-pod-1-v2.yaml
$ kubectl apply -f api-pod-2-v2.yaml
$ kubectl apply -f api-pod-3-v2.yaml

Проблеми цього підходу:

Немає self-healing

Якщо Pod падає або вузол виходить з ладу — Pod просто зникає. Kubernetes не створить новий автоматично. Вам потрібно вручну моніторити стан та відновлювати Pod.

Немає масштабування

Щоб збільшити кількість реплік з 3 до 5, потрібно вручну створити 2 нові Pod. Щоб зменшити — вручну видалити зайві. Немає автоматизації.

Downtime при оновленні

Щоб оновити версію застосунку, потрібно спочатку видалити всі старі Pod, потім створити нові. Є момент, коли жоден Pod не працює — це downtime.

Немає історії версій

Якщо нова версія має баг, немає простого способу повернутись до попередньої. Доведеться вручну змінювати маніфести та застосовувати знову.

Ручна робота

Кожна операція вимагає вашого втручання. Це не масштабується — якщо у вас 10 застосунків по 5 реплік кожен, це 50 Pod для ручного управління.

Це неприйнятно для production. Саме тому існує Deployment.

Що потрібно замість ручного управління

Нам потрібен механізм, який:

  1. Автоматично підтримує задану кількість реплік — якщо Pod падає, система сама створює новий
  2. Дозволяє декларативно описати бажаний стан — "хочу 3 репліки версії 1.0.0", а система сама досягає цього стану
  3. Виконує оновлення без downtime — поступово замінює старі Pod новими, завжди залишаючи працюючі репліки
  4. Зберігає історію версій — можна швидко повернутись до попередньої версії однією командою
  5. Легко масштабується — змінити кількість реплік можна однією командою або автоматично

Саме це і робить Deployment.


Що таке Deployment: формальне визначення

Deployment — це ресурс Kubernetes, який забезпечує декларативне управління набором ідентичних Pod. Ви описуєте бажаний стан (скільки реплік, яка версія образу, які ресурси), а Kubernetes автоматично підтримує цей стан.

Ключова ідея: Deployment працює за принципом reconciliation loop (цикл узгодження). Kubernetes постійно порівнює бажаний стан (описаний у YAML) з поточним станом (що насправді працює в кластері) та виконує дії для усунення розбіжностей.Приклад: Ви вказали replicas: 3, але працює лише 2 Pod (один впав). Deployment Controller виявляє розбіжність та створює третій Pod. Ви нічого не робите вручну — система сама усуває проблему.

Основні можливості Deployment

Self-healing

Якщо Pod видаляється, падає або вузол виходить з ладу — Deployment автоматично створює новий Pod, підтримуючи задану кількість реплік. Це відбувається без вашого втручання.

Декларативне масштабування

Змінити кількість реплік можна однією командою (kubectl scale) або редагуванням YAML. Kubernetes сам створить або видалить Pod для досягнення бажаної кількості.

Rolling Updates

Оновлення відбувається поступово: нові Pod створюються, перевіряються на готовність, і лише після цього старі видаляються. Завжди є працюючі репліки — немає downtime.

Rollback

Kubernetes зберігає історію версій Deployment (за замовчуванням 10 останніх). Повернутись до попередньої версії можна однією командою kubectl rollout undo.

Версіонування

Кожна зміна у специфікації Pod (образ, змінні оточення, ресурси) створює нову ревізію. Ви можете переглянути історію та повернутись до будь-якої версії.

Архітектура: Deployment → ReplicaSet → Pod

Deployment не створює Pod напряму. Він використовує проміжний ресурс — ReplicaSet:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Kubernetes Control Plane" #f8f9fa {
    component "Deployment Controller" as dc #e3f2fd
    component "ReplicaSet Controller" as rsc #fff3e0
}

package "Cluster" #f8f9fa {
    component "Deployment\n(myapp)" as dep #e3f2fd {
        [replicas: 3]
        [image: aspnetapp]
    }
    
    component "ReplicaSet\n(myapp-abc123)" as rs #fff3e0 {
        [replicas: 3]
        [template: aspnetapp]
    }
    
    package "Pods" #e8f5e9 {
        component "Pod 1" as p1
        component "Pod 2" as p2
        component "Pod 3" as p3
    }
}

dc --> dep : manages
dep --> rs : creates
rsc --> rs : manages
rs --> p1 : creates
rs --> p2 : creates
rs --> p3 : creates

note right of dep
    Deployment описує
    бажаний стан:
    - Скільки реплік
    - Який образ
    - Стратегія оновлення
end note

note right of rs
    ReplicaSet підтримує
    задану кількість Pod
    з певним шаблоном
end note

note right of p1
    Pod — це те,
    що насправді
    виконується
end note

@enduml

Чому потрібен ReplicaSet?

ReplicaSet відповідає за підтримку заданої кількості реплік. Його єдине завдання — гарантувати, що завжди працює рівно N Pod з певним шаблоном.

Deployment використовує ReplicaSet для реалізації rolling updates: при оновленні створюється новий ReplicaSet з новим шаблоном Pod, а старий поступово зменшується до нуля. Це дозволяє виконувати оновлення без downtime.

Важливо: Ви ніколи не створюєте ReplicaSet напряму. Завжди використовуйте Deployment, який автоматично керує ReplicaSet. Пряме створення ReplicaSet позбавляє вас можливості rolling updates та rollback.

Анатомія Deployment: структура YAML

Тепер розглянемо, як описати Deployment у YAML. Почнемо з мінімального прикладу та поступово додаватимемо поля.

Мінімальний Deployment

Найпростіший Deployment містить лише обов'язкові поля:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27

Розберемо кожне поле детально.

apiVersion
string required
Для Deployment використовується apps/v1 (не просто v1, як для Pod). Це означає, що Deployment належить до групи API apps. Ця група містить ресурси для управління застосунками: Deployment, StatefulSet, DaemonSet, ReplicaSet.
kind
string required
Тип ресурсу — Deployment. Це вказує Kubernetes, що ви створюєте саме Deployment, а не Pod або інший ресурс.
metadata.name
string required
Унікальне ім'я Deployment у межах namespace. Має відповідати DNS-стандарту: малі літери, цифри, дефіси. Максимум 253 символи. Це ім'я буде використовуватись у командах kubectl та як префікс для імен ReplicaSet та Pod.
spec.replicas
integer
Бажана кількість реплік Pod. Deployment Controller постійно підтримує саме цю кількість. Якщо Pod падає — створюється новий. Якщо реплік більше, ніж задано — зайві видаляються. Якщо не вказано — за замовчуванням створюється 1 репліка.
spec.selector
object required
Селектор для вибору Pod, якими керує цей Deployment. Критично важливо: мітки у selector.matchLabels мають точно збігатись з мітками у template.metadata.labels. Якщо мітки не збігаються — Deployment не зможе знайти свої Pod і буде постійно створювати нові.
spec.selector.matchLabels
map required
Набір міток (key-value пар), за якими Deployment ідентифікує свої Pod. Deployment вибирає всі Pod, які мають всі вказані мітки. Наприклад, app: nginx означає "вибрати всі Pod з міткою app=nginx".
spec.template
object required
Шаблон Pod, який буде створюватись Deployment. Це повна специфікація Pod (як у статті про Pod), але без полів apiVersion та kind. Всі Pod, створені цим Deployment, будуть ідентичними копіями цього шаблону.
spec.template.metadata.labels
map required
Мітки, які будуть додані до кожного створеного Pod. Мають збігатись з spec.selector.matchLabels. Це критично важливо для роботи Deployment.
spec.template.spec
object required
Специфікація Pod — контейнери, volumes, init-контейнери тощо. Це те саме поле spec, яке ви використовували при створенні Pod напряму. Всі поля з статті про Pod доступні тут.

Важливість selector та labels

Зв'язок між selector та template.metadata.labels — це найважливіша частина Deployment. Давайте розберемо детально:

spec:
  selector:
    matchLabels:
      app: nginx  # ← Deployment шукає Pod з цією міткою
  template:
    metadata:
      labels:
        app: nginx  # ← Pod створюються з цією міткою

Що відбувається:

  1. Deployment створює Pod з міткою app: nginxtemplate.metadata.labels)
  2. Deployment Controller постійно шукає Pod з міткою app: nginx (через selector.matchLabels)
  3. Якщо знайдено менше Pod, ніж replicas — створює нові
  4. Якщо знайдено більше Pod, ніж replicas — видаляє зайві

Типова помилка новачків:

spec:
  selector:
    matchLabels:
      app: nginx  # ← Шукає Pod з app=nginx
  template:
    metadata:
      labels:
        app: web  # ← Створює Pod з app=web (ПОМИЛКА!)

У цьому випадку Deployment створить Pod з міткою app: web, але шукатиме Pod з міткою app: nginx. Він не знайде жодного Pod і буде нескінченно створювати нові, бо вважатиме, що реплік недостатньо.

Критично важливо: Мітки у selector.matchLabels та template.metadata.labels мають точно збігатись. Це не рекомендація — це вимога. Kubernetes навіть не дозволить створити Deployment з неспівпадаючими мітками (отримаєте помилку валідації).

Повна специфікація Deployment

Тепер розглянемо всі доступні поля Deployment. Почнемо з базових, потім перейдемо до розширених.

Базові поля (обов'язкові та найчастіше використовувані)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  namespace: default
  labels:
    app: myapp
    version: "1.0"
  annotations:
    description: "My application deployment"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
        version: "1.0"
    spec:
      containers:
        - name: app
          image: mcr.microsoft.com/dotnet/samples:aspnetapp
          ports:
            - containerPort: 8080
          env:
            - name: ENV
              value: "production"
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
metadata.namespace
string
Namespace, у якому буде створено Deployment. Якщо не вказано — використовується default. Namespace — це спосіб ізоляції ресурсів у Kubernetes. Різні команди або проєкти можуть мати свої namespace.
metadata.labels
map
Мітки для самого Deployment (не для Pod!). Використовуються для організації та фільтрації Deployment. Наприклад, можна додати мітку team: backend та потім знайти всі Deployment команди backend через kubectl get deployments -l team=backend.
metadata.annotations
map
Анотації — це довільні метадані, які не використовуються для вибору ресурсів (на відміну від labels). Використовуються для зберігання додаткової інформації: опис, посилання на документацію, контакти відповідальної особи тощо.

Розширені поля: стратегія оновлення

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
spec.strategy
object
Стратегія оновлення Pod при зміні spec.template. Визначає, як Kubernetes замінює старі Pod новими. Є два типи: RollingUpdate (за замовчуванням) та Recreate.
spec.strategy.type
string
Тип стратегії оновлення:
  • RollingUpdate — поступове оновлення: нові Pod створюються, старі видаляються. Завжди є працюючі репліки.
  • Recreate — спочатку видаляються всі старі Pod, потім створюються нові. Є момент downtime.
spec.strategy.rollingUpdate.maxUnavailable
integer | string
Максимальна кількість Pod, які можуть бути недоступними під час оновлення. Може бути абсолютним числом (1, 2) або відсотком від replicas (25%, 50%).Приклад: Якщо replicas: 10 та maxUnavailable: 2, то під час оновлення мінімум 8 Pod мають бути доступними (10 - 2 = 8).Приклад з відсотком: Якщо replicas: 10 та maxUnavailable: 25%, то під час оновлення мінімум 7-8 Pod мають бути доступними (25% від 10 = 2.5, округлюється вниз до 2, тому 10 - 2 = 8).
spec.strategy.rollingUpdate.maxSurge
integer | string
Максимальна кількість додаткових Pod, які можуть бути створені понад replicas під час оновлення. Може бути абсолютним числом або відсотком.Приклад: Якщо replicas: 10 та maxSurge: 2, то під час оновлення максимум 12 Pod можуть існувати одночасно (10 + 2 = 12).Навіщо це потрібно: Додаткові Pod дозволяють швидше виконати оновлення. Нові Pod створюються паралельно зі старими, і лише після того, як нові стануть готовими, старі видаляються.
Як вибрати maxUnavailable та maxSurge:
  • Швидке оновлення, більше ресурсів: maxUnavailable: 0, maxSurge: 50% — створюються багато нових Pod одразу, старі видаляються лише після готовності нових. Потребує більше ресурсів (CPU, пам'ять).
  • Повільне оновлення, менше ресурсів: maxUnavailable: 25%, maxSurge: 0 — старі Pod видаляються, потім створюються нові. Економить ресурси, але оновлення триватиме довше.
  • Збалансований підхід (за замовчуванням): maxUnavailable: 25%, maxSurge: 25% — компроміс між швидкістю та ресурсами.

Розширені поля: контроль життєвого циклу

spec:
  revisionHistoryLimit: 10
  progressDeadlineSeconds: 600
  minReadySeconds: 0
  paused: false
spec.revisionHistoryLimit
integer
Кількість старих ReplicaSet, які зберігаються для можливості rollback. Кожна зміна у spec.template створює нову ревізію (новий ReplicaSet). Старі ReplicaSet зберігаються з replicas: 0 для історії. Якщо встановити 0 — rollback буде неможливий.
spec.progressDeadlineSeconds
integer
Максимальний час (у секундах), протягом якого Deployment має досягти прогресу під час оновлення. Якщо за цей час жоден новий Pod не стане готовим — оновлення вважається невдалим, і Deployment отримає статус Progressing: False з причиною ProgressDeadlineExceeded.Навіщо це потрібно: Запобігає ситуації, коли оновлення "зависає" нескінченно (наприклад, новий образ не може завантажитись або Pod не проходить health checks).
spec.minReadySeconds
integer
Мінімальний час (у секундах), протягом якого новий Pod має бути готовим (без падінь) перед тим, як він вважатиметься доступним. Це додаткова перевірка стабільності.Приклад: Якщо minReadySeconds: 30, то після того, як Pod стане Ready, Kubernetes чекатиме ще 30 секунд. Якщо за цей час Pod не впаде — він вважається доступним і оновлення продовжується. Якщо впаде — оновлення призупиняється.Навіщо це потрібно: Запобігає ситуації, коли Pod стартує успішно, але падає через кілька секунд (наприклад, через помилку підключення до бази даних).
spec.paused
boolean
Якщо true — Deployment призупинено. Зміни у spec.template не застосовуються автоматично. Використовується для ручного контролю оновлень (canary deployments). Щоб продовжити оновлення, потрібно встановити paused: false.

Повний приклад з усіма полями

Тепер об'єднаємо все, що ми вивчили, в один Deployment з усіма важливими полями:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  namespace: default
  labels:
    app: myapp
    tier: backend
    version: "1.0"
  annotations:
    description: "Production deployment for MyApp API"
    contact: "backend-team@example.com"
spec:
  # Кількість реплік
  replicas: 3
  
  # Селектор для вибору Pod
  selector:
    matchLabels:
      app: myapp
  
  # Стратегія оновлення
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  
  # Контроль життєвого циклу
  revisionHistoryLimit: 10
  progressDeadlineSeconds: 600
  minReadySeconds: 5
  
  # Шаблон Pod
  template:
    metadata:
      labels:
        app: myapp
        version: "1.0"
    spec:
      containers:
        - name: app
          image: mcr.microsoft.com/dotnet/samples:aspnetapp
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: "Production"
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"

Цей Deployment:

  • Створює 3 репліки Pod з образом mcr.microsoft.com/dotnet/samples:aspnetapp
  • Використовує rolling update з обережними налаштуваннями (максимум 1 недоступний Pod)
  • Зберігає історію 10 останніх версій для rollback
  • Чекає 5 секунд після готовності Pod перед продовженням оновлення
  • Має таймаут 600 секунд для оновлення

Створення першого Deployment

Тепер створимо реальний Deployment та подивимося, як він працює.

Крок 1: Створення YAML маніфесту

Створіть файл nginx-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80

Крок 2: Застосування маніфесту

Застосуйте маніфест до кластера:

kubectl apply
$ kubectl apply -f nginx-deployment.yaml
deployment.apps/nginx-deployment created

Крок 3: Перевірка створеного Deployment

Переглянемо список Deployment:

kubectl get deployments
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 3/3 3 3 15s

Що означають колонки:

  • NAME: Ім'я Deployment (з metadata.name)
  • READY: 3/3 — 3 з 3 реплік готові до роботи (стан Ready)
  • UP-TO-DATE: 3 — 3 репліки відповідають поточній версії шаблону (актуальний образ, змінні оточення тощо)
  • AVAILABLE: 3 — 3 репліки доступні для обслуговування трафіку (пройшли readiness probe, якщо він налаштований)
  • AGE: Час з моменту створення Deployment

Крок 4: Перевірка створених Pod

Deployment автоматично створив Pod. Подивимося на них:

kubectl get pods
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-7d6b8c9f4d-8xk2p 1/1 Running 0 20s
nginx-deployment-7d6b8c9f4d-m5n7q 1/1 Running 0 20s
nginx-deployment-7d6b8c9f4d-z9w3r 1/1 Running 0 20s

Структура імені Pod:

Ім'я Pod складається з трьох частин: nginx-deployment-7d6b8c9f4d-8xk2p

  1. nginx-deployment — ім'я Deployment
  2. 7d6b8c9f4d — хеш ReplicaSet (унікальний ідентифікатор версії шаблону Pod)
  3. 8xk2p — унікальний суфікс Pod (генерується випадково)

Це дозволяє легко ідентифікувати, до якого Deployment належить Pod, та яку версію шаблону він використовує.

Крок 5: Детальна інформація про Deployment

Переглянемо детальну інформацію:

kubectl describe deployment
$ kubectl describe deployment nginx-deployment
Name: nginx-deployment
Namespace: default
CreationTimestamp: Fri, 09 May 2026 20:20:00 +0000
Labels: app=nginx
Selector: app=nginx
Replicas: 3 desired | 3 updated | 3 total | 3 available
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx:1.27
Port: 80/TCP
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
Events:
Type Reason Age Message
---- ------ ---- -------
Normal ScalingReplicaSet 30s Scaled up replica set nginx-deployment-7d6b8c9f4d to 3

Тут ви бачите:

  • Конфігурацію Deployment (replicas, selector, strategy)
  • Шаблон Pod (образ, порти)
  • Умови (Conditions) — стан Deployment
  • Події (Events) — що відбувалося з Deployment (створення ReplicaSet, масштабування)

ReplicaSet: проміжний шар між Deployment та Pod

Ми згадували, що Deployment не створює Pod напряму. Він використовує ReplicaSet. Давайте розберемося детально, що це і навіщо.

Що таке ReplicaSet

ReplicaSet — це ресурс Kubernetes, який забезпечує підтримку заданої кількості ідентичних Pod. Його єдине завдання — гарантувати, що завжди працює рівно N Pod з певним шаблоном.

Коли ви створюєте Deployment, він автоматично створює ReplicaSet:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Користувач" as user
participant "kubectl" as kubectl
participant "API Server" as api
participant "Deployment Controller" as dc
participant "ReplicaSet Controller" as rsc
participant "Scheduler" as sched
participant "Kubelet" as kubelet

user -> kubectl: kubectl apply -f deployment.yaml
kubectl -> api: Створити Deployment
api -> dc: Подія: новий Deployment
activate dc
dc -> api: Створити ReplicaSet
deactivate dc

api -> rsc: Подія: новий ReplicaSet
activate rsc
rsc -> api: Створити Pod 1
rsc -> api: Створити Pod 2
rsc -> api: Створити Pod 3
deactivate rsc

api -> sched: Подія: нові Pod
activate sched
sched -> api: Призначити Pod вузлам
deactivate sched

api -> kubelet: Подія: Pod призначено вузлу
activate kubelet
kubelet -> kubelet: Завантажити образ
kubelet -> kubelet: Запустити контейнер
kubelet -> api: Pod Running
deactivate kubelet

note right of dc
  Deployment Controller
  створює та керує
  ReplicaSet
end note

note right of rsc
  ReplicaSet Controller
  створює та керує
  Pod
end note

@enduml

Навіщо потрібен ReplicaSet

ReplicaSet виконує дві ключові функції:

  1. Підтримка кількості реплік — якщо Pod падає, ReplicaSet створює новий
  2. Версіонування для rolling updates — при оновленні Deployment створює новий ReplicaSet, а старий залишається для rollback

Переглянемо ReplicaSet, створений нашим Deployment:

kubectl get replicasets
$ kubectl get replicasets
NAME DESIRED CURRENT READY AGE
nginx-deployment-7d6b8c9f4d 3 3 3 2m

Що означають колонки:

  • NAME: Ім'я ReplicaSet (ім'я Deployment + хеш шаблону Pod)
  • DESIRED: Бажана кількість Pod (з spec.replicas Deployment)
  • CURRENT: Поточна кількість Pod (скільки насправді існує)
  • READY: Кількість готових Pod (стан Ready)
  • AGE: Час з моменту створення ReplicaSet
Важливо: Ви ніколи не створюєте ReplicaSet напряму. Завжди використовуйте Deployment. Пряме створення ReplicaSet позбавляє вас можливості:
  • Rolling updates (поступового оновлення)
  • Rollback (повернення до попередньої версії)
  • Історії версій
ReplicaSet — це низькорівневий примітив, на якому будується Deployment. Використовуйте Deployment для управління застосунками.

Self-healing у дії

Тепер продемонструємо головну перевагу Deployment — автоматичне відновлення (self-healing).

Експеримент: видалення Pod

Видалимо один Pod вручну та подивимося, що станеться:

kubectl delete pod
$ kubectl delete pod nginx-deployment-7d6b8c9f4d-8xk2p
pod "nginx-deployment-7d6b8c9f4d-8xk2p" deleted

Одразу перевіримо список Pod:

kubectl get pods
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-7d6b8c9f4d-m5n7q 1/1 Running 0 3m
nginx-deployment-7d6b8c9f4d-z9w3r 1/1 Running 0 3m
nginx-deployment-7d6b8c9f4d-p4k8t 0/1 ContainerCreating 0 2s

Бачимо новий Pod з іменем p4k8t у стані ContainerCreating. Що сталося?

  1. Ми видалили Pod 8xk2p
  2. ReplicaSet Controller виявив, що реплік менше, ніж задано (2 < 3)
  3. ReplicaSet автоматично створив новий Pod p4k8t
  4. Scheduler призначив Pod вузлу
  5. Kubelet завантажує образ та запускає контейнер

Через кілька секунд:

kubectl get pods (через 10 секунд)
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-7d6b8c9f4d-m5n7q 1/1 Running 0 3m15s
nginx-deployment-7d6b8c9f4d-z9w3r 1/1 Running 0 3m15s
nginx-deployment-7d6b8c9f4d-p4k8t 1/1 Running 0 15s

Знову три репліки! Це і є self-healing — система сама усуває розбіжності між бажаним станом (replicas: 3) та поточним станом (2 працюючі Pod).

Reconciliation Loop (цикл узгодження):Deployment Controller та ReplicaSet Controller працюють за принципом reconciliation loop:
  1. Читають бажаний стан з API Server (spec.replicas: 3)
  2. Читають поточний стан з API Server (скільки Pod насправді працює)
  3. Порівнюють бажаний та поточний стан
  4. Якщо є розбіжності — виконують дії для їх усунення (створюють або видаляють Pod)
  5. Повторюють цикл кожні кілька секунд
Це фундаментальний принцип роботи Kubernetes — декларативне управління через постійне узгодження стану.

Візуалізація reconciliation loop

Давайте детально розглянемо, як працює цикл узгодження при видаленні Pod:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "ReplicaSet\nController" as rsc
participant "API Server" as api
participant "etcd" as etcd
participant "Scheduler" as sched
participant "Kubelet" as kubelet

== Початковий стан: 3 Pod працюють ==

rsc -> api: GET /pods?labelSelector=app=nginx
api -> etcd: Читати Pod
etcd --> api: 3 Pod (Running)
api --> rsc: 3 Pod знайдено
rsc -> rsc: Порівняти: desired=3, current=3\n✓ Стан узгоджено

== Користувач видаляє Pod ==

note over api: kubectl delete pod nginx-xxx
api -> etcd: Видалити Pod nginx-xxx
etcd --> api: OK

== Наступна ітерація reconciliation loop ==

rsc -> api: GET /pods?labelSelector=app=nginx
api -> etcd: Читати Pod
etcd --> api: 2 Pod (Running)
api --> rsc: 2 Pod знайдено
rsc -> rsc: Порівняти: desired=3, current=2\n✗ Розбіжність виявлено!

rsc -> api: POST /pods (створити новий Pod)
api -> etcd: Зберегти новий Pod
etcd --> api: OK
api --> rsc: Pod створено

== Scheduler призначає Pod вузлу ==

api -> sched: Подія: новий Pod без вузла
sched -> api: PATCH /pods/nginx-new (nodeName=node-1)
api -> etcd: Оновити Pod
etcd --> api: OK

== Kubelet запускає Pod ==

api -> kubelet: Подія: Pod призначено node-1
kubelet -> kubelet: Завантажити образ nginx:1.27
kubelet -> kubelet: Запустити контейнер
kubelet -> api: PATCH /pods/nginx-new (status=Running)
api -> etcd: Оновити статус Pod
etcd --> api: OK

== Наступна ітерація reconciliation loop ==

rsc -> api: GET /pods?labelSelector=app=nginx
api -> etcd: Читати Pod
etcd --> api: 3 Pod (Running)
api --> rsc: 3 Pod знайдено
rsc -> rsc: Порівняти: desired=3, current=3\n✓ Стан узгоджено

note right of rsc
  Reconciliation loop
  виконується кожні
  5-10 секунд
end note

@enduml

Ключові моменти:

  1. Постійний моніторинг — ReplicaSet Controller не чекає на події. Він постійно (кожні 5-10 секунд) перевіряє стан Pod.
  2. Декларативність — контролер не знає, чому Pod зникли (видалення, збій вузла, OOMKilled). Він просто бачить розбіжність між бажаним та поточним станом і усуває її.
  3. Ідемпотентність — якщо стан уже узгоджено, контролер нічого не робить. Це безпечно викликати reconciliation багато разів.
  4. Розподілена робота — ReplicaSet Controller створює Pod, Scheduler призначає вузол, Kubelet запускає контейнер. Кожен компонент відповідає за свою частину.

Масштабування Deployment

Одна з найважливіших можливостей Deployment — масштабування (scaling). Це зміна кількості реплік Pod для адаптації до навантаження.

Навіщо потрібне масштабування

Уявіть, що ваш веб-застосунок отримує різне навантаження протягом дня:

  • Ніч (02:00-06:00): 100 запитів/хвилину — достатньо 2 реплік
  • День (09:00-18:00): 1000 запитів/хвилину — потрібно 5 реплік
  • Пікове навантаження (12:00-13:00): 5000 запитів/хвилину — потрібно 10 реплік

Без масштабування вам довелося б постійно тримати 10 реплік, витрачаючи ресурси навіть коли вони не потрібні. З масштабуванням ви можете динамічно змінювати кількість реплік.

Три способи масштабування

Kubernetes надає три способи змінити кількість реплік Deployment:

1. Команда kubectl scale

Найшвидший спосіб — одна команда. Ідеально для ручного масштабування або експериментів.

2. Редагування YAML та kubectl apply

Декларативний підхід — змінюєте файл, застосовуєте. Зміни зберігаються у версійному контролі (Git).

3. Команда kubectl edit

Інтерактивне редагування — відкриває редактор з поточною конфігурацією. Зручно для швидких змін без локальних файлів.

Розглянемо кожен спосіб детально.


Спосіб 1: kubectl scale

Найпростіший спосіб — команда kubectl scale:

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

Перевіримо результат:

kubectl get pods
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-7d6b8c9f4d-m5n7q 1/1 Running 0 10m
nginx-deployment-7d6b8c9f4d-z9w3r 1/1 Running 0 10m
nginx-deployment-7d6b8c9f4d-p4k8t 1/1 Running 0 7m
nginx-deployment-7d6b8c9f4d-x2n9k 0/1 ContainerCreating 0 2s
nginx-deployment-7d6b8c9f4d-q7m4p 0/1 ContainerCreating 0 2s

Kubernetes створив 2 нові Pod (x2n9k та q7m4p) для досягнення бажаної кількості 5 реплік.

Через кілька секунд:

kubectl get pods (через 10 секунд)
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-7d6b8c9f4d-m5n7q 1/1 Running 0 10m
nginx-deployment-7d6b8c9f4d-z9w3r 1/1 Running 0 10m
nginx-deployment-7d6b8c9f4d-p4k8t 1/1 Running 0 7m
nginx-deployment-7d6b8c9f4d-x2n9k 1/1 Running 0 15s
nginx-deployment-7d6b8c9f4d-q7m4p 1/1 Running 0 15s

Тепер усі 5 реплік працюють.

Зменшення кількості реплік:

Так само легко зменшити кількість реплік:

kubectl scale (зменшення)
$ kubectl scale deployment nginx-deployment --replicas=2
deployment.apps/nginx-deployment scaled
kubectl get pods
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-7d6b8c9f4d-m5n7q 1/1 Running 0 12m
nginx-deployment-7d6b8c9f4d-z9w3r 1/1 Running 0 12m
nginx-deployment-7d6b8c9f4d-p4k8t 1/1 Terminating 0 9m
nginx-deployment-7d6b8c9f4d-x2n9k 1/1 Terminating 0 2m
nginx-deployment-7d6b8c9f4d-q7m4p 1/1 Terminating 0 2m

Kubernetes видаляє 3 зайві Pod. Статус Terminating означає, що Pod отримав сигнал завершення (SIGTERM) і має 30 секунд (за замовчуванням) для graceful shutdown.

Який Pod буде видалено першим?Kubernetes використовує наступний порядок пріоритетів при виборі Pod для видалення:
  1. Unscheduled Pod (ще не призначені вузлу) — видаляються першими
  2. Pending Pod (чекають на ресурси) — наступні
  3. Running Pod на вузлах з більшою кількістю реплік — для балансування навантаження
  4. Новіші Pod (за часом створення) — старіші Pod зберігаються, бо вони довше працюють без проблем
Це гарантує, що масштабування не порушить балансування навантаження та стабільність.

Переваги kubectl scale:

  • Швидкість — одна команда, миттєвий результат
  • Простота — не потрібно редагувати файли
  • Ідеально для експериментів — швидко збільшити/зменшити репліки для тестування

Недоліки kubectl scale:

  • Не зберігається у Git — зміна не відображена у YAML файлах
  • Можна забути — якщо пізніше застосуєте старий YAML з replicas: 3, масштабування скасується
  • Не підходить для production — краще використовувати декларативний підхід (YAML)

Спосіб 2: Редагування YAML та kubectl apply

Декларативний підхід — змінюєте YAML файл та застосовуєте його:

Крок 1: Відкрийте nginx-deployment.yaml та змініть replicas:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 7  # ← Було 3, стало 7
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80

Крок 2: Застосуйте зміни:

kubectl apply
$ kubectl apply -f nginx-deployment.yaml
deployment.apps/nginx-deployment configured

Зверніть увагу на слово configured (а не created). Це означає, що Deployment уже існував, і Kubernetes застосував зміни.

Крок 3: Перевірте результат:

kubectl get deployments
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 7/7 7 7 20m

Тепер працює 7 реплік.

Переваги kubectl apply:

  • Декларативність — YAML файл — це єдине джерело правди (single source of truth)
  • Версійний контроль — зміни зберігаються у Git, можна відстежити історію
  • Відтворюваність — можна застосувати той самий YAML на іншому кластері
  • Production-ready — рекомендований підхід для production

Недоліки kubectl apply:

  • Повільніше — потрібно редагувати файл, зберегти, застосувати
  • Потрібен локальний файл — якщо ви на іншій машині без файлів, доведеться спочатку отримати YAML
Best practice: Завжди використовуйте kubectl apply для production. Зберігайте всі YAML файли у Git. Це дозволяє:
  • Відстежувати, хто і коли змінив конфігурацію
  • Повернутись до попередньої версії через git revert
  • Автоматизувати розгортання через CI/CD (GitHub Actions, GitLab CI)
  • Мати документацію інфраструктури у вигляді коду (Infrastructure as Code)

Спосіб 3: kubectl edit

Інтерактивне редагування — відкриває редактор з поточною конфігурацією:

kubectl edit
$ kubectl edit deployment nginx-deployment
# Відкриється редактор (vim/nano) з YAML

У редакторі ви побачите повну конфігурацію Deployment (включно з полями, які додав Kubernetes автоматично). Знайдіть рядок replicas та змініть значення:

spec:
  replicas: 4  # ← Змініть на потрібне значення

Збережіть та закрийте редактор (у vim: :wq, у nano: Ctrl+O, Ctrl+X).

kubectl edit (результат)
deployment.apps/nginx-deployment edited

Kubernetes одразу застосує зміни.

Переваги kubectl edit:

  • Не потрібен локальний файл — редагуєте конфігурацію безпосередньо у кластері
  • Швидше за kubectl apply — не потрібно шукати файл, редагувати, зберігати
  • Бачите всі поля — включно з тими, що додав Kubernetes автоматично

Недоліки kubectl edit:

  • Не зберігається у Git — зміни не відображені у локальних файлах
  • Складніше для новачків — потрібно вміти користуватись vim/nano
  • Можна зробити помилку — якщо випадково змінити щось інше, можна зламати Deployment
Обережно з kubectl edit у production!kubectl edit зручний для швидких експериментів, але не рекомендується для production. Причини:
  1. Зміни не зберігаються у Git — немає історії
  2. Легко зробити помилку — випадково змінити щось інше
  3. Немає code review — зміни застосовуються одразу без перевірки колегами
Використовуйте kubectl edit лише для debugging або швидких тестів. Для production завжди використовуйте kubectl apply з YAML файлами у Git.

Порівняння способів масштабування

kubectl scale

Коли використовувати:

  • Швидкі експерименти
  • Тимчасове масштабування під час інциденту
  • Локальна розробка (Minikube)

Приклад: Раптовий сплеск трафіку — швидко збільшити репліки, поки не з'ясуєте причину.

kubectl apply

Коли використовувати:

  • Production розгортання
  • CI/CD pipelines
  • Будь-які зміни, які мають зберігатись

Приклад: Планове збільшення реплік перед маркетинговою кампанією — змінюєте YAML, робите commit, застосовуєте через CI/CD.

kubectl edit

Коли використовувати:

  • Debugging
  • Немає доступу до локальних файлів
  • Швидкі тести

Приклад: Ви на сервері без Git репозиторію, потрібно швидко змінити конфігурацію для перевірки гіпотези.


Візуалізація процесу масштабування

Давайте подивимося, що відбувається всередині Kubernetes при масштабуванні з 3 до 5 реплік:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "kubectl" as kubectl
participant "API Server" as api
participant "etcd" as etcd
participant "Deployment\nController" as dc
participant "ReplicaSet\nController" as rsc
participant "Scheduler" as sched
participant "Kubelet\n(node-1)" as kubelet1
participant "Kubelet\n(node-2)" as kubelet2

== Користувач виконує масштабування ==

kubectl -> api: PATCH /deployments/nginx-deployment\n{spec: {replicas: 5}}
api -> etcd: Оновити Deployment (replicas: 3 → 5)
etcd --> api: OK
api --> kubectl: deployment.apps/nginx-deployment scaled

== Deployment Controller виявляє зміну ==

api -> dc: Подія: Deployment змінено
activate dc
dc -> api: GET /replicasets?owner=nginx-deployment
api -> etcd: Читати ReplicaSet
etcd --> api: ReplicaSet nginx-xxx (replicas: 3)
api --> dc: ReplicaSet знайдено

dc -> api: PATCH /replicasets/nginx-xxx\n{spec: {replicas: 5}}
api -> etcd: Оновити ReplicaSet (replicas: 3 → 5)
etcd --> api: OK
deactivate dc

== ReplicaSet Controller виявляє зміну ==

api -> rsc: Подія: ReplicaSet змінено
activate rsc
rsc -> api: GET /pods?labelSelector=app=nginx
api -> etcd: Читати Pod
etcd --> api: 3 Pod (Running)
api --> rsc: 3 Pod знайдено

rsc -> rsc: Порівняти: desired=5, current=3\nПотрібно створити 2 Pod

rsc -> api: POST /pods (Pod 4)
api -> etcd: Зберегти Pod 4
etcd --> api: OK

rsc -> api: POST /pods (Pod 5)
api -> etcd: Зберегти Pod 5
etcd --> api: OK
deactivate rsc

== Scheduler призначає Pod вузлам ==

api -> sched: Подія: 2 нові Pod без вузла
activate sched
sched -> api: GET /nodes
api -> etcd: Читати вузли
etcd --> api: node-1 (CPU: 50%), node-2 (CPU: 30%)
api --> sched: 2 вузли доступні

sched -> sched: Вибрати вузли:\nPod 4 → node-2 (менше навантаження)\nPod 5 → node-1

sched -> api: PATCH /pods/nginx-xxx-4 (nodeName=node-2)
api -> etcd: Оновити Pod 4
etcd --> api: OK

sched -> api: PATCH /pods/nginx-xxx-5 (nodeName=node-1)
api -> etcd: Оновити Pod 5
etcd --> api: OK
deactivate sched

== Kubelet запускають Pod ==

api -> kubelet2: Подія: Pod 4 призначено node-2
activate kubelet2
kubelet2 -> kubelet2: Завантажити образ nginx:1.27
kubelet2 -> kubelet2: Запустити контейнер
kubelet2 -> api: PATCH /pods/nginx-xxx-4\n(status=Running)
api -> etcd: Оновити статус Pod 4
etcd --> api: OK
deactivate kubelet2

api -> kubelet1: Подія: Pod 5 призначено node-1
activate kubelet1
kubelet1 -> kubelet1: Образ вже є (кешовано)
kubelet1 -> kubelet1: Запустити контейнер
kubelet1 -> api: PATCH /pods/nginx-xxx-5\n(status=Running)
api -> etcd: Оновити статус Pod 5
etcd --> api: OK
deactivate kubelet1

note right of dc
  Весь процес займає
  5-15 секунд залежно від:
  - Швидкості завантаження образу
  - Доступності ресурсів
  - Швидкості старту застосунку
end note

@enduml

Ключові етапи масштабування:

  1. Оновлення Deployment — користувач змінює replicas через kubectl
  2. Deployment Controller — виявляє зміну та оновлює ReplicaSet
  3. ReplicaSet Controller — виявляє розбіжність (3 < 5) та створює 2 нові Pod
  4. Scheduler — призначає нові Pod вузлам з найменшим навантаженням
  5. Kubelet — завантажує образи та запускає контейнери
  6. Готовність — Pod переходять у стан Running та стають доступними

Весь процес повністю автоматичний — ви лише змінюєте одне число (replicas), а Kubernetes виконує всю роботу.


Практичний приклад: ASP.NET Core TodoApi з Deployment

Тепер створимо реальний застосунок ASP.NET Core та розгорнемо його у Kubernetes через Deployment. Це буде простий TodoApi з трьома репліками.

Архітектура застосунку

Наш TodoApi матиме наступну структуру:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Kubernetes Cluster" #f8f9fa {
    component "Deployment\n(todoapi)" as dep #e3f2fd {
        [replicas: 3]
        [image: todoapi:1.0.0]
    }
    
    component "ReplicaSet\n(todoapi-abc123)" as rs #fff3e0 {
        [replicas: 3]
    }
    
    package "Pods" #e8f5e9 {
        component "Pod 1\n10.244.1.10" as p1 {
            [todoapi:1.0.0]
            [Port: 8080]
        }
        component "Pod 2\n10.244.1.11" as p2 {
            [todoapi:1.0.0]
            [Port: 8080]
        }
        component "Pod 3\n10.244.1.12" as p3 {
            [todoapi:1.0.0]
            [Port: 8080]
        }
    }
}

actor "Користувач" as user

dep --> rs : manages
rs --> p1 : creates
rs --> p2 : creates
rs --> p3 : creates

user --> p1 : HTTP GET /todos
user --> p2 : HTTP POST /todos
user --> p3 : HTTP GET /todos/1

note right of dep
    Deployment гарантує:
    - 3 репліки завжди працюють
    - Автоматичне відновлення
    - Можливість масштабування
end note

note bottom of p1
    Кожен Pod — незалежна
    копія застосунку з власною
    пам'яттю та IP-адресою
end note

@enduml

Крок 1: Створення ASP.NET Core Minimal API

Створимо простий TodoApi з використанням ASP.NET Core Minimal API (без Entity Framework для простоти):

Структура проєкту:

TodoApi/
├── Program.cs
├── TodoApi.csproj
├── Dockerfile
└── k8s/
    └── deployment.yaml

Program.cs:

using System.Collections.Concurrent;

var builder = WebApplication.CreateBuilder(args);

// Додаємо Swagger для документації API
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// In-memory сховище (для демонстрації)
var todos = new ConcurrentDictionary<int, Todo>();
var nextId = 1;

var app = builder.Build();

// Swagger UI (доступний лише у Development)
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// Health check endpoint (для Kubernetes)
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }))
   .WithName("HealthCheck")
   .WithOpenApi();

// GET /todos - отримати всі завдання
app.MapGet("/todos", () => 
{
    var hostname = Environment.GetEnvironmentVariable("HOSTNAME") ?? "unknown";
    return Results.Ok(new 
    { 
        todos = todos.Values.ToList(),
        servedBy = hostname,
        count = todos.Count
    });
})
.WithName("GetAllTodos")
.WithOpenApi();

// GET /todos/{id} - отримати завдання за ID
app.MapGet("/todos/{id:int}", (int id) =>
{
    if (todos.TryGetValue(id, out var todo))
    {
        var hostname = Environment.GetEnvironmentVariable("HOSTNAME") ?? "unknown";
        return Results.Ok(new { todo, servedBy = hostname });
    }
    return Results.NotFound(new { error = "Todo not found", id });
})
.WithName("GetTodoById")
.WithOpenApi();

// POST /todos - створити нове завдання
app.MapPost("/todos", (CreateTodoRequest request) =>
{
    var id = Interlocked.Increment(ref nextId);
    var todo = new Todo
    {
        Id = id,
        Title = request.Title,
        IsCompleted = false,
        CreatedAt = DateTime.UtcNow
    };
    
    todos[id] = todo;
    
    var hostname = Environment.GetEnvironmentVariable("HOSTNAME") ?? "unknown";
    return Results.Created($"/todos/{id}", new { todo, servedBy = hostname });
})
.WithName("CreateTodo")
.WithOpenApi();

// PUT /todos/{id} - оновити завдання
app.MapPut("/todos/{id:int}", (int id, UpdateTodoRequest request) =>
{
    if (!todos.TryGetValue(id, out var todo))
    {
        return Results.NotFound(new { error = "Todo not found", id });
    }
    
    todo.Title = request.Title ?? todo.Title;
    todo.IsCompleted = request.IsCompleted ?? todo.IsCompleted;
    
    var hostname = Environment.GetEnvironmentVariable("HOSTNAME") ?? "unknown";
    return Results.Ok(new { todo, servedBy = hostname });
})
.WithName("UpdateTodo")
.WithOpenApi();

// DELETE /todos/{id} - видалити завдання
app.MapDelete("/todos/{id:int}", (int id) =>
{
    if (todos.TryRemove(id, out var todo))
    {
        var hostname = Environment.GetEnvironmentVariable("HOSTNAME") ?? "unknown";
        return Results.Ok(new { message = "Todo deleted", todo, servedBy = hostname });
    }
    return Results.NotFound(new { error = "Todo not found", id });
})
.WithName("DeleteTodo")
.WithOpenApi();

app.Run();

// Моделі
record Todo
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public bool IsCompleted { get; set; }
    public DateTime CreatedAt { get; set; }
}

record CreateTodoRequest(string Title);
record UpdateTodoRequest(string? Title, bool? IsCompleted);

TodoApi.csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
  </ItemGroup>

</Project>
Чому servedBy у відповідях?Кожна відповідь API містить поле servedBy з hostname Pod. Це дозволяє побачити, який саме Pod обробив запит. У Kubernetes кожен Pod має унікальне ім'я (наприклад, todoapi-7d6b8c9f4d-x2n9k), яке зберігається у змінній оточення HOSTNAME.Це корисно для демонстрації балансування навантаження — ви побачите, що різні запити обробляються різними Pod.

Крок 2: Створення Dockerfile

Створимо multi-stage Dockerfile для оптимізації розміру образу:

Dockerfile:

# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Копіюємо .csproj та відновлюємо залежності (кешування шарів)
COPY TodoApi.csproj .
RUN dotnet restore

# Копіюємо решту файлів та збираємо
COPY . .
RUN dotnet publish -c Release -o /app/publish

# Stage 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app

# Створюємо non-root користувача для безпеки
RUN addgroup --system --gid 1000 appgroup && \
    adduser --system --uid 1000 --ingroup appgroup appuser

# Копіюємо зібраний застосунок
COPY --from=build /app/publish .

# Змінюємо власника файлів
RUN chown -R appuser:appgroup /app

# Перемикаємось на non-root користувача
USER appuser

# Відкриваємо порт 8080 (non-privileged port)
EXPOSE 8080

# Налаштовуємо ASP.NET Core для прослуховування на порту 8080
ENV ASPNETCORE_URLS=http://+:8080

# Запускаємо застосунок
ENTRYPOINT ["dotnet", "TodoApi.dll"]
Чому multi-stage build?Multi-stage Dockerfile має дві стадії:
  1. Build stage — використовує dotnet/sdk:8.0 (розмір ~1.2 GB) для компіляції застосунку
  2. Runtime stage — використовує dotnet/aspnet:8.0 (розмір ~200 MB) лише з runtime
Результат: фінальний образ містить лише runtime та скомпільований застосунок, без SDK та проміжних файлів. Це зменшує розмір образу з ~1.2 GB до ~220 MB.Чому non-root користувач?Запуск контейнера від root — це ризик безпеки. Якщо зловмисник зламає застосунок, він матиме root-доступ до контейнера. Non-root користувач обмежує можливості атакуючого.Чому порт 8080, а не 80?Порти < 1024 вимагають root-привілеїв. Використання порту 8080 дозволяє запускати застосунок від non-root користувача.

Крок 3: Збірка та завантаження образу у Minikube

Тепер зберемо Docker образ та завантажимо його у Minikube:

Збірка образу
$ cd TodoApi
# Налаштовуємо Docker для використання Minikube registry
$ eval $(minikube docker-env)
# Збираємо образ
$ docker build -t todoapi:1.0.0 .
[+] Building 45.2s (15/15) FINISHED
=> [build 1/5] FROM mcr.microsoft.com/dotnet/sdk:8.0
=> [build 2/5] COPY TodoApi.csproj .
=> [build 3/5] RUN dotnet restore
=> [build 4/5] COPY . .
=> [build 5/5] RUN dotnet publish -c Release -o /app/publish
=> [runtime 1/4] FROM mcr.microsoft.com/dotnet/aspnet:8.0
=> [runtime 2/4] RUN addgroup --system --gid 1000 appgroup
=> [runtime 3/4] COPY --from=build /app/publish .
=> [runtime 4/4] RUN chown -R appuser:appgroup /app
=> exporting to image
=> => naming to docker.io/library/todoapi:1.0.0
Що робить eval $(minikube docker-env)?Ця команда налаштовує ваш локальний Docker CLI для роботи з Docker daemon всередині Minikube. Після цього всі образи, які ви збираєте, зберігаються безпосередньо у Minikube, і їх не потрібно завантажувати окремо.Без цієї команди вам довелося б:
  1. Зібрати образ локально
  2. Зберегти його у tar-файл
  3. Завантажити у Minikube через minikube image load
З eval $(minikube docker-env) образ одразу доступний у Minikube.

Перевіримо, що образ створено:

Перевірка образу
$ docker images | grep todoapi
todoapi 1.0.0 a1b2c3d4e5f6 2 minutes ago 218MB

Крок 4: Створення Deployment YAML

Створимо маніфест Deployment для TodoApi:

k8s/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: todoapi
  labels:
    app: todoapi
    version: "1.0.0"
spec:
  # Три репліки для балансування навантаження
  replicas: 3
  
  # Селектор для вибору Pod
  selector:
    matchLabels:
      app: todoapi
  
  # Шаблон Pod
  template:
    metadata:
      labels:
        app: todoapi
        version: "1.0.0"
    spec:
      containers:
        - name: todoapi
          image: todoapi:1.0.0
          imagePullPolicy: Never  # Використовувати локальний образ (для Minikube)
          
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          
          # Змінні оточення
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: "Production"
            - name: ASPNETCORE_URLS
              value: "http://+:8080"
          
          # Ресурси (requests та limits)
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          
          # Liveness probe - перевірка, чи застосунок живий
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          
          # Readiness probe - перевірка, чи застосунок готовий приймати трафік
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3

Розберемо нові поля детально.

spec.template.spec.containers[].imagePullPolicy
string
Політика завантаження образу:
  • Always — завжди завантажувати образ з registry (навіть якщо він є локально)
  • IfNotPresent — завантажувати лише якщо образу немає локально
  • Never — ніколи не завантажувати, використовувати лише локальний образ (для Minikube)
Для Minikube використовуємо Never, бо образ зібрано локально. Для production використовуйте Always або IfNotPresent.
spec.template.spec.containers[].resources
object
Ресурси CPU та пам'яті для контейнера. Має два підполя: requests (мінімум, який гарантується) та limits (максимум, який дозволено).
spec.template.spec.containers[].resources.requests
object
Мінімальні ресурси, які Kubernetes гарантує контейнеру. Scheduler використовує це значення для вибору вузла — Pod буде призначено лише вузлу, який має достатньо вільних ресурсів.Приклад:memory: "128Mi" означає, що Pod потребує мінімум 128 MiB пам'яті. Якщо жоден вузол не має 128 MiB вільної пам'яті, Pod залишиться у стані Pending.
spec.template.spec.containers[].resources.limits
object
Максимальні ресурси, які контейнер може використати. Якщо контейнер спробує використати більше:
  • CPU: контейнер буде throttled (обмежено) — він працюватиме повільніше
  • Memory: контейнер буде killed (OOMKilled — Out Of Memory Killed) та перезапущено
Приклад:memory: "256Mi" означає, що якщо контейнер спробує використати більше 256 MiB, Kubernetes його вб'є.
spec.template.spec.containers[].livenessProbe
object
Перевірка, чи застосунок живий (не завис, не deadlock). Якщо liveness probe fails (не проходить) failureThreshold разів підряд — Kubernetes перезапускає контейнер.Навіщо це потрібно: Іноді застосунок може "зависнути" — процес працює, але не відповідає на запити (deadlock, infinite loop). Liveness probe виявляє такі ситуації та перезапускає контейнер.
spec.template.spec.containers[].readinessProbe
object
Перевірка, чи застосунок готовий приймати трафік. Якщо readiness probe fails — Pod виключається з балансування навантаження (не отримує трафік), але не перезапускається.Навіщо це потрібно: Під час старту застосунку може знадобитись час для ініціалізації (підключення до БД, завантаження конфігурації). Readiness probe гарантує, що трафік надходить лише на повністю готові Pod.

Різниця між Liveness та Readiness Probe

Це важлива концепція, яку часто плутають новачки. Давайте розберемо детально:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

state "Pod Lifecycle" as plc {
    state "Container Starting" as starting #fff3e0
    state "Initializing\n(підключення до БД,\nзавантаження конфігурації)" as init #fff3e0
    state "Ready\n(приймає трафік)" as ready #e8f5e9
    state "Unhealthy\n(тимчасова проблема)" as unhealthy #ffebee
    state "Dead\n(deadlock, crash)" as dead #ffebee
    state "Restarting" as restart #fff3e0
    
    [*] --> starting
    starting --> init : Container started
    init --> ready : Readiness probe ✓
    ready --> unhealthy : Readiness probe ✗
    unhealthy --> ready : Readiness probe ✓
    ready --> dead : Liveness probe ✗\n(3 рази підряд)
    unhealthy --> dead : Liveness probe ✗\n(3 рази підряд)
    dead --> restart : Kubernetes kills container
    restart --> starting : Container restarted
}

note right of init
    Readiness probe fails
    → Pod не отримує трафік
    → Контейнер НЕ перезапускається
end note

note right of dead
    Liveness probe fails
    → Kubernetes kills container
    → Контейнер перезапускається
end note

@enduml

Приклад сценаріїв:

Сценарій 1: Старт застосунку

Що відбувається:

  1. Контейнер стартує
  2. Застосунок підключається до БД (5 секунд)
  3. Readiness probe fails → Pod не отримує трафік
  4. Підключення встановлено
  5. Readiness probe ✓ → Pod починає отримувати трафік

Результат: Трафік надходить лише після повної готовності.

Сценарій 2: Тимчасова проблема з БД

Що відбувається:

  1. Pod працює нормально
  2. БД тимчасово недоступна (мережева проблема)
  3. Readiness probe fails → Pod виключається з балансування
  4. Liveness probe ✓ → контейнер НЕ перезапускається
  5. БД знову доступна
  6. Readiness probe ✓ → Pod знову отримує трафік

Результат: Трафік не надходить на проблемний Pod, але контейнер не перезапускається (бо проблема тимчасова).

Сценарій 3: Deadlock у застосунку

Що відбувається:

  1. Pod працює нормально
  2. Deadlock у коді — застосунок завис
  3. Readiness probe fails → Pod виключається з балансування
  4. Liveness probe fails (3 рази підряд)
  5. Kubernetes kills контейнер
  6. Контейнер перезапускається
  7. Після старту readiness probe ✓ → Pod знову працює

Результат: Завислий контейнер автоматично перезапущено.

Типова помилка новачків:Використання однакових налаштувань для liveness та readiness probe:
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

readinessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
Проблема: Якщо застосунок стартує довго (наприклад, 30 секунд), liveness probe почне fails ще до завершення старту та вб'є контейнер. Контейнер перезапуститься, знову не встигне стартувати, знову буде вбитий — crash loop.Рішення: Liveness probe має більший initialDelaySeconds, ніж час старту застосунку. Readiness probe може мати менший initialDelaySeconds.
livenessProbe:
  initialDelaySeconds: 60  # Достатньо часу для старту
  periodSeconds: 10

readinessProbe:
  initialDelaySeconds: 10  # Швидше виявляє готовність
  periodSeconds: 5

Крок 5: Розгортання у Kubernetes

Тепер застосуємо маніфест:

kubectl apply
$ kubectl apply -f k8s/deployment.yaml
deployment.apps/todoapi created

Перевіримо стан Deployment:

kubectl get deployments
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
todoapi 0/3 3 0 5s

Бачимо 0/3 — жоден Pod ще не готовий. Це нормально — Pod стартують. Почекаємо кілька секунд:

kubectl get deployments (через 15 секунд)
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
todoapi 3/3 3 3 20s

Тепер усі 3 репліки готові! Переглянемо Pod:

kubectl get pods
$ kubectl get pods -l app=todoapi
NAME READY STATUS RESTARTS AGE
todoapi-7d6b8c9f4d-x2n9k 1/1 Running 0 25s
todoapi-7d6b8c9f4d-q7m4p 1/1 Running 0 25s
todoapi-7d6b8c9f4d-k5t8w 1/1 Running 0 25s
Прапорець -l app=todoapi:Це label selector — фільтрує Pod за міткою app=todoapi. Без нього ви побачите всі Pod у namespace, включно з Pod інших застосунків.Це той самий селектор, який використовує Deployment у spec.selector.matchLabels.

Переглянемо детальну інформацію про один Pod:

kubectl describe pod
$ kubectl describe pod todoapi-7d6b8c9f4d-x2n9k
Name: todoapi-7d6b8c9f4d-x2n9k
Namespace: default
Priority: 0
Service Account: default
Node: minikube/192.168.49.2
Start Time: Fri, 09 May 2026 20:35:00 +0000
Labels: app=todoapi
version=1.0.0
Status: Running
IP: 10.244.0.15
Controlled By: ReplicaSet/todoapi-7d6b8c9f4d
Containers:
todoapi:
Image: todoapi:1.0.0
Port: 8080/TCP
State: Running
Started: Fri, 09 May 2026 20:35:05 +0000
Ready: True
Restart Count: 0
Limits:
cpu: 500m
memory: 256Mi
Requests:
cpu: 100m
memory: 128Mi
Liveness: http-get http://:8080/health delay=10s timeout=5s period=10s
Readiness: http-get http://:8080/health delay=5s timeout=3s period=5s
Environment:
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_URLS: http://+:8080
Events:
Type Reason Age Message
---- ------ ---- -------
Normal Scheduled 30s Successfully assigned default/todoapi-7d6b8c9f4d-x2n9k to minikube
Normal Pulled 28s Container image "todoapi:1.0.0" already present on machine
Normal Created 28s Created container todoapi
Normal Started 27s Started container todoapi

Тут ви бачите всю інформацію про Pod: образ, порти, ресурси, probes, змінні оточення та події.

Крок 6: Тестування API через port-forward

Щоб протестувати API, використаємо kubectl port-forward для перенаправлення трафіку з локальної машини на Pod:

kubectl port-forward
$ kubectl port-forward deployment/todoapi 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

Тепер API доступний на http://localhost:8080. Відкрийте новий термінал та протестуйте:

Тестування API
# Перевірка health endpoint
$ curl http://localhost:8080/health
{"status":"healthy","timestamp":"2026-05-09T20:36:15.123Z"}
# Створення першого todo
$ curl -X POST http://localhost:8080/todos \
-H "Content-Type: application/json" \
-d '{"title":"Вивчити Kubernetes Deployment"}'
{
"todo": {
"id": 1,
"title": "Вивчити Kubernetes Deployment",
"isCompleted": false,
"createdAt": "2026-05-09T20:36:20.456Z"
},
"servedBy": "todoapi-7d6b8c9f4d-x2n9k"
}
# Створення другого todo
$ curl -X POST http://localhost:8080/todos \
-H "Content-Type: application/json" \
-d '{"title":"Протестувати масштабування"}'
{
"todo": {
"id": 2,
"title": "Протестувати масштабування",
"isCompleted": false,
"createdAt": "2026-05-09T20:36:25.789Z"
},
"servedBy": "todoapi-7d6b8c9f4d-q7m4p"
}
# Отримання всіх todos
$ curl http://localhost:8080/todos
{
"todos": [
{
"id": 1,
"title": "Вивчити Kubernetes Deployment",
"isCompleted": false,
"createdAt": "2026-05-09T20:36:20.456Z"
},
{
"id": 2,
"title": "Протестувати масштабування",
"isCompleted": false,
"createdAt": "2026-05-09T20:36:25.789Z"
}
],
"servedBy": "todoapi-7d6b8c9f4d-k5t8w",
"count": 2
}

Зверніть увагу на поле servedBy — кожен запит обробляється різним Pod! Це демонструє, що kubectl port-forward автоматично балансує навантаження між репліками.

Важливо про in-memory сховище:Наш TodoApi використовує ConcurrentDictionary для зберігання даних у пам'яті. Це означає, що кожен Pod має власне сховище. Якщо ви створите todo через Pod 1, а потім запитаєте список через Pod 2 — ви не побачите цей todo.Чому так?Кожен Pod — це окремий процес з власною пам'яттю. Вони не діляться даними між собою. У реальному застосунку ви б використовували зовнішню базу даних (PostgreSQL, MongoDB), до якої підключаються всі Pod.Демонстрація проблеми:
# Створюємо todo (обробляє Pod 1)
curl -X POST http://localhost:8080/todos -d '{"title":"Test"}'
# Відповідь: servedBy: todoapi-xxx-pod1

# Запитуємо список (обробляє Pod 2)
curl http://localhost:8080/todos
# Відповідь: servedBy: todoapi-xxx-pod2, todos: [] (порожньо!)
Це нормально для демонстрації. У наступних статтях ми додамо PostgreSQL та побачимо, як Pod діляться даними через зовнішню БД.

Крок 7: Тестування self-healing

Тепер продемонструємо self-healing — видалимо один Pod та подивимося, як Deployment автоматично створить новий:

Видалення Pod
$ kubectl delete pod todoapi-7d6b8c9f4d-x2n9k
pod "todoapi-7d6b8c9f4d-x2n9k" deleted

Одразу перевіримо список Pod:

kubectl get pods
$ kubectl get pods -l app=todoapi
NAME READY STATUS RESTARTS AGE
todoapi-7d6b8c9f4d-q7m4p 1/1 Running 0 5m
todoapi-7d6b8c9f4d-k5t8w 1/1 Running 0 5m
todoapi-7d6b8c9f4d-n8p2r 0/1 ContainerCreating 0 3s

Бачимо новий Pod n8p2r у стані ContainerCreating. Через кілька секунд:

kubectl get pods (через 10 секунд)
$ kubectl get pods -l app=todoapi
NAME READY STATUS RESTARTS AGE
todoapi-7d6b8c9f4d-q7m4p 1/1 Running 0 5m15s
todoapi-7d6b8c9f4d-k5t8w 1/1 Running 0 5m15s
todoapi-7d6b8c9f4d-n8p2r 1/1 Running 0 15s

Знову три репліки! Deployment автоматично відновив бажаний стан.

Крок 8: Тестування масштабування

Тепер збільшимо кількість реплік з 3 до 5:

kubectl scale
$ kubectl scale deployment todoapi --replicas=5
deployment.apps/todoapi scaled
kubectl get pods
$ kubectl get pods -l app=todoapi
NAME READY STATUS RESTARTS AGE
todoapi-7d6b8c9f4d-q7m4p 1/1 Running 0 6m
todoapi-7d6b8c9f4d-k5t8w 1/1 Running 0 6m
todoapi-7d6b8c9f4d-n8p2r 1/1 Running 0 1m
todoapi-7d6b8c9f4d-m7q3s 0/1 ContainerCreating 0 2s
todoapi-7d6b8c9f4d-p9k4t 0/1 ContainerCreating 0 2s

Два нові Pod створюються. Через 10-15 секунд:

kubectl get pods (після масштабування)
$ kubectl get pods -l app=todoapi
NAME READY STATUS RESTARTS AGE
todoapi-7d6b8c9f4d-q7m4p 1/1 Running 0 6m20s
todoapi-7d6b8c9f4d-k5t8w 1/1 Running 0 6m20s
todoapi-7d6b8c9f4d-n8p2r 1/1 Running 0 1m20s
todoapi-7d6b8c9f4d-m7q3s 1/1 Running 0 20s
todoapi-7d6b8c9f4d-p9k4t 1/1 Running 0 20s

Тепер працює 5 реплік! Зменшимо назад до 3:

kubectl scale (зменшення)
$ kubectl scale deployment todoapi --replicas=3
deployment.apps/todoapi scaled
kubectl get pods
$ kubectl get pods -l app=todoapi
NAME READY STATUS RESTARTS AGE
todoapi-7d6b8c9f4d-q7m4p 1/1 Running 0 7m
todoapi-7d6b8c9f4d-k5t8w 1/1 Running 0 7m
todoapi-7d6b8c9f4d-n8p2r 1/1 Running 0 2m
todoapi-7d6b8c9f4d-m7q3s 1/1 Terminating 0 1m
todoapi-7d6b8c9f4d-p9k4t 1/1 Terminating 0 1m

Два Pod видаляються (статус Terminating). Kubernetes вибрав найновіші Pod для видалення.

Крок 9: Моніторинг ресурсів

Переглянемо, скільки ресурсів використовують Pod:

kubectl top pods
$ kubectl top pods -l app=todoapi
NAME CPU(cores) MEMORY(bytes)
todoapi-7d6b8c9f4d-q7m4p 2m 45Mi
todoapi-7d6b8c9f4d-k5t8w 1m 43Mi
todoapi-7d6b8c9f4d-n8p2r 2m 44Mi
Якщо команда не працює:kubectl top вимагає Metrics Server. У Minikube його можна увімкнути:
minikube addons enable metrics-server
Почекайте 1-2 хвилини, поки Metrics Server зібре дані, потім спробуйте знову.

Бачимо, що кожен Pod використовує ~2m CPU (0.002 cores) та ~45 MiB пам'яті. Це значно менше за наші limits (500m CPU, 256Mi memory), тому Pod працюють без обмежень.

Крок 10: Перегляд логів

Переглянемо логи одного з Pod:

kubectl logs
$ kubectl logs todoapi-7d6b8c9f4d-q7m4p
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:8080
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app

Якщо хочете стежити за логами в реальному часі (як tail -f):

kubectl logs -f
$ kubectl logs -f todoapi-7d6b8c9f4d-q7m4p
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
# Тут з'являтимуться нові логи в реальному часі

Щоб переглянути логи всіх Pod одночасно:

kubectl logs (всі Pod)
$ kubectl logs -l app=todoapi --tail=10
todoapi-7d6b8c9f4d-q7m4p:
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
todoapi-7d6b8c9f4d-k5t8w:
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
todoapi-7d6b8c9f4d-n8p2r:
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.

Прапорець --tail=10 показує лише останні 10 рядків з кожного Pod.


Очищення ресурсів

Після експериментів видалимо Deployment:

kubectl delete
$ kubectl delete deployment todoapi
deployment.apps "todoapi" deleted

Це автоматично видалить:

  • Deployment
  • ReplicaSet (створений Deployment)
  • Всі Pod (створені ReplicaSet)

Перевіримо:

kubectl get all
$ kubectl get all -l app=todoapi
No resources found in default namespace.

Все видалено!

Альтернативний спосіб — видалення через файл:Якщо ви створювали ресурси через kubectl apply -f deployment.yaml, можете видалити їх тим самим файлом:
kubectl delete -f k8s/deployment.yaml
Це видалить всі ресурси, описані у файлі. Зручно, якщо у файлі кілька ресурсів (Deployment, Service, ConfigMap тощо).

Практичні завдання

Тепер, коли ви розумієте основи Deployment, виконайте наступні завдання для закріплення знань:

Завдання 1: Deployment з різними образами

Мета: Навчитись створювати Deployment з різними образами та порівнювати їх поведінку.

Завдання:

  1. Створіть два Deployment:
    • nginx-deployment з образом nginx:1.27 (3 репліки)
    • httpd-deployment з образом httpd:2.4 (2 репліки)
  2. Перевірте, що всі Pod працюють
  3. Використайте kubectl port-forward для доступу до кожного Deployment та порівняйте відповіді (nginx показує "Welcome to nginx!", httpd показує "It works!")
  4. Видаліть один Pod з кожного Deployment та переконайтесь, що вони автоматично відновлюються

Очікуваний результат:

kubectl get deployments
# NAME               READY   UP-TO-DATE   AVAILABLE   AGE
# nginx-deployment   3/3     3            3           2m
# httpd-deployment   2/2     2            2           2m

kubectl get pods
# NAME                                READY   STATUS    RESTARTS   AGE
# nginx-deployment-xxx-yyy            1/1     Running   0          2m
# nginx-deployment-xxx-zzz            1/1     Running   0          2m
# nginx-deployment-xxx-www            1/1     Running   0          2m
# httpd-deployment-aaa-bbb            1/1     Running   0          2m
# httpd-deployment-aaa-ccc            1/1     Running   0          2m

Завдання 2: Експерименти з ресурсами

Мета: Зрозуміти, як працюють resource requests та limits.

Завдання:

  1. Створіть Deployment з дуже низькими limits:
    resources:
      limits:
        memory: "10Mi"
        cpu: "10m"
    
  2. Спостерігайте, що відбувається з Pod (можливо, OOMKilled)
  3. Поступово збільшуйте limits, поки Pod не стане стабільним
  4. Використайте kubectl top pods для моніторингу реального споживання ресурсів

Очікуваний результат:

Ви побачите, як Pod з недостатніми ресурсами постійно перезапускаються (CrashLoopBackOff або OOMKilled), а після збільшення limits стають стабільними.


Завдання 3: Масштабування під навантаженням

Мета: Навчитись масштабувати застосунок залежно від навантаження.

Завдання:

  1. Розгорніть TodoApi з 2 репліками
  2. Використайте kubectl port-forward та інструмент навантаження (наприклад, ab або wrk) для генерації трафіку
  3. Під час навантаження збільште кількість реплік до 5
  4. Спостерігайте за розподілом навантаження через логи (kubectl logs)
  5. Після завершення тесту зменшіть репліки назад до 2

Очікуваний результат:

Ви побачите, як нові Pod створюються під час навантаження та починають обробляти запити, розподіляючи навантаження між усіма репліками.


Завдання 4: Налаштування Health Checks

Мета: Навчитись правильно налаштовувати liveness та readiness probes.

Завдання:

  1. Створіть Deployment з некоректними health checks (занадто короткий initialDelaySeconds)
  2. Спостерігайте за CrashLoopBackOff
  3. Виправте налаштування та досягніть стабільного стану
  4. Експериментуйте з різними значеннями periodSeconds, timeoutSeconds, failureThreshold

Очікуваний результат:

Ви зрозумієте, як неправильні health checks можуть призвести до постійних перезапусків Pod, та навчитесь підбирати оптимальні значення.


Завдання 5: Декларативне управління через Git

Мета: Навчитись використовувати Git для версійного контролю Kubernetes маніфестів.

Завдання:

  1. Створіть Git репозиторій для Kubernetes маніфестів
  2. Додайте Deployment YAML з 3 репліками
  3. Зробіть commit та застосуйте через kubectl apply
  4. Змініть кількість реплік на 5 у YAML, зробіть commit
  5. Застосуйте зміни через kubectl apply
  6. Використайте git log для перегляду історії змін
  7. Поверніться до попередньої версії через git revert та застосуйте

Очікуваний результат:

Ви матимете повну історію змін у Git, зможете відстежувати, хто і коли змінював конфігурацію, та легко повертатись до попередніх версій.


Резюме

У цій статті ми детально вивчили Deployment — основний ресурс Kubernetes для управління застосунками у production. Ось що ми розглянули:

Проблема ручного управління Pod

Чому Pod не підходить для production: немає self-healing, масштабування, rolling updates та історії версій. Кожна операція вимагає ручного втручання.

Що таке Deployment

Декларативний ресурс для управління набором ідентичних Pod. Працює за принципом reconciliation loop — постійно узгоджує бажаний стан з поточним.

Архітектура: Deployment → ReplicaSet → Pod

Deployment створює ReplicaSet, який створює Pod. Це дозволяє реалізувати rolling updates та rollback через управління кількома ReplicaSet.

Анатомія YAML

Повна специфікація Deployment: metadata, selector, template, strategy, resources, probes. Кожне поле детально розібрано з прикладами.

Self-healing

Автоматичне відновлення Pod при падінні або видаленні. ReplicaSet Controller постійно підтримує задану кількість реплік без вашого втручання.

Масштабування (3 способи)

kubectl scale (швидко), kubectl apply (декларативно), kubectl edit (інтерактивно). Кожен спосіб має свої переваги та недоліки.

Практичний приклад: TodoApi

Повний цикл розробки: ASP.NET Core Minimal API, Dockerfile, Deployment YAML, розгортання у Minikube, тестування через port-forward.

Health Checks

Liveness probe (перевірка живості) та readiness probe (перевірка готовності). Різниця між ними та правильні налаштування для уникнення CrashLoopBackOff.

Resource Management

Requests (гарантовані ресурси) та limits (максимальні ресурси). Що відбувається при перевищенні: CPU throttling та OOMKilled.

Практичні завдання

5 завдань для закріплення знань: різні образи, експерименти з ресурсами, масштабування під навантаженням, налаштування health checks, Git workflow.

Ключові висновки

  1. Завжди використовуйте Deployment у production — ніколи не створюйте Pod напряму. Deployment надає self-healing, масштабування та rolling updates.
  2. Декларативний підхід — описуйте бажаний стан у YAML, а Kubernetes автоматично досягає його. Зберігайте YAML у Git для версійного контролю.
  3. Reconciliation loop — фундаментальний принцип Kubernetes. Контролери постійно порівнюють бажаний стан з поточним та усувають розбіжності.
  4. ReplicaSet — проміжний шар — Deployment не створює Pod напряму. Він використовує ReplicaSet для підтримки кількості реплік та реалізації rolling updates.
  5. Health checks критично важливі — правильно налаштовані liveness та readiness probes гарантують стабільність застосунку. Неправильні налаштування призводять до CrashLoopBackOff.
  6. Resource requests та limits — завжди вказуйте ресурси для Pod. Requests використовуються для scheduling, limits запобігають перевитраті ресурсів.
  7. Масштабування — це просто — змінити кількість реплік можна однією командою або редагуванням YAML. Kubernetes автоматично створює або видаляє Pod.

Що далі?

У наступній статті "Rolling Updates та управління життєвим циклом Deployment" ми детально розглянемо:

  • Як працює rolling update (покрокова візуалізація)
  • Стратегії оновлення: RollingUpdate vs Recreate
  • Параметри: maxSurge, maxUnavailable, progressDeadlineSeconds, minReadySeconds (з математичними розрахунками)
  • Rollback та історія версій
  • Оновлення TodoApi з v1.0 на v2.0 з реальними змінами в коді
  • Troubleshooting типових проблем

Ви навчитесь виконувати оновлення застосунків без downtime та швидко повертатись до попередніх версій у разі проблем.


Корисні команди

Для швидкого доступу — всі команди, які ми використовували у цій статті:

# Створення Deployment
kubectl apply -f deployment.yaml

# Перегляд Deployment
kubectl get deployments
kubectl get deploy  # скорочена форма

# Детальна інформація
kubectl describe deployment <name>

# Перегляд Pod
kubectl get pods
kubectl get pods -l app=myapp  # з фільтром за міткою

# Детальна інформація про Pod
kubectl describe pod <pod-name>

Додаткові ресурси

Офіційна документація Kubernetes

Повна документація про Deployment з усіма полями та прикладами.

API Reference: Deployment

Детальна специфікація API для Deployment v1 (apps/v1).

Best Practices: Resource Management

Рекомендації щодо налаштування resource requests та limits.

Configure Liveness, Readiness and Startup Probes

Детальний гайд з налаштування health checks для Pod.

Наступна стаття: Rolling Updates та управління життєвим циклом Deployment