Kubernetes

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

Оновлення застосунків без downtime — від теорії до практики з детальною візуалізацією, математичними розрахунками та реальними прикладами

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

Проблема: як оновити застосунок без downtime?

У попередній статті ми навчилися створювати Deployment, масштабувати його та використовувати self-healing. Але залишилося найважливіше питання: як оновити застосунок на нову версію без зупинки сервісу?

Сценарій: оновлення веб-застосунку у production

Уявіть, що ваш TodoApi працює у production з 3 репліками. Ви виправили критичний баг та хочете розгорнути нову версію. Які у вас варіанти?

Варіант 1: "Наївний" підхід (з downtime)

Оновлення з downtime
# Видалити всі старі Pod
$ kubectl delete deployment todoapi
⚠ Сервіс недоступний!
# Створити Deployment з новою версією
$ kubectl apply -f todoapi-v2.yaml
⚠ Чекаємо 30 секунд, поки Pod стартують...
✓ Сервіс знову доступний

Проблема: Є період (30-60 секунд), коли жоден Pod не працює. Користувачі отримують помилки 503 Service Unavailable. Це неприйнятно для production.

Варіант 2: Rolling Update (без downtime)

Rolling Update
# Змінити версію образу у YAML
$ kubectl set image deployment/todoapi todoapi=todoapi:2.0.0
→ Створюється новий Pod з версією 2.0.0
→ Новий Pod стає Ready
→ Старий Pod видаляється
→ Повторюється для всіх реплік
✓ Сервіс працював весь час!

Переваги: Завжди є працюючі Pod. Користувачі не помічають оновлення. Якщо нова версія має баг — можна швидко повернутись до старої.

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


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

Rolling Update — це стратегія оновлення Deployment, при якій старі Pod поступово замінюються новими, завжди залишаючи мінімальну кількість працюючих реплік. Це гарантує zero-downtime deployment — оновлення без зупинки сервісу.

Ключова ідея: Kubernetes не видаляє всі старі Pod одразу. Він створює нові Pod, чекає, поки вони стануть готовими (пройдуть readiness probe), і лише після цього видаляє старі. Цей процес повторюється, поки всі Pod не будуть оновлені.Аналогія: Уявіть, що ви міняєте колеса на автомобілі, який їде. Ви не можете зняти всі колеса одразу — машина впаде. Замість цього ви міняєте по одному колесу, завжди залишаючи мінімум 3 колеса на місці. Так само працює Rolling Update.

Основні характеристики Rolling Update

Zero-downtime

Завжди є мінімальна кількість працюючих Pod. Користувачі не помічають оновлення — сервіс доступний весь час.

Поступовість

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

Контрольованість

Ви контролюєте швидкість оновлення через параметри maxSurge та maxUnavailable. Можна зробити оновлення швидким (багато Pod одразу) або обережним (по одному Pod).

Автоматичний rollback

Якщо нові Pod не проходять health checks, оновлення автоматично зупиняється. Старі Pod залишаються працювати. Ви можете вручну повернутись до попередньої версії однією командою.

Як працює Rolling Update: покрокова візуалізація

Давайте детально розберемо, що відбувається під час Rolling Update. Візьмемо приклад: Deployment з 3 репліками оновлюється з версії 1.0 на версію 2.0.

Початковий стан

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

package "Deployment: todoapi" #e3f2fd {
    component "ReplicaSet v1.0\n(todoapi-abc123)" as rs1 #e8f5e9 {
        [replicas: 3]
    }
}

package "Pods (версія 1.0)" #e8f5e9 {
    component "Pod 1\nv1.0" as p1
    component "Pod 2\nv1.0" as p2
    component "Pod 3\nv1.0" as p3
}

rs1 --> p1
rs1 --> p2
rs1 --> p3

note right of rs1
    Всі 3 Pod працюють
    з версією 1.0
end note

@enduml

Стан: 3 Pod з версією 1.0 працюють нормально. Сервіс обробляє запити користувачів.

Крок 1: Користувач ініціює оновлення

Користувач змінює версію образу у Deployment:

Ініціація оновлення
$ kubectl set image deployment/todoapi todoapi=todoapi:2.0.0
deployment.apps/todoapi image updated

Що відбувається всередині Kubernetes:

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

actor "Користувач" as user
participant "kubectl" as kubectl
participant "API Server" as api
participant "etcd" as etcd
participant "Deployment\nController" as dc

user -> kubectl: kubectl set image deployment/todoapi\ntodoapi=todoapi:2.0.0
kubectl -> api: PATCH /deployments/todoapi\n{spec.template.spec.containers[0].image: "todoapi:2.0.0"}
api -> etcd: Оновити Deployment
etcd --> api: OK
api --> kubectl: deployment.apps/todoapi image updated
kubectl --> user: Оновлення ініційовано

api -> dc: Подія: Deployment змінено
activate dc
dc -> dc: Виявлено зміну у spec.template\n(образ змінився з 1.0.0 на 2.0.0)
dc -> dc: Потрібно створити новий ReplicaSet\nдля нової версії Pod
deactivate dc

note right of dc
    Deployment Controller
    виявляє, що шаблон Pod
    змінився, і починає
    rolling update
end note

@enduml

Важливо: Deployment Controller виявляє, що spec.template змінився (образ todoapi:1.0.0todoapi:2.0.0). Це сигнал для створення нового ReplicaSet.

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

Deployment Controller створює новий ReplicaSet для версії 2.0:

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

package "Deployment: todoapi" #e3f2fd {
    component "ReplicaSet v1.0\n(todoapi-abc123)" as rs1 #e8f5e9 {
        [replicas: 3]
    }
    
    component "ReplicaSet v2.0\n(todoapi-def456)" as rs2 #fff3e0 {
        [replicas: 0]
    }
}

package "Pods (версія 1.0)" #e8f5e9 {
    component "Pod 1\nv1.0" as p1
    component "Pod 2\nv1.0" as p2
    component "Pod 3\nv1.0" as p3
}

rs1 --> p1
rs1 --> p2
rs1 --> p3

note right of rs2
    Новий ReplicaSet створено,
    але replicas=0
    (поки що немає Pod)
end note

@enduml

Стан: Тепер є два ReplicaSet:

  • Старий (v1.0): 3 репліки (працюють)
  • Новий (v2.0): 0 реплік (поки що порожній)

Крок 3: Поступове масштабування (ітерація 1)

Deployment Controller починає rolling update:

  1. Збільшує replicas нового ReplicaSet на 1 (0 → 1)
  2. Зменшує replicas старого ReplicaSet на 1 (3 → 2)
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Deployment: todoapi" #e3f2fd {
    component "ReplicaSet v1.0\n(todoapi-abc123)" as rs1 #e8f5e9 {
        [replicas: 2]
    }
    
    component "ReplicaSet v2.0\n(todoapi-def456)" as rs2 #fff3e0 {
        [replicas: 1]
    }
}

package "Pods (версія 1.0)" #e8f5e9 {
    component "Pod 1\nv1.0\nRunning" as p1
    component "Pod 2\nv1.0\nRunning" as p2
    component "Pod 3\nv1.0\nTerminating" as p3 #ffebee
}

package "Pods (версія 2.0)" #fff3e0 {
    component "Pod 4\nv2.0\nContainerCreating" as p4
}

rs1 --> p1
rs1 --> p2
rs1 -[dashed]-> p3

rs2 --> p4

note right of p4
    Новий Pod створюється
    (завантаження образу,
    запуск контейнера)
end note

note right of p3
    Старий Pod отримав
    SIGTERM та завершується
end note

@enduml

Стан:

  • 2 старі Pod працюють (v1.0)
  • 1 старий Pod завершується (v1.0)
  • 1 новий Pod створюється (v2.0)

Важливо: Kubernetes не чекає, поки старий Pod завершиться. Він одразу створює новий Pod паралельно. Це прискорює оновлення.

Крок 4: Очікування готовності нового Pod

Новий Pod проходить lifecycle:

  1. Завантаження образу
  2. Запуск контейнера
  3. Проходження readiness probe
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "ReplicaSet\nController" as rsc
participant "Scheduler" as sched
participant "Kubelet" as kubelet
participant "Pod 4\n(v2.0)" as pod
participant "Readiness\nProbe" as probe

rsc -> pod: Створити Pod
pod -> sched: Призначити вузол
sched -> kubelet: Pod призначено node-1

activate kubelet
kubelet -> kubelet: Завантажити образ\ntodoapi:2.0.0
note right: 5-10 секунд

kubelet -> kubelet: Запустити контейнер
note right: 2-3 секунди

kubelet -> probe: Перевірити readiness\n(HTTP GET /health)
probe --> kubelet: 200 OK

kubelet -> pod: Встановити Ready=True
deactivate kubelet

pod -> rsc: Pod готовий!

note right of rsc
    ReplicaSet Controller
    бачить, що новий Pod
    готовий, і може продовжити
    rolling update
end note

@enduml

Критично важливо: Deployment Controller чекає, поки новий Pod стане Ready (пройде readiness probe), перед тим як продовжити оновлення. Якщо Pod не стає готовим протягом progressDeadlineSeconds (за замовчуванням 600 секунд) — оновлення зупиняється.

Крок 5: Продовження rolling update (ітерація 2)

Після того, як Pod 4 (v2.0) став готовим, Deployment Controller продовжує оновлення:

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

package "Deployment: todoapi" #e3f2fd {
    component "ReplicaSet v1.0\n(todoapi-abc123)" as rs1 #e8f5e9 {
        [replicas: 1]
    }
    
    component "ReplicaSet v2.0\n(todoapi-def456)" as rs2 #fff3e0 {
        [replicas: 2]
    }
}

