Опирается на правила: R-OBS-HC-1R-OBS-HC-3 и R-OBS-HC-X1R-OBS-HC-X3 из Observability Style Guide → раздел 4. Health checks.

Важно знать

  • Liveness и readiness — разные probe с разной семантикой и разными зависимостями.
  • /health/live — UP пока процесс жив и event loop не застрял. Не должен зависеть от внешних систем.
  • /health/ready — UP когда сервис готов принимать трафик: БД подключена, критичные зависимости отвечают.
  • Custom HealthIndicator для критичных внешних систем реализуется с TTL-кешем: без него probe дудосит provider каждые 5 секунд × N реплик.
  • /info отдаёт git-sha, версию и build-time — обязательно для отладки в проде.
  • Health — техническое состояние процесса, не бизнес. orderCount > N → DOWN — антипаттерн.
  • Liveness не зависит от DB: иначе K8s уходит в restart-loop при кратковременном лаге на стороне PostgreSQL.
  • В NestJS health-роуты лучше выносить на отдельный management-порт (:9090), закрытый network policy.

Health checks — то, на что смотрит Kubernetes, ALB и load balancer, решая «дать ли трафик в этот pod». Неправильно настроенные probes — главный источник каскадного отказа: одна реплика DB лагает → все pods unhealthy → весь сервис недоступен.

Установка @nestjs/terminus

npm install @nestjs/terminus

Подключение в AppModule (или в выделенный HealthModule):

import { TerminusModule } from '@nestjs/terminus';

@Module({
  imports: [TerminusModule],
})
export class HealthModule {}

Liveness vs Readiness

R-OBS-HC-1: два endpoint-а с явно разграниченными зависимостями.

import { Controller, Get } from '@nestjs/common';
import {
  HealthCheck,
  HealthCheckService,
  TypeOrmHealthIndicator,
} from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private readonly health: HealthCheckService,
    private readonly db: TypeOrmHealthIndicator,
  ) {}

  @Get('live')
  @HealthCheck()
  liveness() {
    return this.health.check([]);
  }

  @Get('ready')
  @HealthCheck()
  readiness() {
    return this.health.check([
      () => this.db.pingCheck('postgres'),
    ]);
  }
}
EndpointЧто проверяетЧто делает K8s
/health/liveПроцесс отвечает, event loop живUP → продолжать; DOWN → рестартует pod
/health/readyБД, критичные зависимостиUP → шлёт трафик; DOWN → снимает из Service endpoints

Семантическое различие критично:

  • Если БД недоступна 10 секунд — readiness DOWN (трафик уйдёт на другие реплики), liveness UP (рестарт pod-а не поможет — та же БД).
  • Если process завис — liveness DOWN, K8s убивает pod, новый стартует с чистым event loop.

K8s манифест:

spec:
  containers:
    - name: order-service
      livenessProbe:
        httpGet:
          path: /health/live
          port: 9090
        initialDelaySeconds: 30
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /health/ready
          port: 9090
        initialDelaySeconds: 5
        periodSeconds: 5
        failureThreshold: 2

