О насБлогКонтакты
DevOps17 апреля 2026 г. 9 мин 1

Kubernetes networking для разработчиков: Service, Ingress, Network Policy без теории

AunimedaAunimeda
📋 Содержание

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 — посмотрите наши услуги или свяжитесь с нами.

Читайте также

Мониторинг Node.js: Prometheus + Grafana + алерты для productionaunimeda
DevOps

Мониторинг Node.js: Prometheus + Grafana + алерты для production

Настройка полного стека мониторинга для Node.js приложения: prom-client, HTTP-метрики, Prometheus scraping, Grafana дашборды, алерты в Telegram. Узнавайте о проблемах за 2 минуты до пользователей — с рабочим кодом.

Supabase vs Firebase 2026: сравнение для казахстанских стартапов и командaunimeda
Разработка

Supabase vs Firebase 2026: сравнение для казахстанских стартапов и команд

Supabase — open-source BaaS на PostgreSQL с возможностью самохостинга. Firebase — зрелая Google-платформа. PocketBase — один бинарник для MVP. Сравниваем по модели данных, цене, realtime и соответствию требованиям казахстанского рынка.

OWASP Top 10 2025: безопасность веб-приложений для казахстанского разработчикаaunimeda
Разработка

OWASP Top 10 2025: безопасность веб-приложений для казахстанского разработчика

OWASP Top 10 — это стандарт критических рисков безопасности. SQL-инъекции, сломанный контроль доступа, SSRF — каждый пункт с реальной атакой на ваш Node.js/Next.js код и конкретным исправлением. Актуально для проектов на казахстанском рынке.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Казахстане. Бесплатная консультация.

Получить консультацию Все статьи