package "Pods (версія 1.0)" #e8f5e9 {
    component "Pod 1\nv1.0\nRunning" as p1
    component "Pod 2\nv1.0\nTerminating" as p2 #ffebee
}

package "Pods (версія 2.0)" #fff3e0 {
    component "Pod 4\nv2.0\nRunning" as p4
    component "Pod 5\nv2.0\nContainerCreating" as p5
}

rs1 --> p1
rs1 -[dashed]-> p2

rs2 --> p4
rs2 --> p5

note right of p5
    Другий новий Pod
    створюється
end note

@enduml

Стан:

  • 1 старий Pod працює (v1.0)
  • 1 старий Pod завершується (v1.0)
  • 1 новий Pod працює (v2.0)
  • 1 новий Pod створюється (v2.0)

Крок 6: Завершення rolling update (ітерація 3)

Після того, як Pod 5 (v2.0) став готовим:

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

package "Deployment: todoapi" #e3f2fd {
    component "ReplicaSet v1.0\n(todoapi-abc123)" as rs1 #e8f5e9 {
        [replicas: 0]
    }
    
    component "ReplicaSet v2.0\n(todoapi-def456)" as rs2 #fff3e0 {
        [replicas: 3]
    }
}

package "Pods (версія 1.0)" #e8f5e9 {
    component "Pod 1\nv1.0\nTerminating" as p1 #ffebee
}

package "Pods (версія 2.0)" #fff3e0 {
    component "Pod 4\nv2.0\nRunning" as p4
    component "Pod 5\nv2.0\nRunning" as p5
    component "Pod 6\nv2.0\nContainerCreating" as p6
}

rs1 -[dashed]-> p1

rs2 --> p4
rs2 --> p5
rs2 --> p6

note right of rs1
    Старий ReplicaSet
    зменшено до 0 реплік,
    але НЕ видалено
    (для rollback)
end note

@enduml

Стан:

  • 0 старих Pod (останній завершується)
  • 3 нові Pod (2 працюють, 1 створюється)

Крок 7: Фінальний стан

Після того, як Pod 6 (v2.0) став готовим, rolling update завершено:

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

package "Deployment: todoapi" #e3f2fd {
    component "ReplicaSet v1.0\n(todoapi-abc123)" as rs1 #e8f5e9 {
        [replicas: 0]
        [зберігається для rollback]
    }
    
    component "ReplicaSet v2.0\n(todoapi-def456)" as rs2 #fff3e0 {
        [replicas: 3]
    }
}

package "Pods (версія 2.0)" #fff3e0 {
    component "Pod 4\nv2.0\nRunning" as p4
    component "Pod 5\nv2.0\nRunning" as p5
    component "Pod 6\nv2.0\nRunning" as p6
}

rs2 --> p4
rs2 --> p5
rs2 --> p6

note right of rs1
    Старий ReplicaSet
    зберігається з replicas=0
    для можливості rollback
end note

note right of rs2
    Всі 3 Pod працюють
    з версією 2.0
    Rolling update завершено!
end note

@enduml

Результат: Всі Pod оновлені до версії 2.0. Старий ReplicaSet зберігається з replicas: 0 для можливості швидкого rollback.

Чому старий ReplicaSet не видаляється?Kubernetes зберігає старі ReplicaSet (за замовчуванням 10 останніх) для можливості швидкого rollback. Якщо ви виявите баг у версії 2.0 та захочете повернутись до 1.0, Kubernetes просто:
  1. Збільшить replicas старого ReplicaSet (0 → 3)
  2. Зменшить replicas нового ReplicaSet (3 → 0)
Це займає 10-20 секунд, бо образ версії 1.0 вже є на вузлах (кешовано). Без збереження старого ReplicaSet довелося б створювати новий, що займає більше часу.

Повна візуалізація Rolling Update

Тепер об'єднаємо всі кроки в одну sequence diagram:

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

participant "kubectl" as kubectl
participant "API Server" as api
participant "Deployment\nController" as dc
participant "ReplicaSet v1.0\nController" as rsc1
participant "ReplicaSet v2.0\nController" as rsc2
participant "Scheduler" as sched
participant "Kubelet" as kubelet

== Ініціація оновлення ==

kubectl -> api: PATCH /deployments/todoapi\n{image: todoapi:2.0.0}
api -> dc: Подія: Deployment змінено

activate dc
dc -> dc: Виявлено зміну spec.template
dc -> api: Створити ReplicaSet v2.0 (replicas=0)
api -> rsc2: Подія: новий ReplicaSet
deactivate dc

== Ітерація 1: Оновлення першого Pod ==

activate dc
dc -> api: PATCH ReplicaSet v2.0 (replicas: 0→1)
dc -> api: PATCH ReplicaSet v1.0 (replicas: 3→2)
deactivate dc

api -> rsc2: Подія: replicas змінено
activate rsc2
rsc2 -> api: Створити Pod 4 (v2.0)
deactivate rsc2

api -> rsc1: Подія: replicas змінено
activate rsc1
rsc1 -> api: Видалити Pod 3 (v1.0)
deactivate rsc1

api -> sched: Подія: новий Pod 4
sched -> kubelet: Призначити Pod 4 вузлу

activate kubelet
kubelet -> kubelet: Завантажити образ todoapi:2.0.0
kubelet -> kubelet: Запустити контейнер
kubelet -> kubelet: Перевірити readiness probe
kubelet -> api: Pod 4 Ready=True
deactivate kubelet

== Ітерація 2: Оновлення другого Pod ==

activate dc
dc -> dc: Pod 4 готовий, продовжити
dc -> api: PATCH ReplicaSet v2.0 (replicas: 1→2)
dc -> api: PATCH ReplicaSet v1.0 (replicas: 2→1)
deactivate dc

api -> rsc2: Подія: replicas змінено
activate rsc2
rsc2 -> api: Створити Pod 5 (v2.0)
deactivate rsc2

api -> rsc1: Подія: replicas змінено
activate rsc1
rsc1 -> api: Видалити Pod 2 (v1.0)
deactivate rsc1

api -> sched: Подія: новий Pod 5
sched -> kubelet: Призначити Pod 5 вузлу

activate kubelet
kubelet -> kubelet: Образ вже є (кешовано)
kubelet -> kubelet: Запустити контейнер
kubelet -> kubelet: Перевірити readiness probe
kubelet -> api: Pod 5 Ready=True
deactivate kubelet

== Ітерація 3: Оновлення третього Pod ==

activate dc
dc -> dc: Pod 5 готовий, продовжити
dc -> api: PATCH ReplicaSet v2.0 (replicas: 2→3)
dc -> api: PATCH ReplicaSet v1.0 (replicas: 1→0)
deactivate dc

api -> rsc2: Подія: replicas змінено
activate rsc2
rsc2 -> api: Створити Pod 6 (v2.0)
deactivate rsc2

api -> rsc1: Подія: replicas змінено
activate rsc1
rsc1 -> api: Видалити Pod 1 (v1.0)
deactivate rsc1

api -> sched: Подія: новий Pod 6
sched -> kubelet: Призначити Pod 6 вузлу

activate kubelet
kubelet -> kubelet: Образ вже є (кешовано)
kubelet -> kubelet: Запустити контейнер
kubelet -> kubelet: Перевірити readiness probe
kubelet -> api: Pod 6 Ready=True
deactivate kubelet

== Завершення ==

activate dc
dc -> dc: Всі Pod оновлені
dc -> api: Встановити Deployment status:\nAvailable=True, Progressing=False
deactivate dc

note right of dc
  Rolling update завершено!
  Час: ~30-60 секунд
  Downtime: 0 секунд
end note

@enduml

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

  1. Поступовість — Pod оновлюються по одному (або по кілька, залежно від maxSurge/maxUnavailable)
  2. Очікування готовності — перед продовженням оновлення Kubernetes чекає, поки новий Pod стане Ready
  3. Паралельність — створення нового Pod та видалення старого відбуваються паралельно
  4. Кешування образів — після завантаження образу на вузол, наступні Pod стартують швидше
  5. Zero-downtime — завжди є мінімум 2 працюючі Pod (у нашому прикладі)

Стратегії оновлення: RollingUpdate vs Recreate

Kubernetes підтримує дві стратегії оновлення Deployment:

1. RollingUpdate (за замовчуванням)

Поступове оновлення, яке ми щойно розглянули. Це рекомендована стратегія для більшості застосунків.

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1

Переваги:

  • Zero-downtime — сервіс доступний весь час
  • Поступове виявлення проблем — якщо перший новий Pod падає, оновлення зупиняється
  • Можливість rollback — старі Pod ще працюють, можна швидко повернутись

Недоліки:

  • Повільніше за Recreate (потрібен час на поступове оновлення)
  • Потребує більше ресурсів (одночасно працюють старі та нові Pod)
  • Складніше для застосунків, які не підтримують одночасну роботу різних версій

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

  • Веб-застосунки (API, frontend)
  • Stateless сервіси
  • Будь-які застосунки, де downtime неприйнятний

2. Recreate

Спочатку видаляються всі старі Pod, потім створюються нові. Є період downtime.

spec:
  strategy:
    type: Recreate

Переваги:

  • Простота — немає складної логіки поступового оновлення
  • Менше ресурсів — не потрібно одночасно тримати старі та нові Pod
  • Гарантія, що лише одна версія працює — немає проблем з несумісністю версій

Недоліки:

  • Downtime — є період (30-60 секунд), коли сервіс недоступний
  • Ризикованіше — якщо нова версія має баг, користувачі одразу його побачать

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

  • Застосунки, які не підтримують одночасну роботу різних версій (наприклад, через несумісність схеми БД)
  • Stateful застосунки з одним екземпляром (наприклад, база даних)
  • Внутрішні сервіси, де downtime прийнятний (наприклад, cron jobs)

