Kubernetes networking для разработчиков: Service, Ingress, Network Policy без теории
Когда я первый раз задеплоил приложение в Kubernetes — оно запустилось, но не отвечало на запросы. Потратил три часа пока понял что Service selector не совпадает с labels Pod'а. Одна буква в неправильном месте.
Это руководство написано для backend-разработчиков, которые хотят понять сеть в k8s без DevOps-диссертации. Всё по делу, с рабочими манифестами для Node.js приложения.
Как Pod'ы общаются между собой
Каждый Pod в кластере получает свой IP адрес. Но IP Pod'а меняется при перезапуске — поэтому обращаться к Pod'у напрямую по IP нельзя.
Kubernetes DNS (kube-dns / CoreDNS) даёт каждому Service стабильное DNS имя:
<service-name>.<namespace>.svc.cluster.local
Если ваш Node.js сервис хочет обратиться к PostgreSQL сервису в том же namespace:
// Полное DNS имя:
const pgUrl = 'postgresql://postgres:5432/mydb';
// Работает только если postgres Service в том же namespace
// Из другого namespace:
const pgUrl = 'postgresql://postgres.database.svc.cluster.local:5432/mydb';
Внутри одного namespace можно использовать просто имя сервиса. Это работает потому что CoreDNS автодополняет <name> до <name>.<current-namespace>.svc.cluster.local.
Типы Service: когда что использовать
ClusterIP — внутренняя коммуникация
# Сервис для PostgreSQL — только внутри кластера
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: database
spec:
type: ClusterIP # дефолтный тип, можно не писать
selector:
app: postgres # должен совпасть с labels Pod'а
ports:
- port: 5432 # порт сервиса (к этому обращаются другие)
targetPort: 5432 # порт контейнера
ClusterIP создаёт виртуальный IP, доступный только внутри кластера. Никакого внешнего трафика. Используйте для БД, кэша, внутренних микросервисов.
NodePort — прямой доступ через порт ноды
apiVersion: v1
kind: Service
metadata:
name: api-nodeport
spec:
type: NodePort
selector:
app: api
ports:
- port: 3000
targetPort: 3000
nodePort: 30080 # порт 30000-32767, доступен на каждой ноде
Доступен как <node-ip>:30080. Используйте для отладки или когда у вас нет load balancer. В production почти не используется.
LoadBalancer — внешний трафик через облачный балансировщик
apiVersion: v1
kind: Service
metadata:
name: api-lb
annotations:
# Специфично для вашего cloud provider
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
spec:
type: LoadBalancer
selector:
app: api
ports:
- port: 80
targetPort: 3000
Создаёт внешний load balancer у облачного провайдера. Проблема: каждый LoadBalancer сервис = отдельный внешний IP и отдельная оплата. Для нескольких сервисов лучше использовать один Ingress.
Ingress: один entry point для всех сервисов
Ingress позволяет маршрутизировать внешний трафик к разным сервисам по URL path или hostname — через один LoadBalancer.
Установка nginx ingress controller
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.replicaCount=2 \
--set controller.resources.requests.cpu=100m \
--set controller.resources.requests.memory=90Mi
Ingress с TLS (cert-manager + Let's Encrypt)
# Установка cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
# ClusterIssuer для Let's Encrypt
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@yourcompany.kz
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
# Ingress с автоматическим TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# Rate limiting — важно для production
nginx.ingress.kubernetes.io/limit-rps: "100"
nginx.ingress.kubernetes.io/limit-connections: "20"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.yourapp.kz
secretName: api-tls-secret # cert-manager создаст автоматически
rules:
- host: api.yourapp.kz
http:
paths:
- path: /api/v1
pathType: Prefix
backend:
service:
name: api-service
port:
number: 3000
- path: /admin
pathType: Prefix
backend:
service:
name: admin-service
port:
number: 4000
Полный пример: Node.js приложение
# Deployment для Node.js API
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
labels:
app: api
version: "1.0"
spec:
replicas: 3
selector:
matchLabels:
app: api # должен совпасть с template.metadata.labels
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # максимум 1 дополнительный Pod при обновлении
maxUnavailable: 0 # ноль недоступных Pod'ов — zero-downtime деплой
template:
metadata:
labels:
app: api
version: "1.0"
spec:
containers:
- name: api
image: registry.yourcompany.kz/api:1.0.0
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: api-secrets
key: database-url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: api-secrets
key: redis-url
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# Readiness Probe — когда Pod готов принимать трафик
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
# Liveness Probe — когда Pod нужно перезапустить
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 30 # ВАЖНО: дайте время на старт
periodSeconds: 30
failureThreshold: 3
lifecycle:
preStop:
exec:
# Graceful shutdown: ждём 15 сек чтобы завершить in-flight запросы
command: ["/bin/sh", "-c", "sleep 15"]
terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: production
spec:
selector:
app: api
ports:
- port: 3000
targetPort: 3000
Endpoint для проб в Node.js
// health.ts
import express from 'express';
import { db } from './db';
import { redis } from './redis';
const router = express.Router();
// Liveness: жив ли процесс? Простая проверка
router.get('/health/live', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Readiness: готов ли принимать трафик?
// Проверяем реальные зависимости
router.get('/health/ready', async (req, res) => {
try {
// Проверяем БД
await db.$queryRaw`SELECT 1`;
// Проверяем Redis
await redis.ping();
res.json({ status: 'ready', db: 'ok', redis: 'ok' });
} catch (error) {
// 503 — kubernetes не будет слать трафик на этот Pod
res.status(503).json({
status: 'not ready',
error: error instanceof Error ? error.message : 'unknown',
});
}
});
export { router as healthRouter };
Readiness vs Liveness: почему неправильный liveness убивает production
Это критически важно, и мы сами сделали эту ошибку.
Readiness Probe: "Готов ли Pod принимать трафик?" Если не готов — kubernetes убирает его из балансировки, но не перезапускает. Pod живёт, просто трафик не идёт. Используйте для: медленного старта, временной недоступности зависимостей (БД перезапустилась), прогрева кэша.
Liveness Probe: "Жив ли Pod?" Если нет — kubernetes перезапускает контейнер. Используйте только для: deadlock, процесс завис и не отвечает на запросы.
Антипаттерн который мы видели в проде
# ОПАСНО: liveness проверяет доступность БД
livenessProbe:
httpGet:
path: /health/live # а этот endpoint делает SELECT к БД
port: 3000
periodSeconds: 10
failureThreshold: 3
// ОПАСНО: liveness endpoint проверяет БД
router.get('/health/live', async (req, res) => {
await db.$queryRaw`SELECT 1`; // если БД недоступна — kubernetes начнёт рестартить все поды
res.json({ ok: true });
});
Сценарий: БД кратковременно недоступна (rolling update PostgreSQL, 30 секунд). Liveness probe падает на всех Pod'ах → kubernetes рестартует все поды одновременно → во время рестарта трафик некуда идти → полный даунтайм.
Правильный паттерн: liveness только проверяет что process работает (или максимум — что event loop не заблокирован). Readiness проверяет зависимости.
Network Policy: изоляция неймспейсов
По умолчанию в Kubernetes любой Pod может общаться с любым другим Pod'ом в любом namespace. Это плохо с точки зрения безопасности.
# Запрет всего входящего трафика в namespace database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: database
spec:
podSelector: {} # применяется ко всем Pod'ам в namespace
policyTypes:
- Ingress
# Разрешаем только backend (из namespace production) ходить в postgres
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-postgres
namespace: database
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: production # namespace с label name=production
podSelector:
matchLabels:
app: api # только Pod'ы с label app=api
ports:
- protocol: TCP
port: 5432
# Ставим label на namespace (нужно сделать один раз)
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
name: production # это label используется в namespaceSelector выше
# Запрет всего исходящего трафика из namespace production
# кроме явно разрешённого
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
---
# Разрешаем DNS (без этого ничего не работает!)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- protocol: UDP
port: 53
---
# Разрешаем исходящий к БД
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-to-database
namespace: production
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
name: database
ports:
- protocol: TCP
port: 5432
- protocol: TCP
port: 6379 # Redis
Horizontal Pod Autoscaler на кастомных метриках
Стандартный HPA на CPU часто не то что нужно — API может быть медленным из-за очереди запросов, а не из-за CPU.
# HPA на RPS (запросов в секунду) через Prometheus metrics
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 20
metrics:
# CPU как основной триггер
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# Кастомная метрика: RPS на под
- type: Pods
pods:
metric:
name: http_requests_per_second # из Prometheus через prometheus-adapter
target:
type: AverageValue
averageValue: "100" # масштабируем когда >100 RPS на под
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # не скейлим вниз 5 минут после последнего события
policies:
- type: Percent
value: 25 # убираем максимум 25% подов за раз
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0 # скейлим вверх сразу
policies:
- type: Percent
value: 100 # удваиваем количество подов
periodSeconds: 30
Метрику http_requests_per_second нужно экспортировать из приложения:
// metrics.ts — Prometheus метрики в Node.js
import { Registry, Counter, Histogram } from 'prom-client';
export const registry = new Registry();
export const httpRequestsTotal = new Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status_code'],
registers: [registry],
});
export const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
registers: [registry],
});
// Endpoint для Prometheus scraping
app.get('/metrics', async (req, res) => {
res.set('Content-Type', registry.contentType);
res.send(await registry.metrics());
});
Частые ошибки и как их найти
# Pod не запускается — смотрим events
kubectl describe pod <pod-name> -n production
# Трафик не идёт — проверяем selector
kubectl get endpoints api-service -n production
# Если ENDPOINTS пустой — selector не совпадает с labels Pod'а
# Проверяем labels Pod'а
kubectl get pod -n production --show-labels
# Тестируем сетевую доступность из Pod'а
kubectl exec -it <pod-name> -n production -- curl http://postgres.database.svc.cluster.local:5432
# Смотрим логи ingress controller
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx --tail=100
# Network Policy блокирует? Временно включаем debug
kubectl exec -it <pod-name> -n production -- nslookup postgres.database.svc.cluster.local
Сеть в Kubernetes кажется сложной, но 90% проблем — это несовпадение label selector в Service/Deployment, неправильный namespace или забытый Network Policy. Освойте kubectl describe и kubectl get endpoints — они скажут вам всё.
Разрабатываете backend и хотите правильно выстроить инфраструктуру в Kubernetes? Aunimeda — посмотрите наши услуги или свяжитесь с нами.