У попередній статті ми навчилися створювати Pod з одним контейнером — запускати ASP.NET Core API, налаштовувати ресурси, змінні оточення та volumes. Але чому Kubernetes дозволяє розміщувати кілька контейнерів в одному Pod? Чи не простіше було б мати один контейнер на Pod?
Щоб відповісти на це питання, розглянемо реальні проблеми, з якими ви зіткнетеся при розгортанні застосунків у production.
Уявіть, що ви розгортаєте ASP.NET Core Web API, який працює з PostgreSQL. Перед тим, як запустити API, потрібно:
Якщо ви спробуєте зробити все це всередині основного контейнера API, виникнуть проблеми:
Що потрібно: Механізм для виконання підготовчих задач перед запуском основного застосунку. Задачі мають виконуватися один раз, послідовно, і основний застосунок має чекати їх завершення.
Тепер уявіть, що ваш API працює, але вам потрібно:
Якщо ви спробуєте зробити все це всередині основного контейнера API, виникнуть проблеми:
Що потрібно: Механізм для запуску допоміжних процесів, які працюють паралельно з основним застосунком, мають доступ до його даних (логи, метрики), але не змінюють код застосунку.
Kubernetes вирішує обидві проблеми через два патерни:
Init-контейнери
Контейнери, які виконуються перед основним застосунком. Виконуються послідовно, один за одним. Основний застосунок запускається лише після успішного завершення всіх init-контейнерів.
Використання: Міграції бази даних, завантаження конфігурацій, очікування доступності залежних сервісів.
Sidecar-контейнери
Контейнери, які працюють паралельно з основним застосунком. Мають доступ до спільних volumes та мережі. Працюють протягом усього життя Pod.
Використання: Збір логів, експорт метрик, проксіювання трафіку, синхронізація даних.
Обидва патерни базуються на тому, що контейнери в одному Pod мають спільну мережу (localhost) та можуть мати спільні volumes (файлові системи). Це дозволяє їм тісно взаємодіяти без складних налаштувань.
Init-контейнери — це спеціальні контейнери, які виконуються перед основними контейнерами Pod. Вони використовуються для підготовчих задач, які мають виконатися один раз перед стартом застосунку.
Коли Kubernetes створює Pod з init-контейнерами, відбувається наступне:
Ключові властивості init-контейнерів:
Init:Error або Init:CrashLoopBackOff.busybox, alpine) для перевірки доступності сервісів, або образ з .NET SDK для виконання міграцій.restartPolicy Pod. Якщо restartPolicy: Always або OnFailure, init-контейнер буде перезапущено з exponential backoff (як і основні контейнери).Init-контейнери описуються в полі spec.initContainers[], яке має такий самий формат, як spec.containers[]:
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
# Init-контейнери
initContainers:
- name: init-step-1
image: busybox:1.36
command: ["sh", "-c", "echo Init step 1 && sleep 2"]
- name: init-step-2
image: busybox:1.36
command: ["sh", "-c", "echo Init step 2 && sleep 2"]
# Основні контейнери
containers:
- name: app
image: nginx:1.27
spec.containers[]: name, image, command, args, env, volumeMounts, resources тощо.Створіть цей Pod та спостерігайте за його запуском:
Статус Init:0/2 означає "виконується init-контейнер 0 з 2". Після завершення обох init-контейнерів Pod переходить у стан Running.
Найпоширеніший сценарій — перевірити, чи доступна база даних, перед запуском застосунку:
apiVersion: v1
kind: Pod
metadata:
name: wait-for-db
spec:
initContainers:
- name: wait-for-postgres
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for PostgreSQL..."
until nc -z postgres 5432; do
echo "PostgreSQL is not ready yet, waiting..."
sleep 2
done
echo "PostgreSQL is ready!"
containers:
- name: app
image: myapp:1.0
env:
- name: ConnectionStrings__DefaultConnection
value: "Host=postgres;Database=mydb;Username=user;Password=pass"
Що відбувається:
wait-for-postgresnc -z postgres 5432 (netcat — перевірка доступності порту)until продовжуєтьсяappInit:*, ви знаєте, що він чекає БД. Коли Pod у стані Running, застосунок готовий.Init-контейнер може завантажити конфігурації з зовнішнього джерела (наприклад, S3, Git) та зберегти їх у спільний volume:
apiVersion: v1
kind: Pod
metadata:
name: config-loader
spec:
volumes:
- name: config
emptyDir: {}
initContainers:
- name: download-config
image: alpine:3.19
volumeMounts:
- name: config
mountPath: /config
command:
- sh
- -c
- |
echo "Downloading configuration..."
# Симуляція завантаження конфігурації
cat > /config/appsettings.json <<EOF
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"DatabaseSettings": {
"ConnectionString": "Host=postgres;Database=mydb"
}
}
EOF
echo "Configuration downloaded!"
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
Що відбувається:
config типу emptyDir/configappsettings.json у /config/appsettings.json/app/config (read-only)/app/config/appsettings.jsonТепер розглянемо реальний сценарій — виконання міграцій Entity Framework Core перед запуском ASP.NET Core API.
У вас є ASP.NET Core API, який використовує Entity Framework Core для роботи з PostgreSQL. При розгортанні нової версії застосунку потрібно застосувати міграції до бази даних. Якщо ви виконаєте міграції всередині основного контейнера API:
Running → складно діагностуватиРішення: Виконати міграції в init-контейнері. Міграції виконаються один раз перед запуском API, і якщо вони впадуть — Pod не запуститься.
Створіть новий проєкт ASP.NET Core Web API:
Додайте пакети Entity Framework Core:
Створіть модель та DbContext. Додайте файл Todo.cs:
public class Todo
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public bool IsCompleted { get; set; }
}
Додайте файл AppDbContext.cs:
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<Todo> Todos => Set<Todo>();
}
Відредагуйте Program.cs:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Додаємо DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// API endpoints
app.MapGet("/", () => "Todo API with PostgreSQL");
app.MapGet("/api/todos", async (AppDbContext db) =>
await db.Todos.ToListAsync());
app.MapGet("/api/todos/{id}", async (int id, AppDbContext db) =>
await db.Todos.FindAsync(id) is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/api/todos", async (Todo todo, AppDbContext db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/api/todos/{todo.Id}", todo);
});
app.MapPut("/api/todos/{id}", async (int id, Todo inputTodo, AppDbContext db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Title = inputTodo.Title;
todo.IsCompleted = inputTodo.IsCompleted;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/api/todos/{id}", async (int id, AppDbContext db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
});
app.Run();
Додайте рядок підключення у appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=tododb;Username=postgres;Password=postgres"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Створіть початкову міграцію:
Це створить папку Migrations/ з файлами міграції.
Створіть Dockerfile:
# Етап 1: Збірка
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Копіюємо .csproj та відновлюємо залежності
COPY *.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", "TodoApiWithDb.dll"]
Створіть окремий Dockerfile.migrations для виконання міграцій:
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /src
# Копіюємо .csproj та відновлюємо залежності
COPY *.csproj .
RUN dotnet restore
# Копіюємо решту файлів
COPY . .
# Встановлюємо EF Core tools
RUN dotnet tool install --global dotnet-ef
ENV PATH="${PATH}:/root/.dotnet/tools"
# Команда для виконання міграцій
ENTRYPOINT ["dotnet", "ef", "database", "update"]
dotnet ef), а для запуску API достатньо .NET Runtime (легший образ). Окремі Dockerfile дозволяють оптимізувати розмір образів:Зберіть обидва образи:
Спочатку потрібно запустити PostgreSQL. Створіть файл postgres-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: postgres
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: "tododb"
- name: POSTGRES_USER
value: "postgres"
- name: POSTGRES_PASSWORD
value: "postgres"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
Створіть PostgreSQL Pod:
Тепер створіть Pod з init-контейнером, який виконає міграції. Файл todoapi-with-migrations.yaml:
apiVersion: v1
kind: Pod
metadata:
name: todoapi
labels:
app: todoapi
spec:
# Init-контейнери
initContainers:
# 1. Очікування доступності PostgreSQL
- name: wait-for-postgres
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for PostgreSQL..."
until nc -z postgres 5432; do
echo "PostgreSQL is not ready, waiting..."
sleep 2
done
echo "PostgreSQL is ready!"
# 2. Виконання міграцій
- name: run-migrations
image: todoapi-migrations:1.0
imagePullPolicy: Never
env:
- name: ConnectionStrings__DefaultConnection
value: "Host=postgres;Database=tododb;Username=postgres;Password=postgres"
# Основний контейнер
containers:
- name: api
image: todoapi-db:1.0
imagePullPolicy: Never
ports:
- containerPort: 8080
name: http
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Development"
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: ConnectionStrings__DefaultConnection
value: "Host=postgres;Database=tododb;Username=postgres;Password=postgres"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
Що відбувається:
wait-for-postgres): Чекає, поки PostgreSQL стане доступнимrun-migrations): Виконує dotnet ef database update для застосування міграційapi): Запускається лише після успішного завершення обох init-контейнерівСтворіть Pod:
Перевірте логи init-контейнера з міграціями:
Міграції застосовано! Тепер протестуйте API:
Чудово! API працює, міграції застосовано, дані зберігаються в PostgreSQL.
Init:Error, і ви одразу побачите проблемуSidecar-контейнери — це контейнери, які працюють паралельно з основним контейнером протягом усього життя Pod. Вони розширюють або доповнюють функціональність основного застосунку без зміни його коду.
На відміну від init-контейнерів, які виконуються послідовно перед запуском основного застосунку, sidecar-контейнери запускаються одночасно з основним контейнером і працюють паралельно:
Ключові властивості sidecar-контейнерів:
localhost:8080.Збір логів
Експорт метрик
Мережевий проксі
Синхронізація даних
Адаптер даних
Sidecar-контейнери — це звичайні контейнери в spec.containers[]. Немає окремого поля для sidecar, вони просто додаються до списку контейнерів:
apiVersion: v1
kind: Pod
metadata:
name: sidecar-demo
spec:
volumes:
- name: shared-data
emptyDir: {}
containers:
# Основний контейнер
- name: app
image: myapp:1.0
volumeMounts:
- name: shared-data
mountPath: /data
# Sidecar контейнер
- name: sidecar
image: busybox:1.36
volumeMounts:
- name: shared-data
mountPath: /data
readOnly: true
command: ["sh", "-c", "tail -f /data/app.log"]
Обидва контейнери запустяться одночасно та працюватимуть паралельно.
Тепер розглянемо практичний приклад — ASP.NET Core API пише логи у файл, а sidecar-контейнер відправляє ці логи у Elasticsearch.
Ваш ASP.NET Core API працює у Kubernetes. Ви хочете зберігати логи у Elasticsearch для централізованого пошуку та аналізу. Є два підходи:
Підхід 1: Додати Serilog.Sinks.Elasticsearch у код API
Підхід 2: Sidecar-контейнер з Fluent Bit
Модифікуйте Program.cs для запису логів у файл:
using Microsoft.EntityFrameworkCore;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Налаштовуємо Serilog для запису у файл
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("/var/log/app/app.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7)
.CreateLogger();
builder.Host.UseSerilog();
// Додаємо DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Логуємо кожен запит
app.Use(async (context, next) =>
{
Log.Information("Request: {Method} {Path}", context.Request.Method, context.Request.Path);
await next();
Log.Information("Response: {StatusCode}", context.Response.StatusCode);
});
// API endpoints (як раніше)
app.MapGet("/", () => "Todo API with Logging");
app.MapGet("/api/todos", async (AppDbContext db) => await db.Todos.ToListAsync());
// ... інші endpoints
app.Run();
Додайте пакет Serilog:
Оновіть Dockerfile, щоб створити директорію для логів:
# Етап 1: Збірка
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.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
# Створюємо директорію для логів
RUN mkdir -p /var/log/app && chmod 777 /var/log/app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "TodoApiWithDb.dll"]
Перезберіть образ:
Створіть файл todoapi-with-logging.yaml:
apiVersion: v1
kind: Pod
metadata:
name: todoapi-logging
labels:
app: todoapi
spec:
volumes:
# Спільний volume для логів
- name: logs
emptyDir: {}
initContainers:
# Очікування PostgreSQL
- name: wait-for-postgres
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for PostgreSQL..."
until nc -z postgres 5432; do
sleep 2
done
echo "PostgreSQL is ready!"
# Міграції
- name: run-migrations
image: todoapi-migrations:1.0
imagePullPolicy: Never
env:
- name: ConnectionStrings__DefaultConnection
value: "Host=postgres;Database=tododb;Username=postgres;Password=postgres"
containers:
# Основний контейнер - API
- name: api
image: todoapi-db:2.0
imagePullPolicy: Never
ports:
- containerPort: 8080
name: http
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Development"
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: ConnectionStrings__DefaultConnection
value: "Host=postgres;Database=tododb;Username=postgres;Password=postgres"
volumeMounts:
- name: logs
mountPath: /var/log/app
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
# Sidecar контейнер - Log Reader
- name: log-reader
image: busybox:1.36
volumeMounts:
- name: logs
mountPath: /var/log/app
readOnly: true
command:
- sh
- -c
- |
echo "Log reader started, waiting for log file..."
# Чекаємо, поки з'явиться файл логів
while [ ! -f /var/log/app/app.log ]; do
sleep 1
done
echo "Log file found, starting to tail..."
tail -f /var/log/app/app.log
Що відбувається:
logs типу emptyDirapi — пише логи у /var/log/app/app.loglog-reader — читає файл /var/log/app/app.log через tail -flogsСтворіть Pod:
Зверніть увагу: READY показує 2/2 — обидва контейнери працюють!
Тепер ви можете переглядати логи API через sidecar-контейнер:
Тепер зробіть кілька запитів до API:
Перевірте логи знову:
Чудово! Sidecar-контейнер читає логи з файлу в реальному часі.
busybox з tail -f ви б використали Fluent Bit або Fluentd — спеціалізовані інструменти для збору логів. Вони можуть:Давайте підсумуємо різницю між init-контейнерами та sidecar-контейнерами:
| Характеристика | Init-контейнери | Sidecar-контейнери |
|---|---|---|
| Коли запускаються | Перед основними контейнерами | Одночасно з основними контейнерами |
| Порядок виконання | Послідовно, один за одним | Паралельно |
| Тривалість життя | Завершуються після виконання задачі | Працюють протягом усього життя Pod |
| Блокування запуску | Основні контейнери чекають завершення | Не блокують один одного |
| Типові задачі | Міграції БД, завантаження конфігурацій, очікування залежностей | Збір логів, експорт метрик, проксіювання трафіку |
| Приклад | dotnet ef database update | Fluent Bit для збору логів |
| Перезапуск | Перезапускаються при помилці (згідно restartPolicy) | Перезапускаються разом з основним контейнером |
У цій статті ми детально розглянули патерни використання Pod:
Проблеми, які вирішують патерни
Init-контейнери
Sidecar-контейнери
.NET приклади
Ключовий висновок: Multi-container Pod — це потужний інструмент для розділення відповідальності. Основний застосунок займається бізнес-логікою, init-контейнери готують середовище, sidecar-контейнери надають інфраструктурні сервіси. Це робить систему гнучкішою, легшою у підтримці та повторному використанні.
Створіть свій перший Pod з init-контейнером, який виконує підготовчу затримку (імітуючи перевірку або завантаження файлу) перед тим, як запуститься основний веб-сервер.
Маніфест init-delay-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: init-delay-practice
spec:
initContainers:
- name: init-delay
image: busybox:1.36
command: ["sh", "-c", "echo 'Initialization started...'; sleep 5; echo 'Initialization complete!'"]
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo 'Application started!'; sleep 3600"]
Кроки для виконання:
Вимоги:
Дослідіть, як кілька init-контейнерів виконуються послідовно та що відбувається, якщо один з них завершується з помилкою.
Маніфест init-chain-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: init-chain-practice
spec:
volumes:
- name: shared-data
emptyDir: {}
initContainers:
- name: step-1-write
image: busybox:1.36
command: ["sh", "-c", "echo 'Config version 1.0' > /shared/config.txt; echo 'Step 1 complete'"]
volumeMounts:
- name: shared-data
mountPath: /shared
- name: step-2-validate
image: busybox:1.36
command: ["sh", "-c", "cat /shared/config.txt && echo 'Validation complete!'"]
volumeMounts:
- name: shared-data
mountPath: /shared
containers:
- name: main-app
image: busybox:1.36
command: ["sh", "-c", "echo 'Main app is running!'; sleep 3600"]
volumeMounts:
- name: shared-data
mountPath: /shared
Експеримент та питання для самоперевірки:
kubectl get pods.step-1-write на таку, що завершується з помилкою (наприклад, ["sh", "-c", "echo 'Error' && exit 1"]).step-2-validate та main-app?Events при виконанні kubectl describe pod init-chain-practice?Навчіться завантажувати зовнішню конфігурацію (appsettings.json) з ConfigMap, обробляти її за допомогою init-контейнера та монтувати в основний .NET застосунок.
Маніфест config-loader-pod.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: external-config
data:
appsettings.json: |
{
"Logging": {
"LogLevel": {
"Default": "Debug"
}
},
"AllowedHosts": "*"
}
---
apiVersion: v1
kind: Pod
metadata:
name: config-loader-practice
spec:
volumes:
- name: config-volume
emptyDir: {}
- name: config-source
configMap:
name: external-config
initContainers:
- name: loader
image: busybox:1.36
command: ["sh", "-c", "cp /source/appsettings.json /target/appsettings.json && echo 'Config copied successfully!'"]
volumeMounts:
- name: config-source
mountPath: /source
- name: config-volume
mountPath: /target
containers:
- name: dotnet-app
image: mcr.microsoft.com/dotnet/aspnet:8.0
command: ["sh", "-c", "echo 'Dotnet reading settings:'; cat /app/config/appsettings.json; sleep 3600"]
volumeMounts:
- name: config-volume
mountPath: /app/config
Вимоги:
dotnet-app. Переконайтеся, що він успішно вивів змонтований JSON-вміст файлу конфігурації.Реалізуйте sidecar-контейнер, який працює паралельно з основним веб-сервером та періодично опитує його на предмет працездатності, імітуючи роботу агента моніторингу.
Маніфест sidecar-poller-practice.yaml:
apiVersion: v1
kind: Pod
metadata:
name: sidecar-poller-practice
spec:
containers:
# Основний контейнер (веб-сервер)
- name: web-server
image: nginx:1.27
ports:
- containerPort: 80
name: http
# Sidecar-контейнер для моніторингу
- name: metrics-poller
image: busybox:1.36
command: ["sh", "-c", "while true; do wget -qO- http://localhost:80 > /dev/null && echo \"[$(date)] [Metrics] HTTP 200 OK - Server is healthy\"; sleep 10; done"]
Кроки для виконання та перевірки:
Питання для самоперевірки:
metrics-poller має можливість звертатися до веб-сервера через localhost, хоча вони ізольовані один від одного на рівні процесів?Дослідіть, як sidecar-контейнер може зчитувати показники використання пам'яті основним контейнером безпосередньо з віртуальної файлової системи Linux (cgroups).
Маніфест resources-monitor-practice.yaml:
apiVersion: v1
kind: Pod
metadata:
name: resources-monitor-practice
spec:
containers:
- name: memory-consumer
image: busybox:1.36
command: ["sh", "-c", "while true; do echo 'Generating mock processing load...'; sleep 10; done"]
- name: monitor-sidecar
image: busybox:1.36
command: ["sh", "-c", "while true; do if [ -f /sys/fs/cgroup/memory.current ]; then echo \"[Monitor] Memory limit status: $(cat /sys/fs/cgroup/memory.current) bytes\"; else echo \"[Monitor] Memory status: $(cat /sys/fs/cgroup/memory/memory.usage_in_bytes) bytes\"; fi; sleep 5; done"]
Кроки для виконання та перевірки:
Питання для самоперевірки:
Pod — атомарна одиниця Kubernetes
Глибоке розуміння Pod — від базових концепцій до повної специфікації YAML, життєвого циклу та практичних прикладів з .NET
Deployment — декларативне управління Pod
Від ручного управління Pod до автоматизованої оркестрації — self-healing, масштабування та декларативні оновлення