Порівняння стратегій

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

!define ROLLING_COLOR #e8f5e9
!define RECREATE_COLOR #ffebee
!define DOWNTIME_COLOR #fff3e0

rectangle "RollingUpdate" ROLLING_COLOR {
    rectangle "t=0s" as r0 {
        (Pod 1 v1.0)
        (Pod 2 v1.0)
        (Pod 3 v1.0)
    }
    
    rectangle "t=10s" as r10 {
        (Pod 1 v1.0)
        (Pod 2 v1.0)
        (Pod 4 v2.0)
    }
    
    rectangle "t=20s" as r20 {
        (Pod 1 v1.0)
        (Pod 4 v2.0)
        (Pod 5 v2.0)
    }
    
    rectangle "t=30s" as r30 {
        (Pod 4 v2.0)
        (Pod 5 v2.0)
        (Pod 6 v2.0)
    }
    
    r0 -down-> r10
    r10 -down-> r20
    r20 -down-> r30
}

rectangle "Recreate" RECREATE_COLOR {
    rectangle "t=0s" as c0 {
        (Pod 1 v1.0)
        (Pod 2 v1.0)
        (Pod 3 v1.0)
    }
    
    rectangle "t=5s" as c5 DOWNTIME_COLOR {
        note "Downtime!\nЖодного Pod" as n1
    }
    
    rectangle "t=35s" as c35 {
        (Pod 4 v2.0)
        (Pod 5 v2.0)
        (Pod 6 v2.0)
    }
    
    c0 -down-> c5
    c5 -down-> c35
}

note right of r30
    RollingUpdate:
    - Завжди є працюючі Pod
    - Downtime: 0 секунд
    - Час оновлення: 30 секунд
end note

note right of c35
    Recreate:
    - Є період без Pod
    - Downtime: 30 секунд
    - Час оновлення: 35 секунд
end note

@enduml

Параметри Rolling Update: maxSurge та maxUnavailable

Тепер розберемо найважливіші параметри, які контролюють швидкість та безпеку rolling update.

maxUnavailable

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

  • Простими словами (з іншого ракурсу): Це ваш "запас міцності" або допустима втрата потужності. Цей параметр показує, наскільки сильно ви готові тимчасово "просісти" по продуктивності заради швидшого оновлення.
  • Конкретний приклад: Якщо ваш інтернет-магазин під час розпродажу обслуговується 10 контейнерами (replicas: 10) і ви вказуєте maxUnavailable: 30% (тобто 3 контейнери), це означає: "Я згоден, щоб під час оновлення мій сайт тимчасово обслуговували 7 контейнерів замість 10, поки інші 3 перестворюються на нову версію". Якщо ж ви вкажете maxUnavailable: 0 (або 0%), це означає, що ви за жодних обставин не згодні на тимчасове просідання потужності, і Kubernetes не видалить жодного старого контейнера, поки не переконається, що новий успішно піднявся і готовий приймати трафік.

Формат: Абсолютне число (1, 2) або відсоток від replicas (25%, 50%).

Формула розрахунку мінімальної кількості доступних Pod:

min_available = replicas - maxUnavailable

Приклади:

replicas: 10, maxUnavailable: 2
Розрахунок:min_available = 10 - 2 = 8Означає: Під час оновлення мінімум 8 Pod мають бути доступними. Kubernetes може видалити максимум 2 старі Pod одразу.Візуалізація:
Початок:  [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1]  (10 Pod)
Крок 1:   [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v2] [v2]  (8 v1, 2 v2)
Крок 2:   [v1] [v1] [v1] [v1] [v1] [v1] [v2] [v2] [v2] [v2]  (6 v1, 4 v2)
...
Кінець:   [v2] [v2] [v2] [v2] [v2] [v2] [v2] [v2] [v2] [v2]  (10 Pod)
replicas: 10, maxUnavailable: 25%
Розрахунок:25% від 10 = 2.5 → округлюється вниз до 2min_available = 10 - 2 = 8Означає: Те саме, що maxUnavailable: 2 — мінімум 8 Pod доступні.Чому округлення вниз? Kubernetes завжди округлює maxUnavailable вниз для безпеки — краще залишити більше доступних Pod, ніж менше.
replicas: 3, maxUnavailable: 1
Розрахунок:min_available = 3 - 1 = 2Означає: Під час оновлення мінімум 2 Pod доступні. Kubernetes оновлює по одному Pod за раз.Візуалізація:
Початок:  [v1] [v1] [v1]           (3 Pod)
Крок 1:   [v1] [v1] [v2]           (2 v1, 1 v2)
Крок 2:   [v1] [v2] [v2]           (1 v1, 2 v2)
Крок 3:   [v2] [v2] [v2]           (3 Pod)
replicas: 3, maxUnavailable: 0
Розрахунок:min_available = 3 - 0 = 3Означає: Під час оновлення всі 3 Pod мають бути доступними. Kubernetes не може видалити жодного старого Pod, поки не створить новий.Важливо: Це вимагає maxSurge > 0, інакше оновлення неможливе (не можна ні видалити старі, ні створити нові понад ліміт).

maxSurge

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

  • Простими словами (з іншого ракурсу): Це ваш "кредитний ліміт" на ресурси серверів. Цей параметр вказує, скільки додаткової оперативної пам'яті та процесорного часу ви готові тимчасово виділити (позичити) у кластера, щоб паралельно запустити нові контейнери поруч зі старими.
  • Конкретний приклад: Уявіть, що ви переносите меблі зі старої кімнати в нову. Якщо у вас є вільний коридор (додатковий простір), ви можете винести нові меблі туди, розпакувати, а вже потім заносити (це швидкий варіант, аналог maxSurge > 0). Якщо ж коридору немає (вільних ресурсів у кластері обмаль, maxSurge: 0), вам доведеться спочатку викинути старий диван (видалити старий Pod), звільнити місце, і лише потім заносити новий (це повільніше, але економно). З maxSurge: 20% для 10 реплік Kubernetes створює 2 нових контейнери нової версії одночасно, навіть не чіпаючи старі, прискорюючи перехід за рахунок тимчасового використання додаткових ресурсів кластера.

Формат: Абсолютне число (1, 2) або відсоток від replicas (25%, 50%).

Формула розрахунку максимальної кількості Pod під час оновлення:

max_pods = replicas + maxSurge

Приклади:

replicas: 10, maxSurge: 2
Розрахунок:max_pods = 10 + 2 = 12Означає: Під час оновлення максимум 12 Pod можуть існувати одночасно. Kubernetes може створити 2 нові Pod понад 10 реплік.Візуалізація:
Початок:  [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1]        (10 Pod)
Крок 1:   [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v2] [v2]  (12 Pod!)
Крок 2:   [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v2] [v2] [v2] [v2]  (12 Pod!)
...
Кінець:   [v2] [v2] [v2] [v2] [v2] [v2] [v2] [v2] [v2] [v2]        (10 Pod)
Навіщо це потрібно? Додаткові Pod дозволяють швидше виконати оновлення. Нові Pod створюються паралельно зі старими, і лише після готовності нових старі видаляються.
replicas: 10, maxSurge: 50%
Розрахунок:50% від 10 = 5max_pods = 10 + 5 = 15Означає: Під час оновлення максимум 15 Pod можуть існувати одночасно. Дуже швидке оновлення, але потребує багато ресурсів.
replicas: 3, maxSurge: 1
Розрахунок:max_pods = 3 + 1 = 4Означає: Під час оновлення максимум 4 Pod можуть існувати одночасно.Візуалізація:
Початок:  [v1] [v1] [v1]           (3 Pod)
Крок 1:   [v1] [v1] [v1] [v2]      (4 Pod! 3 v1, 1 v2)
Крок 2:   [v1] [v1] [v2] [v2]      (4 Pod! 2 v1, 2 v2)
Крок 3:   [v1] [v2] [v2] [v2]      (4 Pod! 1 v1, 3 v2)
Крок 4:   [v2] [v2] [v2]           (3 Pod)
replicas: 3, maxSurge: 0
Розрахунок:max_pods = 3 + 0 = 3Означає: Під час оновлення максимум 3 Pod можуть існувати одночасно. Kubernetes не може створити додаткові Pod — спочатку має видалити старий, потім створити новий.Важливо: Це вимагає maxUnavailable > 0, інакше оновлення неможливе.

Комбінації maxSurge та maxUnavailable

Різні комбінації цих параметрів дають різну поведінку оновлення:

Швидке оновлення, багато ресурсів

maxSurge: 50%
maxUnavailable: 0

Поведінка: Створюються багато нових Pod одразу (до 50% понад replicas), старі видаляються лише після готовності нових. Завжди є всі репліки доступними.

Приклад (replicas: 10):

  • Крок 1: 10 старих + 5 нових = 15 Pod
  • Крок 2: 5 старих + 10 нових = 15 Pod
  • Крок 3: 0 старих + 10 нових = 10 Pod

Переваги: Найшвидше оновлення, zero-downtime гарантовано