Порт 9090 — отдельный management-server, не business 3000. Это позволяет ограничить /health/* и /metrics сетевой политикой.

Custom HealthIndicator с TTL-кешем

R-OBS-HC-2: для каждой критичной внешней системы — отдельный HealthIndicator. Чтобы probe не делала десятки запросов к provider'у каждые 5 секунд — TTL-кеш.

Пример: SberPaymentHealthIndicator (проверяет доступность платёжного шлюза).

import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { SberPaymentClient } from '../payment/sber-payment.client';

interface CachedResult {
  result: HealthIndicatorResult;
  expiresAt: number;
}

@Injectable()
export class SberPaymentHealthIndicator extends HealthIndicator {
  private cache: CachedResult | null = null;
  private readonly ttlMs = 10_000;

  constructor(private readonly client: SberPaymentClient) {
    super();
  }

  async isHealthy(key: string): Promise<HealthIndicatorResult> {
    const now = Date.now();
    if (this.cache && this.cache.expiresAt > now) {
      return this.cache.result;
    }

    const result = await this.check(key);
    this.cache = { result, expiresAt: now + this.ttlMs };
    return result;
  }

  private async check(key: string): Promise<HealthIndicatorResult> {
    try {
      await this.client.ping();
      return this.getStatus(key, true, { provider: 'sber' });
    } catch (err) {
      const result = this.getStatus(key, false, { provider: 'sber' });
      throw new HealthCheckError('SberPayment ping failed', result);
    }
  }
}

Подключение в контроллере:

@Get('ready')
@HealthCheck()
readiness() {
  return this.health.check([
    () => this.db.pingCheck('postgres'),
    () => this.sberPayment.isHealthy('sber-payment'),
  ]);
}

Без TTL-кеша: K8s проверяет readiness каждые 5 секунд × 10 реплик × isHealthy дёргает Sber → 120 ping/min на платёжный шлюз только от health-check. Rate-limit срабатывает, реальные транзакции фейлятся, readiness DOWN → cascading.

TypeOrmHealthIndicator (pingCheck) дёшевый — выполняет SELECT 1 во внутреннем pool. Его TTL-кешировать не нужно.

/info с git-sha и версией сборки

R-OBS-HC-3: /info обязателен для отладки версии в проде. Без него каждый инцидент начинается с вопроса «какая сборка задеплоена».

В NestJS нет встроенного /actuator/info, поэтому делается простой контроллер, который читает переменные окружения, выставленные CI/CD:

import { Controller, Get } from '@nestjs/common';

@Controller('info')
export class InfoController {
  @Get()
  info() {
    return {
      service: {
        name: process.env.SERVICE_NAME ?? 'order-service',
        version: process.env.APP_VERSION ?? 'unknown',
      },
      git: {
        commitId: process.env.GIT_COMMIT_SHA ?? 'unknown',
        branch: process.env.GIT_BRANCH ?? 'unknown',
      },
      build: {
        time: process.env.BUILD_TIME ?? 'unknown',
      },
    };
  }
}

В Dockerfile (или Helm chart) переменные выставляются из build args:

ARG GIT_COMMIT_SHA
ARG GIT_BRANCH
ARG BUILD_TIME
ARG APP_VERSION

ENV GIT_COMMIT_SHA=$GIT_COMMIT_SHA \
    GIT_BRANCH=$GIT_BRANCH \
    BUILD_TIME=$BUILD_TIME \
    APP_VERSION=$APP_VERSION

Результат /info:

{
  "service": { "name": "order-service", "version": "2.4.1" },
  "git":     { "commitId": "5380f21abc3d", "branch": "main" },
  "build":   { "time": "2026-06-18T14:22:00Z" }
}

Отдельный management-port

R-OBS-CFG-1: health-routes и /metrics — на отдельном порту, не на business 3000.

Паттерн: второй NestFactory.create в bootstrap:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);

  const mgmt = await NestFactory.create(ManagementModule);
  mgmt.setGlobalPrefix('');
  await mgmt.listen(9090);
}

ManagementModule экспортирует только HealthController, InfoController и /metrics. Business-эндпоинты на :9090 недоступны.

Что запрещено

Business-state в health check

R-OBS-HC-X1: «если pending заказов у Customer > 1000 → DOWN» — бизнес-метрика, не техническое состояние.

Health DOWN → K8s снимает реплику из Service endpoints → меньше реплик обрабатывают очередь → ещё хуже. Бизнес-метрики мониторятся через Prometheus + alerting (см. SLO и алерты).

Liveness зависит от внешних систем

R-OBS-HC-X2: классическая ловушка на PostgreSQL.

// КАТАСТРОФА — liveness DOWN при лаге DB
@Get('live')
@HealthCheck()
liveness() {
  return this.health.check([
    () => this.db.pingCheck('postgres'),  // НЕЛЬЗЯ в liveness
  ]);
}

Сценарий: PG лагает 30 секунд из-за VACUUM FULL. Liveness DOWN → K8s убивает pod → новый стартует, та же DB лагает → DOWN → убивает → loop. Все реплики падают за минуту.

Liveness зависит только от самого процесса. Внешние — только в readiness.

Health-probe делает бизнес-операцию

R-OBS-HC-X3: «давайте в probe создадим тестовый Product и удалим». Каждые 5 секунд × 10 реплик = ddos самих себя + грязные данные в аналитике.

Probe — light: SELECT 1, ping, cached value. Не бизнес-логика.

Что запрещено — таблица

АнтипаттернПравилоЧто взамен
Business-state в health checkR-OBS-HC-X1техническое состояние; бизнес → SLO/alerts
Liveness зависит от DB/RedisR-OBS-HC-X2только readiness зависит от внешних
Probe делает бизнес-операциюR-OBS-HC-X3light probe (ping, cached value)
HealthIndicator без TTL-кешаR-OBS-HC-2TTL 5–30 секунд на custom indicator
/health без management-port изоляцииR-OBS-CFG-1отдельный :9090, закрытый network policy
Нет /info с git-shaR-OBS-HC-3InfoController с env-переменными из CI/CD

Куда дальше

  • Observability → раздел 4. Health checks — нормативные формулировки правил.
  • Конфигурация — management-port, exposed endpoints, изоляция.
  • Metrics — бизнес-метрики мониторятся через prom-client, не через health.
  • Logging — nestjs-pino, structured JSON, DI-логгер.
  • Tracing — OTel автоинструментация, manual spans, sampling.
  • Context propagation — AsyncLocalStorage, requestId, userId в guard.
  • SLO и алерты — бизнес-цели и error budget отдельно от probes.