Pod — атомарна одиниця Kubernetes
Pod — атомарна одиниця Kubernetes
Від контейнера до Pod
У попередніх розділах ми навчилися працювати з Docker — створювати образи, запускати контейнери, об'єднувати їх через Docker Compose. Ви вже знаєте, що контейнер — це ізольований процес з власною файловою системою, мережею та ресурсами. Але чому Kubernetes не працює з контейнерами напряму? Чому не можна просто сказати «запусти контейнер mcr.microsoft.com/dotnet/aspnet:8.0», як у Docker?
Щоб відповісти на це питання, потрібно зрозуміти фундаментальну проблему, яку вирішує Kubernetes: оркестрація розподілених застосунків у production-середовищі. У реальних системах застосунок рідко складається з одного процесу. Часто потрібні допоміжні компоненти, які працюють поруч з основним застосунком і тісно з ним взаємодіють.
Проблема: коли одного контейнера недостатньо
Уявіть, що ви розгортаєте ASP.NET Core Web API у production. Окрім самого API, вам потрібно:
- Збирати логи — контейнер, який читає логи з файлу та відправляє їх у централізоване сховище (Elasticsearch, Loki)
- Проксіювати трафік — контейнер, який додає HTTPS, автентифікацію або моніторинг до кожного запиту
- Виконати міграції бази даних — контейнер, який запускається перед основним застосунком і застосовує зміни до схеми бази даних
Усі ці компоненти мають працювати разом, на одному сервері, з спільною мережею та спільними файлами. Якщо основний застосунок переміститься на інший сервер — допоміжні контейнери мають переміститися разом з ним. Якщо основний застосунок зупиниться — допоміжні контейнери теж мають зупинитися.
У Docker Compose ви вирішували це через depends_on та спільні мережі, але це працює лише на одному хості. У розподіленому кластері Kubernetes потрібна інша абстракція — Pod.
Що таке Pod: перше знайомство
Pod (від англ. «стручок», як стручок гороху з кількома горошинами всередині) — це найменша розгортана одиниця у Kubernetes. Pod може містити один або кілька контейнерів, які:
- Завжди розміщуються на одному сервері (вузлі кластера)
- Мають спільну мережу — бачать один одного через
localhost - Можуть мати спільні файлові системи (volumes)
- Запускаються та зупиняються разом
Візуалізація: Pod vs окремі контейнери
Щоб краще зрозуміти різницю, подивимося на діаграму:
На діаграмі видно ключову різницю:
- Docker: Кожен контейнер — окрема одиниця. Ви керуєте ними окремо через
docker run,docker stopтощо. - Kubernetes Pod: Група контейнерів, які Kubernetes розглядає як одне ціле. Один IP, спільна мережа, синхронізований життєвий цикл.
Навіщо це потрібно?
Розглянемо конкретний приклад. У вас є ASP.NET Core API, який пише логи у файл /var/log/app/app.log. Ви хочете відправляти ці логи в Elasticsearch для централізованого зберігання.
Підхід 1: Один контейнер Додати бібліотеку Serilog.Sinks.Elasticsearch у ваш C# код. Але тепер ваш застосунок залежить від Elasticsearch — якщо Elasticsearch недоступний, застосунок може падати або працювати повільно.
Підхід 2: Два контейнери в одному Pod
- Контейнер 1: Ваш ASP.NET Core API пише логи у файл (як завжди)
- Контейнер 2: Fluent Bit читає цей файл через спільний volume і відправляє в Elasticsearch
Переваги:
- Ваш застосунок не знає про Elasticsearch
- Можна замінити Fluent Bit на інший log shipper без зміни коду
- Якщо Elasticsearch недоступний — застосунок продовжує працювати, логи просто накопичуються
Це і є суть Pod — розділення відповідальності між контейнерами, які працюють разом.
Що таке Pod: формальне визначення
Тепер, коли ви розумієте навіщо потрібен Pod, давайте дамо точне визначення.
Pod — це найменша розгортана одиниця у Kubernetes, яка представляє один або кілька контейнерів, що:
node-1, всі його контейнери будуть на node-1.localhost і можуть використовувати одні й ті самі порти (але не конфліктувати між собою).Спільна мережа: як це працює
Коли Kubernetes створює Pod, він насправді створює спеціальний "інфраструктурний" контейнер (pause container), який резервує мережевий namespace. Всі інші контейнери Pod приєднуються до цього namespace. Результат:
- Pod отримує одну IP-адресу з мережі кластера (наприклад,
10.244.1.5) - Контейнери всередині Pod спілкуються через
localhost - Порти, які відкриває контейнер, доступні іншим контейнерам Pod через
localhost:port
http://localhost:5000. Не потрібно знати IP-адресу контейнера API, не потрібно налаштовувати Docker networks — все працює через localhost.Спільні volumes: обмін даними
Контейнери в Pod можуть монтувати одні й ті самі volumes. Це дозволяє їм обмінюватися файлами без мережевих запитів. Наприклад:
- Контейнер 1 (ASP.NET Core API) пише логи у
/var/log/app/app.log - Контейнер 2 (Fluent Bit) читає файл
/var/log/app/app.logта відправляє в Elasticsearch
Обидва контейнери монтують один і той самий volume у різні шляхи своєї файлової системи. Для контейнера 1 це /var/log/app, для контейнера 2 — теж /var/log/app (або будь-який інший шлях).
Анатомія Pod: структура YAML-маніфесту
Тепер, коли ви розумієте концепцію Pod, давайте навчимося описувати Pod у форматі YAML. Kubernetes використовує декларативний підхід — ви описуєте бажаний стан системи у YAML-файлі, а Kubernetes робить все необхідне, щоб досягти цього стану.
Ми розглянемо специфікацію Pod поступово: спочатку найпростіший приклад, потім додаватимемо поля одне за одним, пояснюючи кожне детально.
Мінімальний Pod
Найпростіший Pod містить один контейнер. Ось мінімально необхідна конфігурація:
apiVersion: v1
kind: Pod
metadata:
name: simple-pod
spec:
containers:
- name: nginx
image: nginx:1.27
Це все, що потрібно для створення Pod! Давайте розберемо кожне поле.
Версія схеми API Kubernetes, яку ви використовуєте для створення цього ресурсу.
На що це впливає? Вона вказує Kubernetes, за якими правилами (схемою валідації) розпізнавати та перевіряти поля у вашому YAML-файлі. Різні версії API можуть мати різні доступні поля. Якщо вказати версію неправильно, Kubernetes видасть помилку та не зможе розпарсити маніфест.
Аналогія з розробки:
Це дуже схоже на версіонування у звичайних Web API. Коли ви створюєте маршрути /api/v1/users та /api/v2/users, кожен з них очікує свій формат JSON-запиту. Так само і в Kubernetes: версія вказує на конкретний «контракт» даних.
Чому для Pod пишеться просто v1?
Pod — це фундаментальний («ядерний») ресурс, який існує з першого дня створення Kubernetes. Він належить до так званої Core API Group (базової групи). Для цієї групи префікс опускається, тому ми вказуємо лише версію схеми — v1.
Чому для Deployment пишеться apps/v1?
У Kubernetes є сотні різних ресурсів. Щоб не тримати їх усі в одній величезній схемі, їх розділили на логічні групи API (API Groups). Загальний формат запису виглядає як назва-групи/версія-схеми.
apps— це назва групи API, яка містить ресурси для управління робочими навантаженнями та життєвим циклом застосунків (Deployments, StatefulSets, DaemonSets).v1— це стабільна версія схеми для цієї конкретної групи.
v1 — це стабільна версія, яка існує з самого початку Kubernetes. Інші ресурси можуть мати інші версії (наприклад, apps/v1 для Deployment).Pod. Kubernetes підтримує багато типів ресурсів: Service, Deployment, ConfigMap тощо. Поле kind вказує, що саме ви створюєте.name.my-app, api-server-1, web-frontend.kubectl logs, kubectl exec тощо.[registry/]repository[:tag]. Якщо не вказано registry — використовується Docker Hub. Якщо не вказано tag — використовується latest (не рекомендується для production!). Приклади: nginx:1.27, mcr.microsoft.com/dotnet/aspnet:8.0, myregistry.azurecr.io/myapp:v1.2.3.Створення та перегляд Pod
Збережіть YAML у файл simple-pod.yaml та створіть Pod:
Що означають ці колонки:
- NAME: Ім'я Pod (з
metadata.name) - READY: Скільки контейнерів готові / скільки всього контейнерів (
1/1означає 1 з 1) - STATUS: Поточний стан Pod (
Running,Pending,Failedтощо) - RESTARTS: Скільки разів контейнери перезапускалися
- AGE: Скільки часу минуло з моменту створення Pod
Детальна інформація про Pod
Щоб побачити детальну інформацію про Pod, використовуйте kubectl describe:
Тут ви бачите:
- На якому вузлі (
Node) запущено Pod - IP-адресу Pod у мережі кластера
- Детальну інформацію про кожен контейнер
- Події (Events) — що відбувалося з Pod (завантаження образу, запуск контейнера тощо)
Базові поля Pod: розширюємо конфігурацію
Тепер додамо типові поля, які використовуються у більшості Pod.
Labels (мітки)
Labels — це key-value пари, які використовуються для ідентифікації та групування ресурсів. Вони критично важливі для роботи Service, Deployment та інших ресурсів Kubernetes.
apiVersion: v1
kind: Pod
metadata:
name: labeled-pod
labels:
app: myapp
tier: backend
environment: production
spec:
containers:
- name: app
image: myapp:1.0.0
app (назва застосунку), version (версія), tier (рівень: frontend/backend), environment (dev/staging/production).Навіщо потрібні labels?
Уявіть, що у вас 100 Pod різних застосунків. Як Service дізнається, до яких Pod направляти трафік? Через labels! Service має селектор app: myapp, і він знаходить всі Pod з такою міткою.
Ports (порти)
Декларація портів, які контейнер відкриває:
apiVersion: v1
kind: Pod
metadata:
name: web-pod
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
protocol: TCP
name: http
containerPort: 5000.TCP (за замовчуванням) або UDP. У більшості випадків використовується TCP.http, https, grpc, metrics.ports не відкриває порти назовні кластера! Воно лише документує, які порти використовує контейнер. Щоб зробити Pod доступним ззовні, потрібен Service (це тема наступних статей).Environment Variables (змінні оточення)
Змінні оточення — основний спосіб передачі конфігурації в контейнери:
apiVersion: v1
kind: Pod
metadata:
name: env-pod
spec:
containers:
- name: app
image: myapp:1.0.0
env:
- name: DATABASE_URL
value: 'postgresql://db:5432/mydb'
- name: LOG_LEVEL
value: 'info'
- name: ASPNETCORE_ENVIRONMENT
value: 'Production'
-e у docker run. Кожен елемент масиву має поля name (назва змінної) та value (значення).DATABASE_URL, API_KEY, PORT.appsettings.json. Наприклад, змінна ConnectionStrings__DefaultConnection перевизначить ConnectionStrings:DefaultConnection у JSON (зверніть увагу на подвійне підкреслення __ замість :).Resource Requests and Limits (ресурси)
Kubernetes дозволяє вказати, скільки CPU та пам'яті потрібно контейнеру:
apiVersion: v1
kind: Pod
metadata:
name: resource-pod
spec:
containers:
- name: app
image: myapp:1.0.0
resources:
requests:
memory: '128Mi'
cpu: '250m'
limits:
memory: '256Mi'
cpu: '500m'
requests (мінімум, який гарантується) та limits (максимум, який контейнер може використати).Ki (кібібайт), Mi (мебібайт), Gi (гібібайт). Це бінарні одиниці (1 Mi = 1024 Ki), а не десяткові. Приклади: 128Mi, 1Gi, 512Mi.m (міліядра). 1000m = 1 ядро CPU. 250m = 0.25 ядра (25% одного ядра). Можна писати як 0.25 або 250m — це еквівалентно. Приклади: 100m, 500m, 1, 2.requests.memory. Якщо контейнер спробує виділити більше пам'яті — Kubernetes вб'є процес (OOMKilled).requests.cpu. Якщо контейнер спробує використати більше CPU — він буде throttled (Linux CFS quota).- Requests — це те, що Kubernetes гарантує. Scheduler шукає вузол з достатньою кількістю вільних ресурсів.
- Limits — це те, що контейнер не може перевищити. Якщо перевищить пам'ять — буде вбитий. Якщо перевищить CPU — буде сповільнений.
requests: 128Mi, limits: 256Mi означає "гарантуй мені 128 МБ, але дозволь використати до 256 МБ, якщо на вузлі є вільна пам'ять".Command and Args (команда запуску)
За замовчуванням Kubernetes запускає команду, вказану в Dockerfile (ENTRYPOINT та CMD). Але ви можете перевизначити її:
apiVersion: v1
kind: Pod
metadata:
name: command-pod
spec:
containers:
- name: app
image: busybox:1.36
command: ['sh', '-c']
args: ['echo Hello from Kubernetes! && sleep 3600']
ENTRYPOINT з Dockerfile. Це команда, яка буде виконана при старті контейнера. Формат: масив рядків. Перший елемент — виконуваний файл, решта — аргументи. Приклад: ["dotnet", "MyApp.dll"].CMD з Dockerfile. Це аргументи, які передаються команді з command. Якщо command не вказано — args передаються ENTRYPOINT з Dockerfile. Формат: масив рядків. Приклад: ["--port", "8080", "--verbose"].- Якщо ви хочете повністю змінити команду запуску — використовуйте
command. - Якщо ви хочете лише додати аргументи до існуючої команди з Dockerfile — використовуйте
args. - Якщо вказано обидва —
commandстає виконуваним файлом,args— його аргументами.
ENTRYPOINT ["dotnet", "MyApp.dll"]. Ви можете додати аргументи через args: ["--environment", "Production"], і контейнер запуститься як dotnet MyApp.dll --environment Production.Повний приклад: Pod з усіма базовими полями
Тепер об'єднаємо все, що ми вивчили, в один Pod:
apiVersion: v1
kind: Pod
metadata:
name: full-example-pod
labels:
app: myapp
version: '1.0'
tier: backend
spec:
containers:
- name: app
image: mcr.microsoft.com/dotnet/samples:aspnetapp
ports:
- containerPort: 8080
protocol: TCP
name: http
env:
- name: ASPNETCORE_URLS
value: 'http://+:8080'
- name: ASPNETCORE_ENVIRONMENT
value: 'Production'
- name: ConnectionStrings__DefaultConnection
value: 'Server=db;Database=mydb;User=sa;Password=YourStrong@Passw0rd'
resources:
requests:
memory: '128Mi'
cpu: '250m'
limits:
memory: '512Mi'
cpu: '1000m'
command: ['./aspnetapp']
Цей Pod:
- Має назву
full-example-podта мітки для ідентифікації - Запускає один контейнер з готовим публічним демо-застосунком ASP.NET Core від Microsoft (
mcr.microsoft.com/dotnet/samples:aspnetapp), який можна запустити одразу без створення власного проєкту - Відкриває порт 8080 для HTTP-трафіку
- Налаштовує ASP.NET Core через змінні оточення (додатково передає тестовий рядок підключення)
- Гарантує 128 МБ пам'яті та 0.25 CPU, але дозволяє використати до 512 МБ та 1 CPU
- Запускає застосунок командою
./aspnetapp(перевизначаючи стандартну команду для демонстрації роботи поляcommand)
Створіть цей Pod та перевірте його стан:
Життєвий цикл Pod
Тепер, коли ви знаєте, як описати Pod у YAML, давайте розберемося, що відбувається з Pod від моменту створення до завершення. Розуміння життєвого циклу критично важливе для діагностики проблем та написання надійних застосунків.
Фази Pod
Kubernetes відстежує стан Pod через поле status.phase. Pod проходить через кілька фаз протягом свого життя:
Розглянемо кожну фазу детально:
- Час очікування планування (scheduler ще не призначив Pod вузлу)
- Час завантаження образів контейнерів з registry
- Час створення volumes
- Недостатньо ресурсів на вузлах (CPU, пам'ять)
- Образ контейнера завантажується (великий розмір)
- Помилка доступу до registry (неправильні credentials)
Стани контейнерів
Окрім фази Pod, кожен контейнер всередині Pod має власний стан. Це дає більш детальну інформацію про те, що відбувається:
- Завантажується образ з registry
- Очікується завершення init-контейнерів (про них у наступній статті)
- Контейнер не може запуститися через помилку конфігурації
reason вказує конкретну причину: ContainerCreating, ImagePullBackOff, CrashLoopBackOff.startedAt показує, коли контейнер запустився.exitCode— код завершення процесу (0 = успіх, інше = помилка)reason— причина завершення (Completed,Error,OOMKilled)startedAtтаfinishedAt— часові мітки
Переглянути детальний стан контейнерів можна через kubectl describe:
Політика перезапуску (restartPolicy)
Kubernetes може автоматично перезапускати контейнери, які завершились. Поведінка залежить від restartPolicy:
apiVersion: v1
kind: Pod
metadata:
name: restart-demo
spec:
restartPolicy: Always # Always | OnFailure | Never
containers:
- name: app
image: myapp:1.0
restartPolicy застосовується до всього Pod, а не до окремих контейнерів. Усі контейнери у Pod мають однаковий restartPolicy. Не можна налаштувати різні політики для різних контейнерів одного Pod.Backoff при перезапуску
Коли контейнер падає і перезапускається, Kubernetes не перезапускає його миттєво. Використовується exponential backoff (експоненційна затримка):
- Перший перезапуск: 10 секунд
- Другий перезапуск: 20 секунд
- Третій перезапуск: 40 секунд
- ...
- Максимум: 5 хвилин
Це запобігає ситуації, коли контейнер падає миттєво після запуску (наприклад, через неправильну конфігурацію) і створює нескінченний цикл перезапусків, навантажуючи систему.
Якщо ви бачите стан CrashLoopBackOff — це означає, що контейнер падає одразу після запуску, і Kubernetes чекає перед наступною спробою.
У цьому випадку потрібно виправити код застосунку — контейнер падає через необроблений виняток у C# коді.
Volumes у Pod: спільне сховище
Ми вже згадували, що контейнери в Pod можуть мати спільні volumes для обміну даними. Тепер розглянемо це детально.
Проблема: дані в контейнері ефемерні
За замовчуванням файлова система контейнера ефемерна (тимчасова). Коли контейнер перезапускається — всі зміни у файловій системі втрачаються. Контейнер стартує з чистого стану, як визначено в образі.
Приклад проблеми:
Ваш ASP.NET Core API пише логи у файл /var/log/app/app.log. Контейнер падає і перезапускається. Всі логи втрачено — файл /var/log/app/app.log не існує в новому контейнері.
Рішення: Використати volume — постійне (або спільне) сховище, яке існує незалежно від життєвого циклу контейнера.
Типи volumes
Kubernetes підтримує багато типів volumes. Розглянемо найважливіші для початку:
emptyDir — тимчасове сховище
emptyDir — це порожня директорія, яка створюється разом з Pod і видаляється разом з ним. Дані зберігаються на диску вузла, але існують лише протягом життя Pod.
apiVersion: v1
kind: Pod
metadata:
name: emptydir-pod
spec:
volumes:
- name: cache
emptyDir: {}
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: cache
mountPath: /app/cache
name) та тип (наприклад, emptyDir, configMap, secret).volumeMounts.medium: "Memory" для створення volume в RAM (швидше, але обмежено пам'яттю).spec.volumes[], який потрібно змонтувати./app/cache, /var/log, /etc/config.false (read-write). Корисно для конфігурацій та секретів, які не повинні змінюватися.Використання emptyDir:
- Тимчасовий кеш
- Обмін даними між контейнерами у Pod
- Scratch space для обчислень
emptyDirне зберігаються після видалення Pod. Якщо Pod перезапускається (контейнер падає) — дані зберігаються. Але якщо Pod видаляється (наприклад, через kubectl delete pod) — дані втрачаються назавжди.configMap та secret — конфігурації як volumes
ConfigMap та Secret — це ресурси Kubernetes для зберігання конфігурацій та чутливих даних. Їх можна монтувати як volumes:
apiVersion: v1
kind: Pod
metadata:
name: config-pod
spec:
volumes:
- name: config
configMap:
name: app-config
- name: secrets
secret:
secretName: app-secrets
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: config
mountPath: /etc/config
readOnly: true
- name: secrets
mountPath: /etc/secrets
readOnly: true
Припустимо, у вас є ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
database.conf: |
host=db.example.com
port=5432
logging.conf: |
level=info
format=json
Коли ви змонтуєте цей ConfigMap у /etc/config, контейнер побачить два файли:
/etc/config/database.confз вмістомhost=db.example.com\nport=5432/etc/config/logging.confз вмістомlevel=info\nformat=json
Кожен ключ у ConfigMap стає окремим файлом у змонтованій директорії.
appsettings.json та монтувати його у контейнер. ASP.NET Core автоматично прочитає файл при старті. Це дозволяє змінювати конфігурацію без перезбірки Docker-образу.Приклад: два контейнери з спільним volume
Розглянемо практичний приклад — ASP.NET Core API пише логи у файл, а другий контейнер читає ці логи:
apiVersion: v1
kind: Pod
metadata:
name: shared-logs-pod
spec:
volumes:
- name: logs
emptyDir: {}
containers:
# Основний застосунок
- name: app
image: myapp:1.0
volumeMounts:
- name: logs
mountPath: /var/log/app
env:
- name: ASPNETCORE_URLS
value: 'http://+:8080'
# Контейнер для читання логів
- name: log-reader
image: busybox:1.36
volumeMounts:
- name: logs
mountPath: /var/log/app
readOnly: true
command: ['sh', '-c']
args: ['tail -f /var/log/app/app.log']
Що відбувається:
- Створюється volume
logsтипуemptyDir - Контейнер
appмонтує цей volume у/var/log/app(read-write) - Контейнер
log-readerмонтує той самий volume у/var/log/app(read-only) - Контейнер
appпише логи у/var/log/app/app.log - Контейнер
log-readerчитає файл/var/log/app/app.logчерезtail -f
Обидва контейнери бачать один і той самий файл через спільний volume!
Перевірити логи можна через kubectl logs:
Опція -c log-reader вказує, з якого контейнера читати логи (оскільки в Pod два контейнери).
Практичний приклад: ASP.NET Core Minimal API у Pod
Тепер застосуємо все, що ми вивчили, для створення реального .NET застосунку у Kubernetes. Ми створимо простий Minimal API, який повертає список задач (To-Do List).
Крок 1: Створення .NET проєкту
Створіть новий проєкт ASP.NET Core Minimal API:
Крок 2: Код застосунку
Відредагуйте Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// In-memory список задач
var todos = new List<Todo>
{
new(1, "Вивчити Kubernetes", false),
new(2, "Створити Pod", false),
new(3, "Задеплоїти застосунок", false)
};
app.MapGet("/", () => "Todo API is running!");
app.MapGet("/api/todos", () => todos);
app.MapGet("/api/todos/{id}", (int id) =>
{
var todo = todos.FirstOrDefault(t => t.Id == id);
return todo is not null ? Results.Ok(todo) : Results.NotFound();
});
app.MapPost("/api/todos", (Todo todo) =>
{
todos.Add(todo);
return Results.Created($"/api/todos/{todo.Id}", todo);
});
app.Run();
record Todo(int Id, string Title, bool IsCompleted);
Це простий API з трьома ендпоінтами:
GET /— перевірка здоров'яGET /api/todos— отримати всі задачіGET /api/todos/{id}— отримати задачу за IDPOST /api/todos— створити нову задачу
Крок 3: Dockerfile
Створіть Dockerfile у корені проєкту:
# Етап 1: Збірка
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Копіюємо .csproj та відновлюємо залежності
COPY TodoApi.csproj .
RUN dotnet restore
# Копіюємо решту файлів та збираємо
COPY . .
RUN dotnet publish -c Release -o /app/publish
# Етап 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
# Копіюємо зібраний застосунок
COPY --from=build /app/publish .
# Налаштовуємо порт
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
# Запускаємо застосунок
ENTRYPOINT ["dotnet", "TodoApi.dll"]
Це multi-stage Dockerfile:
- Етап 1 (build): Використовує SDK-образ для збірки застосунку
- Етап 2 (runtime): Використовує легкий runtime-образ лише з необхідними бібліотеками
Крок 4: Збірка та публікація образу
Зберіть Docker-образ:
Якщо ви використовуєте Minikube, завантажте образ у Minikube:
docker build, він зберігається у Docker daemon вашої хост-системи. Команда minikube image load копіює образ у Docker daemon Minikube, щоб Kubernetes міг його використати.Альтернатива: Опублікувати образ у Docker Hub або іншому registry, і Kubernetes завантажить його звідти.Крок 5: Pod YAML маніфест
Створіть файл todoapi-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: todoapi
labels:
app: todoapi
version: '1.0'
spec:
containers:
- name: api
image: todoapi:1.0
imagePullPolicy: Never # Не завантажувати з registry, використовувати локальний образ
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: ASPNETCORE_ENVIRONMENT
value: 'Development'
- name: ASPNETCORE_URLS
value: 'http://+:8080'
resources:
requests:
memory: '64Mi'
cpu: '100m'
limits:
memory: '128Mi'
cpu: '250m'
Always— завжди завантажувати з registry (навіть якщо образ є локально)IfNotPresent— завантажувати лише якщо образу немає локальноNever— ніколи не завантажувати, використовувати лише локальний образ
Never, щоб Kubernetes не намагався завантажити образ з Docker Hub.Крок 6: Створення та тестування Pod
Створіть Pod:
Перевірте логи застосунку:
Застосунок запущено! Тепер потрібно отримати доступ до нього.
Крок 7: Доступ до Pod через port-forward
Оскільки Pod має IP-адресу лише всередині кластера, використайте kubectl port-forward для доступу з вашої машини:
Тепер відкрийте новий термінал та протестуйте API:
Чудово! Ваш .NET застосунок працює у Kubernetes Pod.
Крок 8: Діагностика та debugging
Kubernetes надає кілька команд для діагностики Pod:
Переглянути детальну інформацію:
Виконати команду всередині контейнера:
Опція -it означає інтерактивний режим з TTY (як docker exec -it).
Переглянути використання ресурсів:
Pod використовує 5 міліядер CPU (0.005 ядра) та 45 МБ пам'яті — значно менше, ніж наш ліміт у 250m CPU та 128Mi пам'яті.
Резюме
У цій статті ми детально розглянули Pod — атомарну одиницю Kubernetes:
Концепція Pod
YAML специфікація
metadata (назва, мітки), spec.containers (образ, порти, змінні оточення, ресурси, команда запуску). Кожне поле має чітке призначення та правила використання.Життєвий цикл
restartPolicy (Always, OnFailure, Never).Volumes
emptyDir — для тимчасових даних, configMap та secret — для конфігурацій та чутливих даних..NET приклад
Ключовий висновок: Pod — це низькорівневий примітив. Він дає гнучкість для складних сценаріїв, але не надає автоматизації для production (self-healing, масштабування, rolling updates). У наступних статтях ми вивчимо патерни використання Pod (init-контейнери, sidecar) та вищі абстракції (Deployment), які керують Pod автоматично.
Практичні завдання
Завдання 1: Створення Pod з Nginx та прокидання портів (Port-forwarding)
Створіть свій перший Pod з офіційним веб-сервером Nginx та налаштуйте локальний доступ до нього через порт-форвардинг, щоб перевірити його працездатність.
Маніфест nginx-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: nginx-practice
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
name: http
Кроки для виконання:
- Створіть маніфест
nginx-pod.yamlта збережіть його. - Запустіть Pod у вашому кластері:kubectl apply$ kubectl apply -f nginx-pod.yamlpod/nginx-practice created::
- Налаштуйте перенаправлення порту з локальної машини на Pod:kubectl port-forward$ kubectl port-forward pod/nginx-practice 8080:80Forwarding from 127.0.0.1:8080 -> 80::
Вимоги:
- Перевірте роботу веб-сервера, відкривши браузер за адресою
http://localhost:8080або виконавши командуcurl http://localhost:8080. - Переконайтеся, що ви бачите вітальну сторінку "Welcome to nginx!".
Завдання 2: Мультиконтейнерний Pod та спільна мережа (Localhost)
Дослідіть, як контейнери всередині одного Pod ділять між собою мережевий простір та IP-адресу. Для цього запустіть веб-сервер та допоміжну утиліту в межах єдиного Pod.
Маніфест multi-container-net.yaml:
apiVersion: v1
kind: Pod
metadata:
name: net-sharing-practice
spec:
restartPolicy: Never
containers:
- name: web-server
image: nginx:1.27
- name: utility
image: busybox:1.36
command: ['sleep', '3600']
Кроки для виконання та перевірки:
- Запустіть Pod у кластері.
- Увійдіть у термінал контейнера
utilityза допомогоюkubectl exec:kubectl exec$ kubectl exec -it net-sharing-practice -c utility -- sh/ # wget -O- http://localhost:80Connecting to localhost:80 (127.0.0.1:80)writing to stdout<!DOCTYPE html>... Welcome to nginx!::Питання для самоперевірки:
- Чому контейнер
utilityзміг успішно виконати запит доlocalhost:80, хоча веб-сервер запущено в іншому контейнеріweb-server? - Що станеться, якщо додати до цього Pod третій контейнер, який також намагатиметься зайняти порт
80?
Завдання 3: Обмін даними через спільний Volume (emptyDir)
Налаштуйте спільну ефемерну папку (emptyDir) між двома контейнерами: один з яких періодично генерує логи, а інший — зчитує їх у реальному часі.
Маніфест shared-volume-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: volume-practice
spec:
volumes:
- name: shared-data
emptyDir: {}
containers:
- name: writer
image: busybox:1.36
command:
['sh', '-c', 'while true; do echo "[$(date)] Log entry from writer" >> /data/message.txt; sleep 5; done']
volumeMounts:
- name: shared-data
mountPath: /data
- name: reader
image: busybox:1.36
command: ['sh', '-c', 'sleep 3; tail -f /data/message.txt']
volumeMounts:
- name: shared-data
mountPath: /data
readOnly: true
Кроки для виконання та перевірки:
- Застосуйте маніфест у кластері.
- Підключіться до логів контейнера
readerдля перевірки виводу:kubectl logs$ kubectl logs volume-practice -c reader -f[Sat May 9 20:15:00 UTC 2026] Log entry from writer[Sat May 9 20:15:05 UTC 2026] Log entry from writer::Питання для самоперевірки:
- Як прапорець
readOnly: trueзахищає дані від контейнера-читача? Що станеться, якщо спробувати виконати запис із контейнераreader? - Що станеться з файлом
/data/message.txt, якщо контейнерwriterвпаде та перезапуститься? А якщо видалити весь Pod?
Завдання 4: Дослідження політики перезапуску (restartPolicy)
Дослідіть поведінку Kubernetes при завершенні контейнера з помилкою за різних політик перезапуску.
Кроки для виконання:
- Створіть шаблон маніфесту
restart-practice.yaml:apiVersion: v1 kind: Pod metadata: name: restart-never-practice spec: restartPolicy: Never # Змініть також на Always та OnFailure containers: - name: broken-app image: busybox:1.36 command: ['sh', '-c', 'sleep 5; exit 1'] - Створіть три різні Pods, послідовно змінюючи значення поля
restartPolicyнаNever,AlwaysтаOnFailure(та змінюючи назву Pod уmetadata.name). - Запустіть термінал у режимі відслідковування та спостерігайте за зміною станів:kubectl get pods -w$ kubectl get pods -wNAME READY STATUS RESTARTS AGErestart-never-practice 0/1 ContainerCreating 0 1srestart-never-practice 1/1 Running 0 3srestart-never-practice 0/1 Error 0 8s::
Питання для самоперевірки:
- Опишіть фінальний статус (
STATUS) та кількість перезапусків (RESTARTS) для кожного з трьох Pods через 2 хвилини після запуску. - Що таке стан
CrashLoopBackOffта яка його мета?
Завдання 5: Ліміти ресурсів та падіння через OOMKilled
Налаштуйте жорсткі ліміти пам'яті для контейнера та запустіть у ньому процес, що споживає більше дозволеного, щоб побачити роботу Linux Out-Of-Memory (OOM) Killer.
Маніфест oom-practice-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: oom-practice
spec:
restartPolicy: Never
containers:
- name: memory-hog
image: polonel/stress:latest
command: ['stress', '--vm', '1', '--vm-bytes', '150M', '--timeout', '15s']
resources:
limits:
memory: '50Mi'
cpu: '200m'
requests:
memory: '25Mi'
cpu: '100m'
Кроки для виконання та перевірки:
- Запустіть Pod у кластері.
- Зачекайте 10-15 секунд та перевірте статус Pod:kubectl get pod$ kubectl get pod oom-practiceNAME READY STATUS RESTARTS AGEoom-practice 0/1 Terminated: OOMKilled 0 12s::
- Дослідіть деталі завершення через
describe:kubectl describe pod$ kubectl describe pod oom-practice...State: TerminatedReason: OOMKilledExit Code: 137::Питання для самоперевірки:
- Чому перевищення ліміту CPU призводить лише до уповільнення обчислень (throttling), тоді як перевищення ліміту пам'яті (
limits.memory) миттєво вбиває процес? - Що означає
Exit Code 137?
Завдання 6: Конфігурація через ConfigMap (Дослідницьке)
Навчіться монтувати файли конфігурації через ConfigMap безпосередньо у файлову систему контейнера та спостерігати за їх синхронізацією.
Маніфест config-practice.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-settings
data:
config.json: |
{
"theme": "dark",
"features": ["auth", "billing"]
}
---
apiVersion: v1
kind: Pod
metadata:
name: config-practice
spec:
volumes:
- name: config-volume
configMap:
name: app-settings
containers:
- name: app
image: busybox:1.36
command: ['sh', '-c', "while true; do cat /etc/config/config.json; echo ''; sleep 10; done"]
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
Кроки для виконання та перевірки:
- Запустіть ConfigMap та Pod за допомогою
kubectl apply -f config-practice.yaml. - Запустіть читання логів Pod:
kubectl logs$ kubectl logs config-practice -f{ "theme": "dark", "features": ["auth", "billing"] }::
- Відкрийте ConfigMap на редагування в іншому терміналі:
kubectl edit configmap$ kubectl edit configmap app-settings::
- Змініть
"theme": "dark"на"theme": "light", збережіть файл та вийдіть з редактора. - Поспостерігайте за логами Pod (може знадобитися до 1 хвилини для синхронізації Kubelet).
Питання для самоперевірки:
- Чи змінився вміст файлу конфігурації всередині контейнера автоматично? Поясніть механізм синхронізації ConfigMap.