Недоліки: Потребує 150% ресурсів (CPU, пам'ять) під час оновлення

Повільне оновлення, мало ресурсів

maxSurge: 0
maxUnavailable: 25%

Поведінка: Спочатку видаляються старі Pod (до 25%), потім створюються нові. Економить ресурси, але є період зниженої доступності.

Приклад (replicas: 10):

  • Крок 1: 7-8 старих + 2-3 нових = 10 Pod (мінімум 7 доступних)
  • Крок 2: 5 старих + 5 нових = 10 Pod
  • Крок 3: 0 старих + 10 нових = 10 Pod

Переваги: Не потребує додаткових ресурсів

Недоліки: Повільніше, є період зниженої доступності (7 замість 10 Pod)

Збалансований підхід (за замовчуванням)

maxSurge: 25%
maxUnavailable: 25%

Поведінка: Компроміс між швидкістю та ресурсами. Можна створити до 25% додаткових Pod та видалити до 25% старих одночасно.

Приклад (replicas: 10):

  • Крок 1: 7-8 старих + 2-3 нових = 10-11 Pod
  • Крок 2: 5 старих + 5 нових = 10 Pod
  • Крок 3: 0 старих + 10 нових = 10 Pod

Переваги: Баланс між швидкістю та ресурсами

Недоліки: Не найшвидше, не найекономніше

Обережне оновлення (по одному)

maxSurge: 1
maxUnavailable: 0

Поведінка: Оновлення по одному Pod за раз. Завжди є всі репліки доступними. Найбезпечніший підхід.

Приклад (replicas: 10):

  • Крок 1: 10 старих + 1 новий = 11 Pod
  • Крок 2: 9 старих + 2 нових = 11 Pod
  • ...
  • Крок 10: 0 старих + 10 нових = 10 Pod

Переваги: Максимальна безпека, легко виявити проблеми на ранній стадії

Недоліки: Найповільніше оновлення (10 ітерацій для 10 реплік)

Математичні розрахунки для різних сценаріїв

Давайте розрахуємо, скільки Pod буде під час оновлення для різних конфігурацій:

Дано: replicas: 10

maxSurgemaxUnavailablemin_availablemax_podsДіапазон Pod під час оновлення
019109-10 Pod
025%8108-10 Pod
10101110-11 Pod
119119-11 Pod
25%25%8128-12 Pod
50%0101510-15 Pod
100%0102010-20 Pod

Висновки:

  1. Більший maxSurge → швидше оновлення, але більше ресурсів
  2. Більший maxUnavailable → швидше оновлення, але менша доступність
  3. maxSurge=0, maxUnavailable=0 → неможливо (оновлення заблоковано)
  4. Для критичних сервісів: maxSurge > 0, maxUnavailable = 0 (завжди повна доступність)
  5. Для економії ресурсів: maxSurge = 0, maxUnavailable > 0 (без додаткових Pod)

Додаткові параметри життєвого циклу

Окрім maxSurge та maxUnavailable, є ще кілька важливих параметрів:

progressDeadlineSeconds

Максимальний час (у секундах), протягом якого Deployment має показати хоча б якийсь рух вперед (прогрес) під час оновлення чи розгортання.

  • Простими словами (з іншого ракурсу): Це ваш "сигнальний таймер терпіння" для системи моніторингу або CI/CD пайплайну. Він відповідає на запитання: "Скільки часу ми готові чекати безрезультатного "висіння" процесу оновлення, перш ніж офіційно визнати, що щось пішло не так і підняти тривогу?"
  • Конкретний приклад: Уявіть, що ви запустили оновлення застосунку і пішли пити каву. Якщо через помилку в коді новий контейнер не може запуститися й постійно падає, Kubernetes не буде чекати вічно. З progressDeadlineSeconds: 300 (5 хвилин) система засікає час. Якщо за ці 5 хвилин жоден новий Pod не зміг успішно запуститись і стати Ready (тобто не було жодного прогресу), Kubernetes зупинить безглузді спроби, переведе статус оновлення в ProgressDeadlineExceeded (перевищено термін прогресу), що дозволить вашому автоматичному CI/CD скрипту чи ArgoCD одразу зрозуміти проблему та ініціювати відкат (rollback) до попередньої стабільної версії.

За замовчуванням: 600 (10 хвилин)

Що вважається "прогресом"?

  • Новий Pod став Ready
  • Старий Pod був видалений
  • Будь-яка зміна у кількості доступних реплік

Що відбувається при перевищенні таймауту?

Якщо за progressDeadlineSeconds жоден новий Pod не став готовим, Deployment отримує статус:

status:
  conditions:
    - type: Progressing
      status: "False"
      reason: ProgressDeadlineExceeded
      message: "ReplicaSet 'todoapi-def456' has timed out progressing."

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

Приклад:

spec:
  progressDeadlineSeconds: 300  # 5 хвилин
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

Сценарій: Новий образ має баг — Pod стартує, але не проходить readiness probe. Через 5 хвилин Kubernetes зупиняє оновлення та повідомляє про проблему.

Важливо:progressDeadlineSeconds — це не загальний час оновлення. Це час між прогресами. Якщо кожен Pod стартує за 30 секунд, а у вас 10 реплік, загальний час оновлення може бути 5 хвилин, і це нормально (бо є прогрес кожні 30 секунд).Неправильне розуміння: "Оновлення має завершитись за 600 секунд"Правильне розуміння: "Між кожним прогресом (новий Pod Ready) має пройти не більше 600 секунд"

minReadySeconds

Мінімальний час (у секундах), протягом якого новостворений Pod має безперервно працювати у стані "Ready" (готовий приймати трафік) без жодних падінь і перезапусків, перш ніж Kubernetes вважатиме його повністю стабільним і продовжить оновлювати інші репліки.

  • Простими словами (з іншого ракурсу): Це "карантинний період" або тест на витривалість для нових контейнерів. Він захищає вас від ситуації, коли контейнер формально запустився, відрапортував "я готовий!", але через 5 секунд упав через внутрішню помилку ініціалізації чи невірно зчитану конфігурацію.
  • Конкретний приклад: Уявіть нового працівника у команді. Якщо ви дасте йому роботу і він у першу ж секунду скаже "все зрозуміло!", ви не побіжите одразу звільняти старого працівника. Ви почекаєте хоча б день-два, щоб переконатися, що він дійсно справляється. Так само і з minReadySeconds: 30. Коли новий Pod проходить readiness probe (наприклад, віддав HTTP 200 на тестовий запит), Kubernetes не переходить одразу до видалення наступного старого Pod. Він тримає новий Pod "на карантині" рівно 30 секунд. Якщо протягом цих 30 секунд новий контейнер не впав, не перезапустився і стабільно тримав статус Ready, Kubernetes робить висновок: "Все чудово, цей контейнер дійсно здоровий та працездатний", маркує його як Available і спокійно продовжує оновлення решти кластера.

За замовчуванням: 0 (Pod вважається доступним одразу після Ready=True)

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

Іноді Pod стартує успішно (проходить readiness probe), але падає через кілька секунд (наприклад, через помилку підключення до БД, яка виявляється не одразу). minReadySeconds додає додаткову перевірку стабільності.

Приклад:

spec:
  minReadySeconds: 30
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

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

  1. Новий Pod стартує
  2. Pod проходить readiness probe → Ready=True
  3. Kubernetes чекає 30 секунд
  4. Якщо за ці 30 секунд Pod не впав → він вважається доступним, оновлення продовжується
  5. Якщо Pod впав → оновлення зупиняється

Візуалізація:

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

state "Pod створено" as created
state "Container запущено" as running
state "Readiness probe ✓" as ready
state "Очікування minReadySeconds" as waiting #fff3e0
state "Pod доступний" as available #e8f5e9
state "Pod впав" as crashed #ffebee

[*] --> created
created --> running : 5s
running --> ready : 10s
ready --> waiting : Ready=True

waiting --> available : 30s без падінь
waiting --> crashed : Pod впав протягом 30s

available --> [*] : Оновлення продовжується
crashed --> [*] : Оновлення зупиняється

note right of waiting
    minReadySeconds = 30
    Kubernetes чекає 30 секунд
    після Ready=True
end note

@enduml

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

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

Типові значення:

  • 0 — для простих застосунків (за замовчуванням)
  • 10-30 — для більшості веб-застосунків
  • 60-120 — для складних застосунків з довгою ініціалізацією

Health Checks для .NET застосунків

Тепер розглянемо, як правильно налаштувати health checks для ASP.NET Core застосунків у Kubernetes.

Базові health checks у ASP.NET Core

ASP.NET Core має вбудовану підтримку health checks через пакет Microsoft.Extensions.Diagnostics.HealthChecks.

Простий приклад:

var builder = WebApplication.CreateBuilder(args);

// Додаємо health checks
builder.Services.AddHealthChecks();

var app = builder.Build();

// Endpoint для health checks
app.MapHealthChecks("/health");

app.Run();

Це створює endpoint /health, який повертає:

  • 200 OK + "Healthy" — якщо все добре
  • 503 Service Unavailable + "Unhealthy" — якщо є проблеми

Розширені health checks з перевірками

Для production потрібні більш детальні перевірки:

using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks()
    // Перевірка підключення до БД
    .AddNpgSql(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        name: "postgresql",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "sql" })
    
    // Перевірка доступності зовнішнього API
    .AddUrlGroup(
        uri: new Uri("https://api.example.com/health"),
        name: "external-api",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "external" })
    
    // Кастомна перевірка пам'яті
    .AddCheck<MemoryHealthCheck>("memory");

var app = builder.Build();

// Liveness endpoint — перевіряє, чи застосунок живий
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // Не виконувати жодних перевірок, лише базову
});

// Readiness endpoint — перевіряє, чи застосунок готовий
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("db") || check.Tags.Contains("external")
});

// Детальний endpoint для debugging
app.MapHealthChecks("/health/detailed", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";
        var result = System.Text.Json.JsonSerializer.Serialize(new
        {
            status = report.Status.ToString(),
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                duration = e.Value.Duration.TotalMilliseconds
            }),
            totalDuration = report.TotalDuration.TotalMilliseconds
        });
        await context.Response.WriteAsync(result);
    }
});

