Rolling Updates та управління життєвим циклом Deployment
Rolling Updates та управління життєвим циклом Deployment
Проблема: як оновити застосунок без downtime?
У попередній статті ми навчилися створювати Deployment, масштабувати його та використовувати self-healing. Але залишилося найважливіше питання: як оновити застосунок на нову версію без зупинки сервісу?
Сценарій: оновлення веб-застосунку у production
Уявіть, що ваш TodoApi працює у production з 3 репліками. Ви виправили критичний баг та хочете розгорнути нову версію. Які у вас варіанти?
Варіант 1: "Наївний" підхід (з downtime)
Проблема: Є період (30-60 секунд), коли жоден Pod не працює. Користувачі отримують помилки 503 Service Unavailable. Це неприйнятно для production.
Варіант 2: Rolling Update (без downtime)
Переваги: Завжди є працюючі Pod. Користувачі не помічають оновлення. Якщо нова версія має баг — можна швидко повернутись до старої.
Саме це і робить Rolling Update.
Що таке Rolling Update: формальне визначення
Rolling Update — це стратегія оновлення Deployment, при якій старі Pod поступово замінюються новими, завжди залишаючи мінімальну кількість працюючих реплік. Це гарантує zero-downtime deployment — оновлення без зупинки сервісу.
Основні характеристики Rolling Update
Zero-downtime
Поступовість
Контрольованість
maxSurge та maxUnavailable. Можна зробити оновлення швидким (багато Pod одразу) або обережним (по одному Pod).Автоматичний rollback
Як працює Rolling Update: покрокова візуалізація
Давайте детально розберемо, що відбувається під час Rolling Update. Візьмемо приклад: Deployment з 3 репліками оновлюється з версії 1.0 на версію 2.0.
Початковий стан
Стан: 3 Pod з версією 1.0 працюють нормально. Сервіс обробляє запити користувачів.
Крок 1: Користувач ініціює оновлення
Користувач змінює версію образу у Deployment:
Що відбувається всередині Kubernetes:
Важливо: Deployment Controller виявляє, що spec.template змінився (образ todoapi:1.0.0 → todoapi:2.0.0). Це сигнал для створення нового ReplicaSet.
Крок 2: Створення нового ReplicaSet
Deployment Controller створює новий ReplicaSet для версії 2.0:
Стан: Тепер є два ReplicaSet:
- Старий (v1.0): 3 репліки (працюють)
- Новий (v2.0): 0 реплік (поки що порожній)
Крок 3: Поступове масштабування (ітерація 1)
Deployment Controller починає rolling update:
- Збільшує
replicasнового ReplicaSet на 1 (0 → 1) - Зменшує
replicasстарого ReplicaSet на 1 (3 → 2)
Стан:
- 2 старі Pod працюють (v1.0)
- 1 старий Pod завершується (v1.0)
- 1 новий Pod створюється (v2.0)
Важливо: Kubernetes не чекає, поки старий Pod завершиться. Він одразу створює новий Pod паралельно. Це прискорює оновлення.
Крок 4: Очікування готовності нового Pod
Новий Pod проходить lifecycle:
- Завантаження образу
- Запуск контейнера
- Проходження readiness probe
Критично важливо: Deployment Controller чекає, поки новий Pod стане Ready (пройде readiness probe), перед тим як продовжити оновлення. Якщо Pod не стає готовим протягом progressDeadlineSeconds (за замовчуванням 600 секунд) — оновлення зупиняється.
Крок 5: Продовження rolling update (ітерація 2)
Після того, як Pod 4 (v2.0) став готовим, Deployment Controller продовжує оновлення:
Стан:
- 1 старий Pod працює (v1.0)
- 1 старий Pod завершується (v1.0)
- 1 новий Pod працює (v2.0)
- 1 новий Pod створюється (v2.0)
Крок 6: Завершення rolling update (ітерація 3)
Після того, як Pod 5 (v2.0) став готовим:
Стан:
- 0 старих Pod (останній завершується)
- 3 нові Pod (2 працюють, 1 створюється)
Крок 7: Фінальний стан
Після того, як Pod 6 (v2.0) став готовим, rolling update завершено:
Результат: Всі Pod оновлені до версії 2.0. Старий ReplicaSet зберігається з replicas: 0 для можливості швидкого rollback.
- Збільшить
replicasстарого ReplicaSet (0 → 3) - Зменшить
replicasнового ReplicaSet (3 → 0)
Повна візуалізація Rolling Update
Тепер об'єднаємо всі кроки в одну sequence diagram:
Ключові моменти:
- Поступовість — Pod оновлюються по одному (або по кілька, залежно від
maxSurge/maxUnavailable) - Очікування готовності — перед продовженням оновлення Kubernetes чекає, поки новий Pod стане
Ready - Паралельність — створення нового Pod та видалення старого відбуваються паралельно
- Кешування образів — після завантаження образу на вузол, наступні Pod стартують швидше
- 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)
Порівняння стратегій
Параметри 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
Приклади:
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)
25% від 10 = 2.5 → округлюється вниз до 2min_available = 10 - 2 = 8Означає: Те саме, що maxUnavailable: 2 — мінімум 8 Pod доступні.Чому округлення вниз? Kubernetes завжди округлює maxUnavailable вниз для безпеки — краще залишити більше доступних Pod, ніж менше.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)
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
Приклади:
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)
50% від 10 = 5max_pods = 10 + 5 = 15Означає: Під час оновлення максимум 15 Pod можуть існувати одночасно. Дуже швидке оновлення, але потребує багато ресурсів.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)
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
| maxSurge | maxUnavailable | min_available | max_pods | Діапазон Pod під час оновлення |
|---|---|---|---|---|
| 0 | 1 | 9 | 10 | 9-10 Pod |
| 0 | 25% | 8 | 10 | 8-10 Pod |
| 1 | 0 | 10 | 11 | 10-11 Pod |
| 1 | 1 | 9 | 11 | 9-11 Pod |
| 25% | 25% | 8 | 12 | 8-12 Pod |
| 50% | 0 | 10 | 15 | 10-15 Pod |
| 100% | 0 | 10 | 20 | 10-20 Pod |
Висновки:
- Більший maxSurge → швидше оновлення, але більше ресурсів
- Більший maxUnavailable → швидше оновлення, але менша доступність
- maxSurge=0, maxUnavailable=0 → неможливо (оновлення заблоковано)
- Для критичних сервісів:
maxSurge > 0, maxUnavailable = 0(завжди повна доступність) - Для економії ресурсів:
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
Що відбувається:
- Новий Pod стартує
- Pod проходить readiness probe →
Ready=True - Kubernetes чекає 30 секунд
- Якщо за ці 30 секунд Pod не впав → він вважається доступним, оновлення продовжується
- Якщо Pod впав → оновлення зупиняється
Візуалізація:
Коли використовувати:
- Застосунки з повільною ініціалізацією (підключення до БД, завантаження конфігурації)
- Застосунки, які можуть падати через кілька секунд після старту
- Критичні сервіси, де важлива стабільність
Типові значення:
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"
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):
Readiness endpoint (/health/ready):
Детальний endpoint (/health/detailed):
Resource Management для .NET застосунків
Правильне налаштування ресурсів критично важливе для стабільності .NET застосунків у Kubernetes.
Розуміння requests та limits
Що відбувається при перевищенні 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
- Завжди встановлюйте
DOTNET_RUNNING_IN_CONTAINER=true— це критично важливо - Встановлюйте memory limits — без них .NET може з'їсти всю пам'ять вузла
- Requests = 50-70% від limits — залишає запас для GC
- Моніторте 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: Згенерувати навантаження та виміряти споживання
Крок 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.
Скільки ревізій зберігається?
За замовчуванням Kubernetes зберігає 10 останніх ревізій. Це контролюється параметром revisionHistoryLimit:
spec:
revisionHistoryLimit: 10 # За замовчуванням
Якщо встановити 0 — rollback буде неможливий (старі ReplicaSet видаляються одразу).
Перегляд історії ревізій
Переглянемо історію оновлень Deployment:
Що означають колонки:
- REVISION — номер ревізії (збільшується з кожним оновленням)
- CHANGE-CAUSE — причина зміни (якщо вказана через annotation)
kubectl set image deployment/todoapi todoapi=todoapi:2.0.0 \
--record
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
Детальна інформація про ревізію
Переглянемо детальну інформацію про конкретну ревізію:
Тут ви бачите повну конфігурацію Pod для цієї ревізії.
Rollback до попередньої версії
Якщо нова версія має баг, можна швидко повернутись до попередньої:
Що відбувається:
- Kubernetes знаходить попередню ревізію (revision 2)
- Збільшує
replicasстарого ReplicaSet (revision 2) з 0 до 3 - Зменшує
replicasпоточного ReplicaSet (revision 3) з 3 до 0 - Виконує rolling update у зворотному напрямку
Швидкість rollback:
Rollback зазвичай займає 10-20 секунд, бо:
- Образ попередньої версії вже є на вузлах (кешовано)
- Не потрібно завантажувати образ з registry
- Kubernetes просто перемикає ReplicaSet
Rollback до конкретної ревізії
Можна повернутись не лише до попередньої, а до будь-якої ревізії:
Це повертає Deployment до ревізії 1 (самої першої версії).
Моніторинг процесу rollout
Під час оновлення або rollback можна стежити за прогресом:
Ця команда блокується до завершення rollout. Корисно для CI/CD pipelines.
Призупинення та відновлення rollout
Іноді потрібно призупинити оновлення (наприклад, для canary deployment):
Після паузи оновлення зупиняється. Можна внести кілька змін, а потім відновити:
Навіщо це потрібно?
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:
- Оновлено версію у
/healthendpoint (1.0.0 → 2.0.0) - Додано новий endpoint
/statsз детальною статистикою
Збірка нової версії
Зберемо образ версії 2.0:
Виконання rolling update
Тепер оновимо Deployment на нову версію:
Стежимо за прогресом:
Спостерігаємо за Pod у реальному часі:
Бачимо класичний rolling update: нові Pod створюються, старі видаляються по черзі.
Тестування нової версії
Після завершення оновлення протестуємо новий endpoint:
Новий endpoint /stats працює! Оновлення успішне.
Симуляція проблеми та rollback
Тепер уявімо, що версія 2.0 має критичний баг (наприклад, endpoint /stats падає під навантаженням). Потрібно швидко повернутись до версії 1.0.
Перевіримо версію:
Rollback виконано за 15-20 секунд! Застосунок повернувся до стабільної версії 1.0.
Перегляд історії після rollback
Що сталося з ревізіями?
- Ревізія 1 (v1.0) зникла — вона стала ревізією 3 після rollback
- Ревізія 2 (v2.0) залишилась у історії
- Ревізія 3 — це знову v1.0 (результат rollback)
Kubernetes не видаляє ревізії, а створює нову з тим самим шаблоном Pod.
Troubleshooting: типові проблеми та їх вирішення
Тепер розглянемо найчастіші проблеми при rolling updates та як їх діагностувати.
Проблема 1: Оновлення зависло (Progressing: False)
Симптоми:
Бачимо 2/3 — лише 2 з 3 реплік готові. Оновлення не завершується.
Діагностика:
Причина: 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.
Рішення:
- Переглянути логи:
kubectl logs todoapi-def456-aaa - Перевірити readiness probe endpoint вручну:
kubectl exec -it todoapi-def456-aaa -- curl localhost:8080/health/ready - Виправити проблему (наприклад, БД недоступна) або відкотити:
kubectl rollout undo deployment/todoapi
Недостатньо ресурсів на вузлах
Перевірка:
kubectl describe pod todoapi-def456-aaa
# Events:
# Warning FailedScheduling 5m 0/3 nodes are available: insufficient memory
Рішення:
- Зменшити
resources.requestsу Deployment - Додати більше вузлів до кластера
- Видалити непотрібні 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
Рішення:
- Перевірити, чи існує образ:
docker images | grep todoapi - Перевірити
imagePullPolicy(для Minikube має бутиNever) - Перевірити credentials для private registry
Проблема 2: Pod постійно перезапускаються (CrashLoopBackOff)
Симптоми:
Діагностика:
Перегляд логів:
Можливі причини:
- Помилка у коді (exception при старті)
- Відсутня залежність (DI не може resolve service)
- Неправильна конфігурація (змінні оточення, ConfigMap)
- OOMKilled (перевищено memory limits)
Рішення:
- Виправити код та зібрати новий образ
- Або виконати 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.
Причина: Pod не завершується gracefully (не обробляє SIGTERM).
Що відбувається:
- Kubernetes надсилає SIGTERM контейнеру
- Контейнер має 30 секунд (за замовчуванням) для graceful shutdown
- Якщо контейнер не завершується — 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
# Список Pod
kubectl get pods -l app=todoapi
# Спостереження в реальному часі
kubectl get pods -l app=todoapi -w
# Детальна інформація про Pod
kubectl describe pod <pod-name>
# Логи Pod
kubectl logs <pod-name>
# Логи попереднього контейнера (якщо Pod перезапустився)
kubectl logs <pod-name> --previous
# Виконати команду всередині Pod
kubectl exec -it <pod-name> -- /bin/bash
# Перевірити health endpoint
kubectl exec -it <pod-name> -- curl localhost:8080/health
# Копіювати файли з Pod
kubectl cp <pod-name>:/app/logs/app.log ./app.log
# Події кластера
kubectl get events --sort-by=.metadata.creationTimestamp
# Моніторинг ресурсів
kubectl top pods -l app=todoapi
# Rollback до попередньої версії
kubectl rollout undo deployment/todoapi
# Rollback до конкретної ревізії
kubectl rollout undo deployment/todoapi --to-revision=2
# Призупинити оновлення
kubectl rollout pause deployment/todoapi
# Відновити оновлення
kubectl rollout resume deployment/todoapi
Практичні завдання
Тепер виконайте завдання для закріплення знань про rolling updates.
Завдання 1: Експерименти з maxSurge та maxUnavailable
Мета: Зрозуміти, як різні комбінації параметрів впливають на швидкість та безпеку оновлення.
Завдання:
- Створіть Deployment з 5 репліками nginx
- Виконайте 3 оновлення з різними налаштуваннями:
maxSurge: 0, maxUnavailable: 1(повільне, економне)maxSurge: 1, maxUnavailable: 0(безпечне, zero-downtime)maxSurge: 100%, maxUnavailable: 0(швидке, ресурсомістке)
- Для кожного оновлення виміряйте:
- Час оновлення (від початку до завершення)
- Максимальну кількість Pod під час оновлення
- Мінімальну кількість доступних Pod
- Порівняйте результати
Очікуваний результат: Ви побачите, як різні налаштування впливають на швидкість та ресурси.
deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
Команди:
# Створення
kubectl apply -f deployment.yaml
# Тест 1: maxSurge=0, maxUnavailable=1
time kubectl set image deployment/nginx-test nginx=nginx:1.26
kubectl get pods -l app=nginx -w # Спостерігати
# Тест 2: maxSurge=1, maxUnavailable=0
kubectl patch deployment nginx-test -p '{"spec":{"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}'
time kubectl set image deployment/nginx-test nginx=nginx:1.27
kubectl get pods -l app=nginx -w
# Тест 3: maxSurge=100%, maxUnavailable=0
kubectl patch deployment nginx-test -p '{"spec":{"strategy":{"rollingUpdate":{"maxSurge":"100%","maxUnavailable":0}}}}'
time kubectl set image deployment/nginx-test nginx=nginx:1.25
kubectl get pods -l app=nginx -w
# Очищення
kubectl delete deployment nginx-test
Очікувані результати:
| Конфігурація | Час оновлення | Max Pod | Min Available |
|---|---|---|---|
| maxSurge=0, maxUnavailable=1 | ~60s | 5 | 4 |
| maxSurge=1, maxUnavailable=0 | ~75s | 6 | 5 |
| maxSurge=100%, maxUnavailable=0 | ~30s | 10 | 5 |
Завдання 2: Симуляція невдалого оновлення
Мета: Навчитись діагностувати та виправляти проблеми при rolling update.
Завдання:
- Створіть Deployment з образом, який не існує (наприклад,
nginx:nonexistent) - Спостерігайте, як Kubernetes намагається оновити Pod
- Діагностуйте проблему через
kubectl describe pod - Виконайте rollback до робочої версії
- Перевірте, що застосунок знову працює
Очікуваний результат: Ви навчитесь виявляти проблеми з образами та швидко відкочувати оновлення.
Команди:
# Створення робочого Deployment
kubectl create deployment nginx-test --image=nginx:1.27 --replicas=3
# Перевірка, що все працює
kubectl get pods -l app=nginx-test
# NAME READY STATUS RESTARTS AGE
# nginx-test-xxx-yyy 1/1 Running 0 10s
# nginx-test-xxx-zzz 1/1 Running 0 10s
# nginx-test-xxx-www 1/1 Running 0 10s
# Спроба оновлення на неіснуючий образ
kubectl set image deployment/nginx-test nginx=nginx:nonexistent --record
# Спостереження за процесом (оновлення зависне)
kubectl rollout status deployment/nginx-test
# Waiting for deployment "nginx-test" rollout to finish: 1 out of 3 new replicas have been updated...
# Перегляд Pod (новий Pod не може стартувати)
kubectl get pods -l app=nginx-test
# NAME READY STATUS RESTARTS AGE
# nginx-test-xxx-yyy 1/1 Running 0 2m
# nginx-test-xxx-zzz 1/1 Running 0 2m
# nginx-test-aaa-bbb 0/1 ImagePullBackOff 0 30s
# Діагностика проблеми
kubectl describe pod nginx-test-aaa-bbb
# Events:
# Warning Failed 30s Failed to pull image "nginx:nonexistent": rpc error: code = NotFound desc = manifest for nginx:nonexistent not found
# Rollback до попередньої версії
kubectl rollout undo deployment/nginx-test
# Перевірка, що rollback успішний
kubectl rollout status deployment/nginx-test
# deployment "nginx-test" successfully rolled out
kubectl get pods -l app=nginx-test
# NAME READY STATUS RESTARTS AGE
# nginx-test-xxx-yyy 1/1 Running 0 3m
# nginx-test-xxx-zzz 1/1 Running 0 3m
# nginx-test-xxx-www 1/1 Running 0 3m
# Очищення
kubectl delete deployment nginx-test
Ключові моменти:
- Kubernetes не відкочує автоматично — старі Pod продовжують працювати
- Нові Pod застрягають у стані
ImagePullBackOffабоErrImagePull - Rollback виконується швидко (10-15 секунд), бо образ вже є на вузлах
- Сервіс залишається доступним весь час (старі Pod працюють)
Завдання 3: Canary Deployment
Мета: Навчитись виконувати canary deployment — оновлення з поступовою перевіркою.
Завдання:
- Створіть Deployment з 10 репліками nginx:1.25
- Призупиніть rollout
- Оновіть образ на nginx:1.26 (створюється лише 1 новий Pod)
- Перевірте метрики нового Pod (логи, CPU, пам'ять)
- Якщо все добре — відновіть rollout для оновлення решти Pod
- Якщо є проблеми — виконайте rollback
Очікуваний результат: Ви навчитесь безпечно оновлювати застосунки, перевіряючи нову версію на малій кількості Pod перед повним rollout.
Команди:
# Створення Deployment з 10 репліками
kubectl create deployment nginx-canary --image=nginx:1.25 --replicas=10
# Перевірка, що все працює
kubectl get pods -l app=nginx-canary
# 10 Pod у стані Running
# Призупинити rollout
kubectl rollout pause deployment/nginx-canary
# Оновити образ (створюється лише 1 новий Pod через maxSurge)
kubectl set image deployment/nginx-canary nginx=nginx:1.26 --record
# Спостереження (лише 1 новий Pod створюється)
kubectl get pods -l app=nginx-canary
# NAME READY STATUS RESTARTS AGE
# nginx-canary-abc123-xxx 1/1 Running 0 2m (старий)
# nginx-canary-abc123-yyy 1/1 Running 0 2m (старий)
# ...
# nginx-canary-def456-aaa 1/1 Running 0 10s (новий!)
# Перевірка нового Pod
NEW_POD=$(kubectl get pods -l app=nginx-canary -o jsonpath='{.items[0].metadata.name}')
# Логи
kubectl logs $NEW_POD
# Ресурси
kubectl top pod $NEW_POD
# Тестування
kubectl exec -it $NEW_POD -- curl localhost
# Якщо все добре — продовжити rollout
kubectl rollout resume deployment/nginx-canary
# Спостереження за повним оновленням
kubectl rollout status deployment/nginx-canary
# Перевірка, що всі Pod оновлені
kubectl get pods -l app=nginx-canary
# Всі Pod мають новий hash (def456)
# Очищення
kubectl delete deployment nginx-canary
Альтернатива: Rollback якщо є проблеми
# Якщо новий Pod має проблеми
kubectl rollout undo deployment/nginx-canary
# Відновити rollout (щоб undo застосувався)
kubectl rollout resume deployment/nginx-canary
Переваги canary deployment:
- Ризик мінімальний — лише 1 Pod з новою версією
- Можна перевірити метрики, логи, поведінку під навантаженням
- Якщо є проблеми — вплив на 10% трафіку (1 з 10 Pod)
- Швидкий rollback — більшість Pod ще на старій версії
Завдання 4: Blue-Green Deployment
Мета: Навчитись виконувати blue-green deployment — миттєве перемикання між версіями.
Завдання:
- Створіть два Deployment:
app-blue(v1.0) таapp-green(v2.0) - Створіть Service, який спрямовує трафік на
app-blue - Перевірте, що трафік йде на v1.0
- Змініть Service selector на
app-green - Перевірте, що трафік миттєво перемкнувся на v2.0
- Якщо є проблеми — поверніть selector на
app-blue
Очікуваний результат: Ви навчитесь виконувати миттєве перемикання між версіями без rolling update.
app-blue-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
env:
- name: VERSION
value: "1.0.0"
app-green-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: nginx
image: nginx:1.26
ports:
- containerPort: 80
env:
- name: VERSION
value: "2.0.0"
service.yaml:
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
version: blue # Спочатку трафік на blue
ports:
- port: 80
targetPort: 80
type: ClusterIP
Команди:
# Створення обох Deployment
kubectl apply -f app-blue-deployment.yaml
kubectl apply -f app-green-deployment.yaml
# Створення Service (трафік на blue)
kubectl apply -f service.yaml
# Перевірка, що обидва Deployment працюють
kubectl get deployments
# NAME READY UP-TO-DATE AVAILABLE AGE
# app-blue 3/3 3 3 30s
# app-green 3/3 3 3 30s
# Тестування (трафік йде на blue - v1.0)
kubectl run test-pod --image=curlimages/curl --rm -it --restart=Never -- \
curl http://myapp-service
# Перемикання на green (v2.0)
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"green"}}}'
# Тестування (трафік миттєво перемкнувся на green - v2.0)
kubectl run test-pod --image=curlimages/curl --rm -it --restart=Never -- \
curl http://myapp-service
# Якщо є проблеми — повернутись на blue
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"blue"}}}'
# Після успішного тестування — видалити blue
kubectl delete deployment app-blue
# Очищення
kubectl delete deployment app-green
kubectl delete service myapp-service
Переваги blue-green deployment:
- Миттєве перемикання — зміна selector займає < 1 секунди
- Zero-downtime — обидві версії працюють, перемикання без простою
- Швидкий rollback — просто змінити selector назад
- Тестування у production — green працює, але не отримує трафік
Недоліки:
- Подвійні ресурси — потрібно тримати обидві версії одночасно
- Складніше для stateful застосунків — проблеми з БД, якщо схема змінилась
Резюме
У цій статті ми детально вивчили Rolling Updates та управління життєвим циклом Deployment. Ось що ми розглянули:
Проблема оновлення без downtime
Що таке Rolling Update
Покрокова візуалізація
Стратегії оновлення
Параметри maxSurge та maxUnavailable
progressDeadlineSeconds та minReadySeconds
Health Checks для .NET
Resource Management для .NET
Rollback та історія версій
Практичний приклад: TodoApi v1.0 → v2.0
Troubleshooting
Практичні завдання
Ключові висновки
- Rolling Update — стандарт для production — завжди використовуйте RollingUpdate стратегію для stateless застосунків. Recreate лише для особливих випадків.
- maxSurge та maxUnavailable контролюють все — правильний підбір цих параметрів критично важливий. Для zero-downtime:
maxSurge > 0, maxUnavailable = 0. - Health checks обов'язкові — без readiness probe rolling update не працює правильно. Liveness probe запобігає deadlock. Startup probe для повільних застосунків.
- .NET потребує особливої уваги — обов'язково встановлюйте
DOTNET_RUNNING_IN_CONTAINER=true. GC має знати про memory limits. - Rollback має бути швидким — зберігайте достатню кількість ревізій (
revisionHistoryLimit). Образи кешуються на вузлах для швидкого rollback. - Моніторинг критично важливий — стежте за
kubectl rollout status, логами, метриками. Виявляйте проблеми на ранній стадії. - 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>
# Rollback до попередньої версії
kubectl rollout undo deployment/<name>
# Rollback до конкретної ревізії
kubectl rollout undo deployment/<name> --to-revision=<number>
# Перегляд історії
kubectl rollout history deployment/<name>
# Детальна інформація про ревізію
kubectl rollout history deployment/<name> --revision=<number>
# Зміна maxSurge та maxUnavailable
kubectl patch deployment <name> -p '{"spec":{"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}'
# Зміна progressDeadlineSeconds
kubectl patch deployment <name> -p '{"spec":{"progressDeadlineSeconds":300}}'
# Зміна minReadySeconds
kubectl patch deployment <name> -p '{"spec":{"minReadySeconds":30}}'
# Зміна revisionHistoryLimit
kubectl patch deployment <name> -p '{"spec":{"revisionHistoryLimit":5}}'
# Перегляд стану Deployment
kubectl describe deployment <name>
# Перегляд ReplicaSet
kubectl get replicasets -l app=<name>
# Детальна інформація про ReplicaSet
kubectl describe replicaset <replicaset-name>
# Логи всіх Pod
kubectl logs -l app=<name> --tail=50
# Логи попереднього контейнера
kubectl logs <pod-name> --previous
# Події
kubectl get events --sort-by=.metadata.creationTimestamp --field-selector involvedObject.name=<name>
Додаткові ресурси
Попередня стаття: Deployment — декларативне управління Pod
Deployment — декларативне управління Pod
Від ручного управління Pod до автоматизованої оркестрації — self-healing, масштабування та декларативні оновлення
Service — мережева абстракція для Pod
Від ефемерних IP-адрес Pod до стабільних Service endpoints — service discovery, балансування навантаження та мережева архітектура Kubernetes