У попередній статті ми навчилися створювати Deployment, масштабувати його та використовувати self-healing. Але залишилося найважливіше питання: як оновити застосунок на нову версію без зупинки сервісу?
Уявіть, що ваш TodoApi працює у production з 3 репліками. Ви виправили критичний баг та хочете розгорнути нову версію. Які у вас варіанти?
Варіант 1: "Наївний" підхід (з downtime)
Проблема: Є період (30-60 секунд), коли жоден Pod не працює. Користувачі отримують помилки 503 Service Unavailable. Це неприйнятно для production.
Варіант 2: Rolling Update (без downtime)
Переваги: Завжди є працюючі Pod. Користувачі не помічають оновлення. Якщо нова версія має баг — можна швидко повернутись до старої.
Саме це і робить Rolling Update.
Rolling Update — це стратегія оновлення Deployment, при якій старі Pod поступово замінюються новими, завжди залишаючи мінімальну кількість працюючих реплік. Це гарантує zero-downtime deployment — оновлення без зупинки сервісу.
Zero-downtime
Поступовість
Контрольованість
maxSurge та maxUnavailable. Можна зробити оновлення швидким (багато Pod одразу) або обережним (по одному Pod).Автоматичний rollback
Давайте детально розберемо, що відбувається під час Rolling Update. Візьмемо приклад: Deployment з 3 репліками оновлюється з версії 1.0 на версію 2.0.
Стан: 3 Pod з версією 1.0 працюють нормально. Сервіс обробляє запити користувачів.
Користувач змінює версію образу у Deployment:
Що відбувається всередині Kubernetes:
Важливо: Deployment Controller виявляє, що spec.template змінився (образ todoapi:1.0.0 → todoapi:2.0.0). Це сигнал для створення нового ReplicaSet.
Deployment Controller створює новий ReplicaSet для версії 2.0:
Стан: Тепер є два ReplicaSet:
Deployment Controller починає rolling update:
replicas нового ReplicaSet на 1 (0 → 1)replicas старого ReplicaSet на 1 (3 → 2)Стан:
Важливо: Kubernetes не чекає, поки старий Pod завершиться. Він одразу створює новий Pod паралельно. Це прискорює оновлення.
Новий Pod проходить lifecycle:
Критично важливо: Deployment Controller чекає, поки новий Pod стане Ready (пройде readiness probe), перед тим як продовжити оновлення. Якщо Pod не стає готовим протягом progressDeadlineSeconds (за замовчуванням 600 секунд) — оновлення зупиняється.
Після того, як Pod 4 (v2.0) став готовим, Deployment Controller продовжує оновлення:
Стан:
Після того, як Pod 5 (v2.0) став готовим:
Стан:
Після того, як Pod 6 (v2.0) став готовим, rolling update завершено:
Результат: Всі Pod оновлені до версії 2.0. Старий ReplicaSet зберігається з replicas: 0 для можливості швидкого rollback.
replicas старого ReplicaSet (0 → 3)replicas нового ReplicaSet (3 → 0)Тепер об'єднаємо всі кроки в одну sequence diagram:
Ключові моменти:
maxSurge/maxUnavailable)ReadyKubernetes підтримує дві стратегії оновлення Deployment:
Поступове оновлення, яке ми щойно розглянули. Це рекомендована стратегія для більшості застосунків.
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
Переваги:
Недоліки:
Коли використовувати:
Спочатку видаляються всі старі Pod, потім створюються нові. Є період downtime.
spec:
strategy:
type: Recreate
Переваги:
Недоліки:
Коли використовувати:
Тепер розберемо найважливіші параметри, які контролюють швидкість та безпеку rolling update.
Максимальна кількість Pod, які можуть бути недоступними (відключеними) під час оновлення.
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, інакше оновлення неможливе (не можна ні видалити старі, ні створити нові понад ліміт).Максимальна кількість додаткових 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: 50%
maxUnavailable: 0
Поведінка: Створюються багато нових Pod одразу (до 50% понад replicas), старі видаляються лише після готовності нових. Завжди є всі репліки доступними.
Приклад (replicas: 10):
Переваги: Найшвидше оновлення, zero-downtime гарантовано
Недоліки: Потребує 150% ресурсів (CPU, пам'ять) під час оновлення
Повільне оновлення, мало ресурсів
maxSurge: 0
maxUnavailable: 25%
Поведінка: Спочатку видаляються старі Pod (до 25%), потім створюються нові. Економить ресурси, але є період зниженої доступності.
Приклад (replicas: 10):
Переваги: Не потребує додаткових ресурсів
Недоліки: Повільніше, є період зниженої доступності (7 замість 10 Pod)
Збалансований підхід (за замовчуванням)
maxSurge: 25%
maxUnavailable: 25%
Поведінка: Компроміс між швидкістю та ресурсами. Можна створити до 25% додаткових Pod та видалити до 25% старих одночасно.
Приклад (replicas: 10):
Переваги: Баланс між швидкістю та ресурсами
Недоліки: Не найшвидше, не найекономніше
Обережне оновлення (по одному)
maxSurge: 1
maxUnavailable: 0
Поведінка: Оновлення по одному Pod за раз. Завжди є всі репліки доступними. Найбезпечніший підхід.
Приклад (replicas: 10):
Переваги: Максимальна безпека, легко виявити проблеми на ранній стадії
Недоліки: Найповільніше оновлення (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 > 0, maxUnavailable = 0 (завжди повна доступність)maxSurge = 0, maxUnavailable > 0 (без додаткових Pod)Окрім maxSurge та maxUnavailable, є ще кілька важливих параметрів:
Максимальний час (у секундах), протягом якого Deployment має показати хоча б якийсь рух вперед (прогрес) під час оновлення чи розгортання.
progressDeadlineSeconds: 300 (5 хвилин) система засікає час. Якщо за ці 5 хвилин жоден новий Pod не зміг успішно запуститись і стати Ready (тобто не було жодного прогресу), Kubernetes зупинить безглузді спроби, переведе статус оновлення в ProgressDeadlineExceeded (перевищено термін прогресу), що дозволить вашому автоматичному CI/CD скрипту чи ArgoCD одразу зрозуміти проблему та ініціювати відкат (rollback) до попередньої стабільної версії.За замовчуванням: 600 (10 хвилин)
Що вважається "прогресом"?
ReadyЩо відбувається при перевищенні таймауту?
Якщо за 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 секунд"Мінімальний час (у секундах), протягом якого новостворений Pod має безперервно працювати у стані "Ready" (готовий приймати трафік) без жодних падінь і перезапусків, перш ніж Kubernetes вважатиме його повністю стабільним і продовжить оновлювати інші репліки.
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
Що відбувається:
Ready=TrueВізуалізація:
Коли використовувати:
Типові значення:
0 — для простих застосунків (за замовчуванням)10-30 — для більшості веб-застосунків60-120 — для складних застосунків з довгою ініціалізацієюТепер розглянемо, як правильно налаштувати health checks для ASP.NET Core застосунків у Kubernetes.
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" — якщо є проблемиДля 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 Probe (/health/live)
Мета: Перевірити, чи застосунок живий (не deadlock, не crash).
Що перевіряти:
Що НЕ перевіряти:
Приклад:
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false // Лише базова перевірка
});
Результат: Завжди повертає 200 OK, якщо процес працює.
Readiness Probe (/health/ready)
Мета: Перевірити, чи застосунок готовий приймати трафік.
Що перевіряти:
Приклад:
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("db") || check.Tags.Contains("external")
});
Результат: Повертає 200 OK лише якщо БД доступна та зовнішні API працюють.
Тепер налаштуємо 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 не пройде. Це дає застосунку достатньо часу на старт.Коли використовувати:Liveness endpoint (/health/live):
Readiness endpoint (/health/ready):
Детальний endpoint (/health/detailed):
Правильне налаштування ресурсів критично важливе для стабільності .NET застосунків у Kubernetes.
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) контейнер.
Симптоми:
OOMKilled у kubectl describe podRESTARTS збільшуєтьсяПриклад:
resources:
limits:
memory: "256Mi"
Якщо застосунок спробує виділити 300 MB, Kubernetes вб'є контейнер з exit code 137.
Як виявити:
kubectl describe pod <pod-name>
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
.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:
DOTNET_RUNNING_IN_CONTAINER=true — це критично важливоdotnet-counters або Application Insightsresources:
requests:
memory: "128Mi" # Мінімум для роботи
cpu: "100m"
limits:
memory: "256Mi" # Максимум (GC використає ~192 MB)
cpu: "500m"
Як визначити правильні значення requests та limits?
Крок 1: Запустити без limits та виміряти
resources:
requests:
memory: "128Mi"
cpu: "100m"
# Без limits — дозволити використовувати скільки потрібно
Крок 2: Згенерувати навантаження та виміряти споживання
Крок 3: Встановити limits з запасом
На основі вимірювань:
600m (запас 25%)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
Одна з найважливіших можливостей Deployment — швидкий rollback (повернення до попередньої версії).
Кожна зміна у spec.template створює нову ревізію (revision) Deployment. Kubernetes зберігає старі ReplicaSet для можливості rollback.
Скільки ревізій зберігається?
За замовчуванням Kubernetes зберігає 10 останніх ревізій. Це контролюється параметром revisionHistoryLimit:
spec:
revisionHistoryLimit: 10 # За замовчуванням
Якщо встановити 0 — rollback буде неможливий (старі ReplicaSet видаляються одразу).
Переглянемо історію оновлень Deployment:
Що означають колонки:
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 для цієї ревізії.
Якщо нова версія має баг, можна швидко повернутись до попередньої:
Що відбувається:
replicas старого ReplicaSet (revision 2) з 0 до 3replicas поточного ReplicaSet (revision 3) з 3 до 0Швидкість rollback:
Rollback зазвичай займає 10-20 секунд, бо:
Можна повернутись не лише до попередньої, а до будь-якої ревізії:
Це повертає Deployment до ревізії 1 (самої першої версії).
Під час оновлення або rollback можна стежити за прогресом:
Ця команда блокується до завершення rollout. Корисно для CI/CD pipelines.
Іноді потрібно призупинити оновлення (наприклад, для 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 (з попередньої статті):
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);
Тепер додамо новий 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:
/health endpoint (1.0.0 → 2.0.0)/stats з детальною статистикоюЗберемо образ версії 2.0:
Тепер оновимо Deployment на нову версію:
Стежимо за прогресом:
Спостерігаємо за Pod у реальному часі:
Бачимо класичний rolling update: нові Pod створюються, старі видаляються по черзі.
Після завершення оновлення протестуємо новий endpoint:
Новий endpoint /stats працює! Оновлення успішне.
Тепер уявімо, що версія 2.0 має критичний баг (наприклад, endpoint /stats падає під навантаженням). Потрібно швидко повернутись до версії 1.0.
Перевіримо версію:
Rollback виконано за 15-20 секунд! Застосунок повернувся до стабільної версії 1.0.
Що сталося з ревізіями?
Kubernetes не видаляє ревізії, а створює нову з тим самим шаблоном Pod.
Тепер розглянемо найчастіші проблеми при rolling updates та як їх діагностувати.
Симптоми:
Бачимо 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-aaakubectl exec -it todoapi-def456-aaa -- curl localhost:8080/health/readykubectl rollout undo deployment/todoapiНедостатньо ресурсів на вузлах
Перевірка:
kubectl describe pod todoapi-def456-aaa
# Events:
# Warning FailedScheduling 5m 0/3 nodes are available: insufficient memory
Рішення:
resources.requests у DeploymentОбраз не може завантажитись
Перевірка:
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 todoapiimagePullPolicy (для Minikube має бути Never)Симптоми:
Діагностика:
Перегляд логів:
Можливі причини:
Рішення:
kubectl rollout undo deployment/todoapiСимптоми: 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), що прискорить оновлення.
Симптоми: Користувачі отримують помилки 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, чекає його готовності, і лише потім видаляє старий. Завжди є повна кількість реплік.
Симптоми: Після оновлення залишаються старі Pod у стані Terminating.
Причина: Pod не завершується gracefully (не обробляє SIGTERM).
Що відбувається:
Рішення для .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
# Статус 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.
Мета: Зрозуміти, як різні комбінації параметрів впливають на швидкість та безпеку оновлення.
Завдання:
maxSurge: 0, maxUnavailable: 1 (повільне, економне)maxSurge: 1, maxUnavailable: 0 (безпечне, zero-downtime)maxSurge: 100%, maxUnavailable: 0 (швидке, ресурсомістке)Очікуваний результат: Ви побачите, як різні налаштування впливають на швидкість та ресурси.
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 |
Мета: Навчитись діагностувати та виправляти проблеми при rolling update.
Завдання:
nginx:nonexistent)kubectl describe podОчікуваний результат: Ви навчитесь виявляти проблеми з образами та швидко відкочувати оновлення.
Команди:
# Створення робочого 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
Ключові моменти:
ImagePullBackOff або ErrImagePullМета: Навчитись виконувати canary deployment — оновлення з поступовою перевіркою.
Завдання:
Очікуваний результат: Ви навчитесь безпечно оновлювати застосунки, перевіряючи нову версію на малій кількості 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:
Мета: Навчитись виконувати blue-green deployment — миттєве перемикання між версіями.
Завдання:
app-blue (v1.0) та app-green (v2.0)app-blueapp-greenapp-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:
Недоліки:
У цій статті ми детально вивчили Rolling Updates та управління життєвим циклом Deployment. Ось що ми розглянули:
Проблема оновлення без downtime
Що таке Rolling Update
Покрокова візуалізація
Стратегії оновлення
Параметри maxSurge та maxUnavailable
progressDeadlineSeconds та minReadySeconds
Health Checks для .NET
Resource Management для .NET
Rollback та історія версій
Практичний приклад: TodoApi v1.0 → v2.0
Troubleshooting
Практичні завдання
maxSurge > 0, maxUnavailable = 0.DOTNET_RUNNING_IN_CONTAINER=true. GC має знати про memory limits.revisionHistoryLimit). Образи кешуються на вузлах для швидкого rollback.kubectl rollout status, логами, метриками. Виявляйте проблеми на ранній стадії.Ви вивчили основи Deployment та rolling updates. Наступні теми для поглибленого вивчення:
Для швидкого доступу — всі команди для роботи з 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