app.Run();

// Кастомна перевірка пам'яті
public class MemoryHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var allocated = GC.GetTotalMemory(forceFullCollection: false);
        var threshold = 1024L * 1024L * 1024L; // 1 GB
        
        var status = allocated < threshold 
            ? HealthStatus.Healthy 
            : HealthStatus.Unhealthy;
        
        return Task.FromResult(new HealthCheckResult(
            status,
            description: $"Allocated memory: {allocated / 1024 / 1024} MB"));
    }
}

Різниця між Liveness та Readiness для .NET

Liveness Probe (/health/live)

Мета: Перевірити, чи застосунок живий (не deadlock, не crash).

Що перевіряти:

  • Базову доступність процесу (просто повернути 200 OK)
  • Критичні внутрішні компоненти (наприклад, чи не зависла черга повідомлень)

Що НЕ перевіряти:

  • Підключення до БД (якщо БД недоступна, це не означає, що застосунок мертвий)
  • Зовнішні API (їхня недоступність не означає deadlock)

Приклад:

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // Лише базова перевірка
});

Результат: Завжди повертає 200 OK, якщо процес працює.

Readiness Probe (/health/ready)

Мета: Перевірити, чи застосунок готовий приймати трафік.

Що перевіряти:

  • Підключення до БД (якщо БД недоступна, застосунок не може обробляти запити)
  • Зовнішні залежності (API, черги повідомлень)
  • Завершення ініціалізації (кеші завантажені, конфігурація прочитана)

Приклад:

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("db") || check.Tags.Contains("external")
});

Результат: Повертає 200 OK лише якщо БД доступна та зовнішні API працюють.

Налаштування Kubernetes probes для .NET

Тепер налаштуємо Deployment YAML для використання цих endpoints:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: todoapi
spec:
  replicas: 3
  selector:
    matchLabels:
      app: todoapi
  template:
    metadata:
      labels:
        app: todoapi
    spec:
      containers:
        - name: todoapi
          image: todoapi:2.0.0
          ports:
            - containerPort: 8080
          
          # Liveness probe — перевірка живості
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 30    # Час на старт застосунку
            periodSeconds: 10           # Перевірка кожні 10 секунд
            timeoutSeconds: 5           # Таймаут запиту
            failureThreshold: 3         # 3 невдалі спроби → restart
            successThreshold: 1         # 1 успішна спроба → healthy
          
          # Readiness probe — перевірка готовності
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 10    # Менше за liveness (швидше виявити готовність)
            periodSeconds: 5            # Частіше перевіряти
            timeoutSeconds: 3           # Менший таймаут
            failureThreshold: 3         # 3 невдалі спроби → not ready
            successThreshold: 1         # 1 успішна спроба → ready
          
          # Startup probe — для застосунків з повільним стартом
          startupProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 0
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 30        # 30 * 5s = 150s максимум на старт
            successThreshold: 1
          
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
Startup Probe — що це?Startup probe — це спеціальна перевірка для застосунків з повільним стартом. Вона відключає liveness та readiness probes до тих пір, поки застосунок не стартує.Проблема без startup probe:Якщо застосунок стартує 60 секунд, а livenessProbe.initialDelaySeconds: 30, то liveness probe почне перевіряти застосунок через 30 секунд. Застосунок ще не готовий → liveness fails → Kubernetes вб'є контейнер → restart → знову 60 секунд старту → знову fails → crash loop.Рішення з startup probe:Startup probe перевіряє застосунок кожні 5 секунд, максимум 30 разів (150 секунд). Liveness та readiness probes не працюють, поки startup probe не пройде. Це дає застосунку достатньо часу на старт.Коли використовувати:
  • Застосунки з повільним стартом (> 30 секунд)
  • Застосунки, які завантажують багато даних при старті
  • Legacy застосунки з довгою ініціалізацією

Приклад відповідей health checks

Liveness endpoint (/health/live):

curl /health/live
$ curl http://localhost:8080/health/live
Healthy

Readiness endpoint (/health/ready):

curl /health/ready (успішно)
$ curl http://localhost:8080/health/ready
Healthy
curl /health/ready (БД недоступна)
$ curl http://localhost:8080/health/ready
HTTP/1.1 503 Service Unavailable
Unhealthy

Детальний endpoint (/health/detailed):

curl /health/detailed
$ curl http://localhost:8080/health/detailed
{
"status": "Healthy",
"checks": [
{
"name": "postgresql",
"status": "Healthy",
"description": "Connection successful",
"duration": 12.5
},
{
"name": "external-api",
"status": "Healthy",
"description": null,
"duration": 45.2
},
{
"name": "memory",
"status": "Healthy",
"description": "Allocated memory: 156 MB",
"duration": 0.8
}
],
"totalDuration": 58.5
}

Resource Management для .NET застосунків

Правильне налаштування ресурсів критично важливе для стабільності .NET застосунків у Kubernetes.

Розуміння requests та limits

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

rectangle "Вузол (Node)" {
    rectangle "Доступні ресурси" as total #e3f2fd {
        [CPU: 4 cores]
        [Memory: 8 GB]
    }
    
    rectangle "Pod 1" as p1 #e8f5e9 {
        rectangle "requests" as r1 #fff3e0 {
            [CPU: 500m]
            [Memory: 512Mi]
        }
        rectangle "limits" as l1 #ffebee {
            [CPU: 1000m]
            [Memory: 1Gi]
        }
        rectangle "Реальне використання" as u1 {
            [CPU: 750m]
            [Memory: 768Mi]
        }
    }
    
    rectangle "Pod 2" as p2 #e8f5e9 {
        rectangle "requests" as r2 #fff3e0 {
            [CPU: 250m]
            [Memory: 256Mi]
        }
        rectangle "limits" as l2 #ffebee {
            [CPU: 500m]
            [Memory: 512Mi]
        }
        rectangle "Реальне використання" as u2 {
            [CPU: 300m]
            [Memory: 384Mi]
        }
    }
}

note right of r1
    Requests — гарантовані ресурси
    Scheduler використовує для
    вибору вузла
end note

note right of l1
    Limits — максимум
    CPU: throttling
    Memory: OOMKilled
end note

note right of u1
    Реальне використання
    може бути між
    requests та limits
end note

@enduml

Що відбувається при перевищенні limits

CPU Throttling

Що відбувається: Якщо Pod спробує використати більше CPU, ніж limits.cpu, Kubernetes обмежує (throttle) його CPU.

Симптоми:

  • Застосунок працює повільніше
  • Запити обробляються довше
  • Таймаути у клієнтів

Приклад:

resources:
  limits:
    cpu: "500m"  # 0.5 cores

Якщо застосунок спробує використати 1 core, Kubernetes обмежить його до 0.5 cores. Застосунок не впаде, але працюватиме повільніше.

Як виявити: kubectl top pods показує CPU usage близько до limits

OOMKilled (Out Of Memory)

Що відбувається: Якщо Pod спробує використати більше пам'яті, ніж limits.memory, Kubernetes вб'є (kill) контейнер.

Симптоми:

  • Pod постійно перезапускається
  • Статус OOMKilled у kubectl describe pod
  • RESTARTS збільшується

Приклад:

resources:
  limits:
    memory: "256Mi"

Якщо застосунок спробує виділити 300 MB, Kubernetes вб'є контейнер з exit code 137.

Як виявити:

kubectl describe pod <pod-name>
# Last State:     Terminated
#   Reason:       OOMKilled
#   Exit Code:    137

.NET Garbage Collection та Kubernetes

.NET має особливості роботи з пам'яттю, які важливо враховувати у Kubernetes:

Проблема: .NET GC не знає про Kubernetes memory limits. Він бачить всю пам'ять вузла (наприклад, 8 GB) та намагається використати до 75% від неї. Але Pod має limit 256 MB → OOMKilled.

Рішення: Налаштувати .NET GC для роботи у контейнері:

# У Dockerfile
ENV DOTNET_RUNNING_IN_CONTAINER=true
ENV DOTNET_GCHeapHardLimit=0x10000000  # 256 MB у hex (опціонально)

Або через Deployment YAML:

spec:
  containers:
    - name: todoapi
      image: todoapi:2.0.0
      env:
        - name: DOTNET_RUNNING_IN_CONTAINER
          value: "true"
        - name: DOTNET_GCHeapHardLimit
          value: "0x10000000"  # 256 MB
      resources:
        limits:
          memory: "256Mi"

Що робить DOTNET_RUNNING_IN_CONTAINER:

  • .NET GC читає cgroup limits (Kubernetes memory limits)
  • GC використовує максимум 75% від limits (наприклад, 192 MB з 256 MB)
  • Це запобігає OOMKilled
Best practice для .NET у Kubernetes:
  1. Завжди встановлюйте DOTNET_RUNNING_IN_CONTAINER=true — це критично важливо
  2. Встановлюйте memory limits — без них .NET може з'їсти всю пам'ять вузла
  3. Requests = 50-70% від limits — залишає запас для GC
  4. Моніторте GC — використовуйте dotnet-counters або Application Insights
Приклад:
resources:
  requests:
    memory: "128Mi"  # Мінімум для роботи
    cpu: "100m"
  limits:
    memory: "256Mi"  # Максимум (GC використає ~192 MB)
    cpu: "500m"

Підбір оптимальних ресурсів для .NET

Як визначити правильні значення requests та limits?

Крок 1: Запустити без limits та виміряти

resources:
  requests:
    memory: "128Mi"
    cpu: "100m"
  # Без limits — дозволити використовувати скільки потрібно

Крок 2: Згенерувати навантаження та виміряти споживання

Вимірювання ресурсів
# Генерація навантаження
$ wrk -t10 -c100 -d60s http://localhost:8080/todos
# Моніторинг ресурсів (у іншому терміналі)
$ kubectl top pods -l app=todoapi --watch
NAME CPU(cores) MEMORY(bytes)
todoapi-xxx-yyy 450m 180Mi
todoapi-xxx-zzz 420m 175Mi
todoapi-xxx-www 480m 185Mi

Крок 3: Встановити limits з запасом

На основі вимірювань:

  • Пікове CPU: 480m → встановити limit 600m (запас 25%)
  • Пікова пам'ять: 185 MB → встановити limit 256Mi (запас 38%)
resources:
  requests:
    memory: "128Mi"  # Базове споживання
    cpu: "200m"      # Середнє споживання
  limits:
    memory: "256Mi"  # Пік + запас
    cpu: "600m"      # Пік + запас

Типові проблеми та рішення:

Проблема: OOMKilled під час GC

Симптоми: Pod падає під час garbage collection

Причина: GC потребує додаткової пам'яті для роботи. Якщо limits занадто жорсткі, GC не може виділити пам'ять → OOMKilled.

Рішення: Збільшити limits на 20-30% понад пікове споживання:

resources:
  limits:
    memory: "320Mi"  # Було 256Mi

Проблема: Повільні запити під навантаженням

Симптоми: Запити обробляються повільно, таймаути

Причина: CPU throttling — застосунок досяг CPU limits

Рішення: Збільшити CPU limits або додати більше реплік:

resources:
  limits:
    cpu: "1000m"  # Було 500m
# АБО
spec:
  replicas: 5  # Було 3

Rollback та історія версій

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

Як Kubernetes зберігає історію

Кожна зміна у spec.template створює нову ревізію (revision) Deployment. Kubernetes зберігає старі ReplicaSet для можливості rollback.

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

package "Deployment: todoapi" #e3f2fd {
    rectangle "Revision History" {
        component "ReplicaSet v1.0\n(revision 1)" as rs1 #e8f5e9 {
            [replicas: 0]
            [image: todoapi:1.0.0]
        }
        
        component "ReplicaSet v1.1\n(revision 2)" as rs2 #e8f5e9 {
            [replicas: 0]
            [image: todoapi:1.1.0]
        }
        
        component "ReplicaSet v2.0\n(revision 3)" as rs3 #fff3e0 {
            [replicas: 3]
            [image: todoapi:2.0.0]
        }
    }
}

note right of rs1
    Старі ReplicaSet
    зберігаються з replicas=0
    для швидкого rollback
end note

note right of rs3
    Поточна версія
    (активна)
end note

@enduml

Скільки ревізій зберігається?

За замовчуванням Kubernetes зберігає 10 останніх ревізій. Це контролюється параметром revisionHistoryLimit:

spec:
  revisionHistoryLimit: 10  # За замовчуванням

Якщо встановити 0 — rollback буде неможливий (старі ReplicaSet видаляються одразу).

Перегляд історії ревізій

Переглянемо історію оновлень Deployment:

kubectl rollout history
$ kubectl rollout history deployment/todoapi
deployment.apps/todoapi
REVISION CHANGE-CAUSE
1
2
3 kubectl set image deployment/todoapi todoapi=todoapi:2.0.0

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

  • REVISION — номер ревізії (збільшується з кожним оновленням)
  • CHANGE-CAUSE — причина зміни (якщо вказана через annotation)
Як додати CHANGE-CAUSE:Щоб у історії було зрозуміло, що змінилось, додайте annotation при оновленні:
kubectl set image deployment/todoapi todoapi=todoapi:2.0.0 \
  --record
Або через annotation у YAML:
metadata:
  annotations:
    kubernetes.io/change-cause: "Update to version 2.0.0 with bug fixes"
Тепер у історії буде:
REVISION  CHANGE-CAUSE
3         Update to version 2.0.0 with bug fixes

Детальна інформація про ревізію

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

kubectl rollout history (детально)
$ kubectl rollout history deployment/todoapi --revision=3
deployment.apps/todoapi with revision #3
Pod Template:
Labels: app=todoapi
version=2.0.0
Annotations: kubernetes.io/change-cause: kubectl set image deployment/todoapi todoapi=todoapi:2.0.0
Containers:
todoapi:
Image: todoapi:2.0.0
Port: 8080/TCP
Limits:
cpu: 500m
memory: 256Mi
Requests:
cpu: 100m
memory: 128Mi

Тут ви бачите повну конфігурацію Pod для цієї ревізії.

Rollback до попередньої версії

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

kubectl rollout undo
$ kubectl rollout undo deployment/todoapi
deployment.apps/todoapi rolled back

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

  1. Kubernetes знаходить попередню ревізію (revision 2)
  2. Збільшує replicas старого ReplicaSet (revision 2) з 0 до 3
  3. Зменшує replicas поточного ReplicaSet (revision 3) з 3 до 0
  4. Виконує rolling update у зворотному напрямку
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "kubectl" as kubectl
participant "Deployment\nController" as dc
participant "ReplicaSet v1.1\n(revision 2)" as rs2
participant "ReplicaSet v2.0\n(revision 3)" as rs3

kubectl -> dc: kubectl rollout undo deployment/todoapi

activate dc
dc -> dc: Знайти попередню ревізію (2)
dc -> rs2: Збільшити replicas: 0 → 3
dc -> rs3: Зменшити replicas: 3 → 0
deactivate dc

note right of dc
    Rolling update
    у зворотному напрямку:
    v2.0 → v1.1
end note

rs2 -> rs2: Створити Pod з v1.1
rs3 -> rs3: Видалити Pod з v2.0

note right of rs2
    Образ v1.1 вже є
    на вузлах (кешовано)
    → швидкий rollback
    (10-20 секунд)
end note

@enduml

Швидкість rollback:

Rollback зазвичай займає 10-20 секунд, бо:

  • Образ попередньої версії вже є на вузлах (кешовано)
  • Не потрібно завантажувати образ з registry
  • Kubernetes просто перемикає ReplicaSet

Rollback до конкретної ревізії

Можна повернутись не лише до попередньої, а до будь-якої ревізії:

kubectl rollout undo (конкретна ревізія)
$ kubectl rollout undo deployment/todoapi --to-revision=1
deployment.apps/todoapi rolled back

Це повертає Deployment до ревізії 1 (самої першої версії).

Моніторинг процесу rollout

Під час оновлення або rollback можна стежити за прогресом:

kubectl rollout status
$ kubectl rollout status deployment/todoapi
Waiting for deployment "todoapi" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "todoapi" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "todoapi" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "todoapi" rollout to finish: 2 old replicas are pending termination...
Waiting for deployment "todoapi" rollout to finish: 1 old replicas are pending termination...
deployment "todoapi" successfully rolled out

Ця команда блокується до завершення rollout. Корисно для CI/CD pipelines.

Призупинення та відновлення rollout

Іноді потрібно призупинити оновлення (наприклад, для canary deployment):

kubectl rollout pause
$ kubectl rollout pause deployment/todoapi
deployment.apps/todoapi paused

Після паузи оновлення зупиняється. Можна внести кілька змін, а потім відновити:

kubectl rollout resume
$ kubectl rollout resume deployment/todoapi
deployment.apps/todoapi resumed

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

Canary deployment: Оновити 1 Pod, перевірити метрики, і якщо все добре — продовжити оновлення решти Pod.

# Призупинити оновлення
kubectl rollout pause deployment/todoapi

# Оновити образ (створюється лише 1 новий Pod)
kubectl set image deployment/todoapi todoapi=todoapi:2.0.0

# Почекати 5 хвилин, перевірити метрики
sleep 300

# Якщо все добре — продовжити
kubectl rollout resume deployment/todoapi

# Якщо є проблеми — rollback
kubectl rollout undo deployment/todoapi

Практичний приклад: оновлення TodoApi з v1.0 на v2.0

Тепер створимо реальний приклад оновлення застосунку з новою функціональністю.

Версія 1.0: Базовий CRUD

Це наш початковий TodoApi (з попередньої статті):

Program.cs (v1.0):

using System.Collections.Concurrent;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var todos = new ConcurrentDictionary<int, Todo>();
var nextId = 1;

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapGet("/health", () => Results.Ok(new { status = "healthy", version = "1.0.0" }));

app.MapGet("/todos", () => Results.Ok(new { todos = todos.Values, count = todos.Count }));

app.MapGet("/todos/{id:int}", (int id) =>
    todos.TryGetValue(id, out var todo) ? Results.Ok(todo) : Results.NotFound());

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;
    return Results.Created($"/todos/{id}", todo);
});

app.MapPut("/todos/{id:int}", (int id, UpdateTodoRequest request) =>
{
    if (!todos.TryGetValue(id, out var todo))
        return Results.NotFound();
    
    todo.Title = request.Title ?? todo.Title;
    todo.IsCompleted = request.IsCompleted ?? todo.IsCompleted;
    return Results.Ok(todo);
});

app.MapDelete("/todos/{id:int}", (int id) =>
    todos.TryRemove(id, out var todo) ? Results.Ok(todo) : Results.NotFound());

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);

Версія 2.0: Додавання статистики

Тепер додамо новий endpoint /stats для статистики:

Program.cs (v2.0):

using System.Collections.Concurrent;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var todos = new ConcurrentDictionary<int, Todo>();
var nextId = 1;

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// Оновлена версія у health check
app.MapGet("/health", () => Results.Ok(new { status = "healthy", version = "2.0.0" }));

app.MapGet("/todos", () => Results.Ok(new { todos = todos.Values, count = todos.Count }));

app.MapGet("/todos/{id:int}", (int id) =>
    todos.TryGetValue(id, out var todo) ? Results.Ok(todo) : Results.NotFound());

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;
    return Results.Created($"/todos/{id}", todo);
});

app.MapPut("/todos/{id:int}", (int id, UpdateTodoRequest request) =>
{
    if (!todos.TryGetValue(id, out var todo))
        return Results.NotFound();
    
    todo.Title = request.Title ?? todo.Title;
    todo.IsCompleted = request.IsCompleted ?? todo.IsCompleted;
    return Results.Ok(todo);
});

app.MapDelete("/todos/{id:int}", (int id) =>
    todos.TryRemove(id, out var todo) ? Results.Ok(todo) : Results.NotFound());

// НОВИЙ ENDPOINT: Статистика
app.MapGet("/stats", () =>
{
    var allTodos = todos.Values.ToList();
    var completed = allTodos.Count(t => t.IsCompleted);
    var pending = allTodos.Count - completed;
    var oldestTodo = allTodos.MinBy(t => t.CreatedAt);
    var newestTodo = allTodos.MaxBy(t => t.CreatedAt);
    
    return Results.Ok(new
    {
        total = allTodos.Count,
        completed,
        pending,
        completionRate = allTodos.Count > 0 ? (double)completed / allTodos.Count * 100 : 0,
        oldestTodo = oldestTodo?.CreatedAt,
        newestTodo = newestTodo?.CreatedAt
    });
})
.WithName("GetStatistics")
.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);

Зміни у версії 2.0:

  1. Оновлено версію у /health endpoint (1.0.0 → 2.0.0)
  2. Додано новий endpoint /stats з детальною статистикою

Збірка нової версії

Зберемо образ версії 2.0:

Збірка v2.0
$ eval $(minikube docker-env)
$ docker build -t todoapi:2.0.0 .
[+] Building 42.1s (15/15) FINISHED
=> exporting to image
=> => naming to docker.io/library/todoapi:2.0.0

Виконання rolling update

Тепер оновимо Deployment на нову версію:

Rolling update на v2.0
$ kubectl set image deployment/todoapi todoapi=todoapi:2.0.0 --record
deployment.apps/todoapi image updated

Стежимо за прогресом:

kubectl rollout status
$ kubectl rollout status deployment/todoapi
Waiting for deployment "todoapi" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "todoapi" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "todoapi" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "todoapi" rollout to finish: 1 old replicas are pending termination...
deployment "todoapi" successfully rolled out

Спостерігаємо за Pod у реальному часі:

kubectl get pods -w
$ kubectl get pods -l app=todoapi -w
NAME READY STATUS RESTARTS AGE
todoapi-abc123-xxx 1/1 Running 0 5m
todoapi-abc123-yyy 1/1 Running 0 5m
todoapi-abc123-zzz 1/1 Running 0 5m
todoapi-def456-aaa 0/1 Pending 0 0s
todoapi-def456-aaa 0/1 ContainerCreating 0 2s
todoapi-def456-aaa 1/1 Running 0 15s
todoapi-abc123-xxx 1/1 Terminating 0 5m15s
todoapi-def456-bbb 0/1 Pending 0 0s
todoapi-def456-bbb 0/1 ContainerCreating 0 1s
todoapi-def456-bbb 1/1 Running 0 12s
todoapi-abc123-yyy 1/1 Terminating 0 5m27s
todoapi-def456-ccc 0/1 Pending 0 0s
todoapi-def456-ccc 0/1 ContainerCreating 0 2s
todoapi-def456-ccc 1/1 Running 0 14s
todoapi-abc123-zzz 1/1 Terminating 0 5m41s

Бачимо класичний rolling update: нові Pod створюються, старі видаляються по черзі.

Тестування нової версії

Після завершення оновлення протестуємо новий endpoint:

Тестування v2.0
$ kubectl port-forward deployment/todoapi 8080:8080 &
# Перевірка версії
$ curl http://localhost:8080/health
{"status":"healthy","version":"2.0.0"}
# Створення кількох todos
$ curl -X POST http://localhost:8080/todos -H "Content-Type: application/json" \
-d '{"title":"Вивчити Rolling Updates"}'
$ curl -X POST http://localhost:8080/todos -H "Content-Type: application/json" \
-d '{"title":"Протестувати Rollback"}'
# Позначити перший як completed
$ curl -X PUT http://localhost:8080/todos/1 -H "Content-Type: application/json" \
-d '{"isCompleted":true}'
# НОВИЙ ENDPOINT: Статистика
$ curl http://localhost:8080/stats
{
"total": 2,
"completed": 1,
"pending": 1,
"completionRate": 50.0,
"oldestTodo": "2026-05-09T20:50:00.123Z",
"newestTodo": "2026-05-09T20:50:05.456Z"
}

Новий endpoint /stats працює! Оновлення успішне.

Симуляція проблеми та rollback

Тепер уявімо, що версія 2.0 має критичний баг (наприклад, endpoint /stats падає під навантаженням). Потрібно швидко повернутись до версії 1.0.

Rollback до v1.0
$ kubectl rollout undo deployment/todoapi
deployment.apps/todoapi rolled back
Моніторинг rollback
$ kubectl rollout status deployment/todoapi
Waiting for deployment "todoapi" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "todoapi" rollout to finish: 2 out of 3 new replicas have been updated...
deployment "todoapi" successfully rolled out

Перевіримо версію:

Перевірка версії після rollback
$ curl http://localhost:8080/health
{"status":"healthy","version":"1.0.0"}
# Endpoint /stats більше не існує
$ curl http://localhost:8080/stats
HTTP/1.1 404 Not Found

Rollback виконано за 15-20 секунд! Застосунок повернувся до стабільної версії 1.0.

Перегляд історії після rollback

kubectl rollout history
$ kubectl rollout history deployment/todoapi
deployment.apps/todoapi
REVISION CHANGE-CAUSE
2 kubectl set image deployment/todoapi todoapi=todoapi:2.0.0 --record=true
3

Що сталося з ревізіями?

  • Ревізія 1 (v1.0) зникла — вона стала ревізією 3 після rollback
  • Ревізія 2 (v2.0) залишилась у історії
  • Ревізія 3 — це знову v1.0 (результат rollback)

Kubernetes не видаляє ревізії, а створює нову з тим самим шаблоном Pod.


Troubleshooting: типові проблеми та їх вирішення

Тепер розглянемо найчастіші проблеми при rolling updates та як їх діагностувати.

Проблема 1: Оновлення зависло (Progressing: False)

Симптоми:

kubectl get deployments
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
todoapi 2/3 1 2 10m

Бачимо 2/3 — лише 2 з 3 реплік готові. Оновлення не завершується.

Діагностика:

kubectl describe deployment
$ kubectl describe deployment todoapi
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing False ProgressDeadlineExceeded
Events:
Type Reason Age Message
---- ------ ---- -------
Warning FailedCreate 5m Error creating: pods "todoapi-xxx" is forbidden: exceeded quota

Причина: ProgressDeadlineExceeded — оновлення не досягло прогресу за progressDeadlineSeconds.

Можливі причини:

Новий Pod не проходить readiness probe

Перевірка:

kubectl get pods
# NAME                       READY   STATUS    RESTARTS   AGE
# todoapi-def456-aaa         0/1     Running   0          5m

Pod у стані Running, але READY = 0/1 — readiness probe fails.

Рішення:

  1. Переглянути логи: kubectl logs todoapi-def456-aaa
  2. Перевірити readiness probe endpoint вручну: kubectl exec -it todoapi-def456-aaa -- curl localhost:8080/health/ready
  3. Виправити проблему (наприклад, БД недоступна) або відкотити: kubectl rollout undo deployment/todoapi

Недостатньо ресурсів на вузлах

Перевірка:

kubectl describe pod todoapi-def456-aaa
# Events:
#   Warning  FailedScheduling  5m  0/3 nodes are available: insufficient memory

Рішення:

  1. Зменшити resources.requests у Deployment
  2. Додати більше вузлів до кластера
  3. Видалити непотрібні Pod для звільнення ресурсів

Образ не може завантажитись

Перевірка:

kubectl describe pod todoapi-def456-aaa
# Events:
#   Warning  Failed  5m  Failed to pull image "todoapi:2.0.0": rpc error: code = Unknown desc = Error response from daemon: pull access denied

Рішення:

  1. Перевірити, чи існує образ: docker images | grep todoapi
  2. Перевірити imagePullPolicy (для Minikube має бути Never)
  3. Перевірити credentials для private registry

Проблема 2: Pod постійно перезапускаються (CrashLoopBackOff)

Симптоми:

kubectl get pods
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
todoapi-def456-aaa 0/1 CrashLoopBackOff 5 3m

Діагностика:

kubectl describe pod
$ kubectl describe pod todoapi-def456-aaa
Last State: Terminated
Reason: Error
Exit Code: 1
Started: Fri, 09 May 2026 20:50:00 +0000
Finished: Fri, 09 May 2026 20:50:05 +0000

Перегляд логів:

kubectl logs
$ kubectl logs todoapi-def456-aaa
Unhandled exception. System.InvalidOperationException: Unable to resolve service for type 'MyService'

Можливі причини:

  • Помилка у коді (exception при старті)
  • Відсутня залежність (DI не може resolve service)
  • Неправильна конфігурація (змінні оточення, ConfigMap)
  • OOMKilled (перевищено memory limits)

Рішення:

  1. Виправити код та зібрати новий образ
  2. Або виконати rollback: kubectl rollout undo deployment/todoapi

Проблема 3: Оновлення занадто повільне

Симптоми: Rolling update займає 10+ хвилин для 10 реплік.

Причина: Обережні налаштування maxSurge та maxUnavailable.

Поточна конфігурація:

strategy:
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0

Це означає: оновлювати по 1 Pod за раз, завжди тримати всі репліки доступними.

Рішення: Збільшити maxSurge для швидшого оновлення:

strategy:
  rollingUpdate:
    maxSurge: 50%      # Було: 1
    maxUnavailable: 0

Тепер Kubernetes створить 50% нових Pod одразу (5 з 10), що прискорить оновлення.

Проблема 4: Downtime під час оновлення

Симптоми: Користувачі отримують помилки 503 під час rolling update.

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

Діагностика:

Перевірте maxUnavailable:

strategy:
  rollingUpdate:
    maxSurge: 0
    maxUnavailable: 1

Якщо maxSurge: 0, Kubernetes спочатку видаляє старий Pod, потім створює новий. Є момент, коли реплік менше, ніж потрібно.

Рішення: Встановити maxSurge > 0 та maxUnavailable: 0:

strategy:
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0

Тепер Kubernetes спочатку створює новий Pod, чекає його готовності, і лише потім видаляє старий. Завжди є повна кількість реплік.

Проблема 5: Старі Pod не видаляються

Симптоми: Після оновлення залишаються старі Pod у стані Terminating.

kubectl get pods
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
todoapi-abc123-xxx 1/1 Terminating 0 10m
todoapi-def456-aaa 1/1 Running 0 2m

Причина: Pod не завершується gracefully (не обробляє SIGTERM).

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

  1. Kubernetes надсилає SIGTERM контейнеру
  2. Контейнер має 30 секунд (за замовчуванням) для graceful shutdown
  3. Якщо контейнер не завершується — Kubernetes надсилає SIGKILL (force kill)

Рішення для .NET:

Додати обробку graceful shutdown:

var builder = WebApplication.CreateBuilder(args);

// ... конфігурація ...

var app = builder.Build();

// Налаштування graceful shutdown
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();

lifetime.ApplicationStopping.Register(() =>
{
    Console.WriteLine("Application is stopping. Finishing current requests...");
    // Тут можна закрити з'єднання з БД, flush кеші тощо
});

app.Run();

Також можна збільшити terminationGracePeriodSeconds у Deployment:

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60  # За замовчуванням 30
      containers:
        - name: todoapi
          image: todoapi:2.0.0

Корисні команди для debugging

# Статус Deployment
kubectl get deployment todoapi

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

# Статус rollout
kubectl rollout status deployment/todoapi

# Історія ревізій
kubectl rollout history deployment/todoapi

# Детальна інформація про ревізію
kubectl rollout history deployment/todoapi --revision=3

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

Тепер виконайте завдання для закріплення знань про rolling updates.

Завдання 1: Експерименти з maxSurge та maxUnavailable

Мета: Зрозуміти, як різні комбінації параметрів впливають на швидкість та безпеку оновлення.

Завдання:

  1. Створіть Deployment з 5 репліками nginx
  2. Виконайте 3 оновлення з різними налаштуваннями:
    • maxSurge: 0, maxUnavailable: 1 (повільне, економне)
    • maxSurge: 1, maxUnavailable: 0 (безпечне, zero-downtime)
    • maxSurge: 100%, maxUnavailable: 0 (швидке, ресурсомістке)
  3. Для кожного оновлення виміряйте:
    • Час оновлення (від початку до завершення)
    • Максимальну кількість Pod під час оновлення
    • Мінімальну кількість доступних Pod
  4. Порівняйте результати

Очікуваний результат: Ви побачите, як різні налаштування впливають на швидкість та ресурси.


Завдання 2: Симуляція невдалого оновлення

Мета: Навчитись діагностувати та виправляти проблеми при rolling update.

Завдання:

  1. Створіть Deployment з образом, який не існує (наприклад, nginx:nonexistent)
  2. Спостерігайте, як Kubernetes намагається оновити Pod
  3. Діагностуйте проблему через kubectl describe pod
  4. Виконайте rollback до робочої версії
  5. Перевірте, що застосунок знову працює

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


Завдання 3: Canary Deployment

Мета: Навчитись виконувати canary deployment — оновлення з поступовою перевіркою.

Завдання:

  1. Створіть Deployment з 10 репліками nginx:1.25
  2. Призупиніть rollout
  3. Оновіть образ на nginx:1.26 (створюється лише 1 новий Pod)
  4. Перевірте метрики нового Pod (логи, CPU, пам'ять)
  5. Якщо все добре — відновіть rollout для оновлення решти Pod
  6. Якщо є проблеми — виконайте rollback

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


Завдання 4: Blue-Green Deployment

Мета: Навчитись виконувати blue-green deployment — миттєве перемикання між версіями.

Завдання:

  1. Створіть два Deployment: app-blue (v1.0) та app-green (v2.0)
  2. Створіть Service, який спрямовує трафік на app-blue
  3. Перевірте, що трафік йде на v1.0
  4. Змініть Service selector на app-green
  5. Перевірте, що трафік миттєво перемкнувся на v2.0
  6. Якщо є проблеми — поверніть selector на app-blue

Очікуваний результат: Ви навчитесь виконувати миттєве перемикання між версіями без rolling update.


Резюме

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

Проблема оновлення без downtime

Чому наївний підхід (видалити всі Pod → створити нові) неприйнятний для production. Потрібен механізм поступового оновлення.

Що таке Rolling Update

Стратегія оновлення, при якій старі Pod поступово замінюються новими, завжди залишаючи мінімальну кількість працюючих реплік. Zero-downtime гарантовано.

Покрокова візуалізація

Детальний розбір кожного кроку rolling update з PlantUML діаграмами: від ініціації до завершення. Розуміння внутрішньої роботи Kubernetes.

Стратегії оновлення

RollingUpdate (поступове, zero-downtime) vs Recreate (швидке, з downtime). Коли використовувати кожну стратегію.

Параметри maxSurge та maxUnavailable

Детальний розбір з математичними розрахунками. Як різні комбінації впливають на швидкість, ресурси та безпеку оновлення.

progressDeadlineSeconds та minReadySeconds

Додаткові параметри для контролю життєвого циклу. Як запобігти зависанню оновлення та перевірити стабільність нових Pod.

Health Checks для .NET

Детальна реалізація liveness, readiness та startup probes для ASP.NET Core. Різниця між ними та правильні налаштування.

Resource Management для .NET

Особливості .NET GC у Kubernetes. Як правильно налаштувати requests/limits, щоб уникнути OOMKilled та CPU throttling.

Rollback та історія версій

Як Kubernetes зберігає історію ревізій. Швидкий rollback до попередньої версії або конкретної ревізії. Canary та blue-green deployments.

Практичний приклад: TodoApi v1.0 → v2.0

Реальне оновлення ASP.NET Core застосунку з додаванням нового endpoint. Повний цикл: збірка, rolling update, тестування, rollback.

Troubleshooting

Типові проблеми та їх вирішення: зависле оновлення, CrashLoopBackOff, повільний rollout, downtime, Pod не видаляються. Корисні команди для debugging.

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

4 завдання для закріплення знань: експерименти з параметрами, симуляція невдалого оновлення, canary deployment, blue-green deployment.

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

  1. Rolling Update — стандарт для production — завжди використовуйте RollingUpdate стратегію для stateless застосунків. Recreate лише для особливих випадків.
  2. maxSurge та maxUnavailable контролюють все — правильний підбір цих параметрів критично важливий. Для zero-downtime: maxSurge > 0, maxUnavailable = 0.
  3. Health checks обов'язкові — без readiness probe rolling update не працює правильно. Liveness probe запобігає deadlock. Startup probe для повільних застосунків.
  4. .NET потребує особливої уваги — обов'язково встановлюйте DOTNET_RUNNING_IN_CONTAINER=true. GC має знати про memory limits.
  5. Rollback має бути швидким — зберігайте достатню кількість ревізій (revisionHistoryLimit). Образи кешуються на вузлах для швидкого rollback.
  6. Моніторинг критично важливий — стежте за kubectl rollout status, логами, метриками. Виявляйте проблеми на ранній стадії.
  7. Canary та blue-green для критичних оновлень — для важливих застосунків використовуйте поступове розгортання з перевіркою на малій кількості Pod.

Що далі?

Ви вивчили основи Deployment та rolling updates. Наступні теми для поглибленого вивчення:

  • Service та Ingress — як організувати мережевий доступ до Pod
  • ConfigMap та Secret — управління конфігурацією та секретами
  • StatefulSet — для stateful застосунків (бази даних)
  • HorizontalPodAutoscaler — автоматичне масштабування на основі метрик
  • Helm — пакетний менеджер для Kubernetes
  • GitOps — автоматизація розгортання через Git (ArgoCD, Flux)

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

Для швидкого доступу — всі команди для роботи з rolling updates:

# Оновлення образу
kubectl set image deployment/<name> <container>=<image>:<tag>

# Оновлення з записом у історію
kubectl set image deployment/<name> <container>=<image>:<tag> --record

# Моніторинг процесу
kubectl rollout status deployment/<name>

# Спостереження за Pod у реальному часі
kubectl get pods -l app=<name> -w

# Призупинити оновлення
kubectl rollout pause deployment/<name>

# Відновити оновлення
kubectl rollout resume deployment/<name>

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

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

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

Performing a Rolling Update

Офіційний туторіал з rolling updates від Kubernetes.

Deployment Strategies

Детальний опис стратегій оновлення: RollingUpdate та Recreate.

ASP.NET Core Health Checks

Офіційна документація Microsoft про health checks у ASP.NET Core.

.NET in Containers

Best practices для запуску .NET застосунків у контейнерах.

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