Logo

NestJS의 liveness/readiness 엔드포인트

마이크로서비스(microservices) 아키텍처나 분산 시스템 환경에서는 모든 서비스가 정상적으로 살아서 동작하는지를 검사하는 것이 매우 중요합니다. 이를 위해서 각 서비스에 생존 여부(liveness)와 가용 여부(readiness)를 응답해주는 HTTP 엔드포인트(endpoint)가 필요하기 마련인데요.

이번 포스팅에서는 NestJS 앱에서 이러한 엔드포인트(endpoint)를 어떻게 구현할 수 있는지에 대해서 알아보도록 하겠습니다.

실습 프로젝트 구성

먼저 간단한 실습을 위해서 NestJS 프로젝트가 하나 필요할 것 같은데요. 터미널에서 NestJS CLI 도구의 nest new 명령어를 실행하여 새로운 프로젝트를 구성하도록 하겠습니다.

$ nest new our-nestjs
⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? (Use arrow keys)npm
  yarn
  pnpm

NestJS CLI를 설치하고 NestJS 프로젝트를 구성하는 기본적인 방법은 관련 포스팅을 참고 바랍니다.

liveness 엔드포인트 구현

먼저 단순히 서비스가 살아있는지 죽었는지를 재빠르게 확인하기 위해서 사용하는 liveness 엔드포인트(endpoint)를 구현해볼까요?

주로 AWS ELB와 같은 로드 밸런서(load balancer)나 쿠버네티스(Kubernetes)와 컨테이너 관리 플랫폼이 이러한 엔드포인트를 주기적으로 찔러보면서 서비스의 생존여부를 모니터링하게 됩니다.

우선 NestJS CLI 도구로 health 모듈을 생성한 후, health 컨트롤러도 생성하겠습니다.

$ nest generate module health
CREATE src/health/health.module.ts (83 bytes)
UPDATE src/app.module.ts (315 bytes)
$ nest generate controller health
CREATE src/health/health.controller.spec.ts (492 bytes)
CREATE src/health/health.controller.ts (101 bytes)
UPDATE src/health/health.module.ts (174 bytes)

health 컨트롤러 구현은 매우 단순합니다. 응답 상태가 항상 200 OK가 되어야 하며, 사람이 직접 확인하는 용도가 아니므로 응답 전문은 그닥 중요하지 않습니다. GET /health로 요청이 들어오면 checkHealth() 메서드가 처리하도록 코드를 작성하겠습니다.

src/health/health.controller.ts
import { Controller, Get } from "@nestjs/common";

@Controller()
export class HealthController {
  @Get("health")
  checkHealth() {
    return "OK";
  }
}

이제 터미널에서 curl 명령어를 사용하여 작성한 엔드포인트를 호출해보면 예상했던 응답이 돌아옵니다.

$ curl http://localhost:3000/health -i
HTTP/1.1 200 OKX-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 2
ETag: W/"2-nOO9QiTIwXgNtWtBJezz8kv3SLc"
Date: Mon, 20 Feb 2023 21:36:32 GMT
Connection: keep-alive
Keep-Alive: timeout=5

OK

readiness 엔드포인트 구현 - 메모리 검사

다음으로 서비스가 살아있을 뿐만 아니라 정상적으로 요청을 처리할 수 있는 상태인지를 확인할 때 사용하는 readiness 엔드포인트(endpoint)를 작성해보겠습니다.

NestJS에서는 좀 더 쉽게 이러한 엔드포인트를 구현할 수 있도록 @nestjs/terminus라는 패키지를 제공하고 있는데요. 이 패키지를 이용하면 하드웨어 사용량이나 외부 API가 정상인지를 손쉽게 검사할 수 있습니다.

우선 실습 프로젝트에 @nestjs/terminus라는 npm 패키지를 프로젝트에 설치합니다.

$ npm i @nestjs/terminus

warn preInstall No repository field
┌ [1/4] 🔍  Resolving dependencies
└ Completed in 7.49s
┌ [2/4] 🚚  Fetching dependencies
│ info pruneDeps Excluding 1 dependency. For
│ more information use `--verbose`.
└ Completed in 11.68s
┌ [3/4] 🔗  Linking dependencies
└ Completed in 13.773s
info security We found `install` scripts which
turbo skips for security reasons. For more
information see
https://turbo.sh/install-scripts.
└─ @nestjs/core@9.3.9

success Saved lockfile "package-lock.json"
success Updated "package.json"

success Install finished in 33.014s

그리고 health 모듈에서 @nestjs/terminus 패키지의 TerminusModule를 불러오겠습니다.

src/health/health.module.ts
import { Module } from "@nestjs/common";
import { TerminusModule } from "@nestjs/terminus";import { HealthController } from "./health.controller";

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

이제 힙(heap) 메모리 사용량이 300MB가 넘어가는지를 검사하도록 readiness 엔드포인트(endpoint)를 구현해볼까요? GET /status로 요청이 들어오면, checkStatus() 메서드가 처리하도록 코드를 작성하겠습니다. checkStatus() 메서드에는 @HealthCheck() 어노테이션을 붙여줘야합니다.

src/health/health.controller.ts
import { Controller, Get } from "@nestjs/common";
import {  HealthCheck,  HealthCheckService,  MemoryHealthIndicator,} from "@nestjs/terminus";
@Controller()
export class HealthController {
  constructor(    private readonly health: HealthCheckService,    private readonly memory: MemoryHealthIndicator  ) {}
  @Get("health")
  checkHealth() {
    return "OK";
  }

  @Get("status")  @HealthCheck()  checkStatus() {    return this.health.check([      () => this.memory.checkHeap("memory_heap", 300 * 1024 * 1024),    ]);  }}

@nestjs/terminus 패키지에서 제공하는 MemoryHealthIndicator를 사용하여 이처럼 아주 짧은 코드로 메모리 사용량을 검사할 수 있습니다.

추가한 엔드포인트(endpoint) 호출해보면 200 OK 응답 상태와 함께 메모리 검사 결과가 JSON 형태로 응답이 될 것입니다.

curl http://localhost:3000/status -i
HTTP/1.1 200 OKX-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 107
ETag: W/"6b-ouXVoNOXyOxnMfI7caewF8/p97A"
Date: Mon, 20 Feb 2023 21:51:46 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{
  "status": "ok",  "info": { "memory_heap": { "status": "up" } },
  "error": {},
  "details": { "memory_heap": { "status": "up" } }
}

status(readiness) 엔드포인트 구현 - API 검사

내부적으로 다른 API에 의존하는 서비스의 경우 원격 호출해야하는 API의 상태도 확인하는 것이 좋습니다. 외부 API에 장애가 있다면 해당 서비스 자체적으로는 아무 문제가 없더라도 정상적으로 요청을 처리할 수 있는 상태라고 보기 어렵기 때문이죠.

이럴 때는 @nestjs/terminus 패키지에서 제공하는 HttpHealthIndicator를 사용하여 외부 API가 접속이 가능한지 확인해볼 수 있는데요. 이를 위해서는 HTTP 클라이언트가 되서 해당 API를 원격으로 호출해야하 하므로 axios@nestjs/axios 패키지를 설치해야합니다. (사실 외부 API를 호출하고 있다면 이미 해당 프로젝트에 설치되어있을 확률이 매우 높겠죠?)

$ npm i axios @nestjs/axios
warn preInstall No repository field
┌ [1/4] 🔍  Resolving dependencies
└ Completed in 5.961s
┌ [2/4] 🚚  Fetching dependencies
│ info pruneDeps Excluding 1 dependency. For more
│ information use `--verbose`.
└ Completed in 0.016s
┌ [3/4] 🔗  Linking dependencies
└ Completed in 0.025s
info security We found `install` scripts which turbo
skips for security reasons. For more information see
https://turbo.sh/install-scripts.
└─ @nestjs/core@9.3.9

success Saved lockfile "package-lock.json"
success Updated "package.json"

success Install finished in 6.053s

그 다음 health 모듈에서 @nestjs/terminus 패키지의 HttpModule을 불러오겠습니다.

src/health/health.module.ts
import { Module } from "@nestjs/common";
import { TerminusModule } from "@nestjs/terminus";
import { HttpModule } from "@nestjs/axios";import { HealthController } from "./health.controller";

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

이제 health 컨트롤러로 돌아와서 생성자의 인자로 HttpHealthIndicator을 추가하고, checkStatus() 메서드 내에서 사용하면 됩니다.

src/health/health.controller.ts
import { Controller, Get } from "@nestjs/common";
import {
  HealthCheck,
  HealthCheckService,
  MemoryHealthIndicator,
  HttpHealthIndicator,} from "@nestjs/terminus";

@Controller()
export class HealthController {
  constructor(
    private readonly health: HealthCheckService,
    private readonly memory: MemoryHealthIndicator,
    private readonly http: HttpHealthIndicator  ) {}

  @Get("health")
  checkHealth() {
    return "OK";
  }

  @Get("status")
  @HealthCheck()
  checkStatus() {
    return this.health.check([
      () => this.memory.checkHeap("memory_heap", 300 * 1024 * 1024),
      () => this.http.pingCheck("other-api", "http://localhost:3001"),    ]);
  }
}

다시 터미널에서 readiness 엔드포인트를 호출해보면 응답 전문에 외부 API 검사 결과까지 추가되고 503 Service Unavailable 상태가 응답될 것입니다. 3001 포트에 아무 프로세스도 띄어논 것이 없기 때문에 당연한 결과입니다.

curl http://localhost:3000/status -i
HTTP/1.1 503 Service UnavailableX-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 277
ETag: W/"115-HMPfu5KBHxhtl8RGFTFr8j39gCA"
Date: Mon, 20 Feb 2023 22:07:21 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{
  "status": "error",  "info": { "memory_heap": { "status": "up" } },
  "error": {
    "other-api": {
      "status": "down",
      "message": "connect ECONNREFUSED 127.0.0.1:3001"
    }
  },
  "details": {
    "memory_heap": { "status": "up" },
    "other-api": {
      "status": "down",
      "message": "connect ECONNREFUSED 127.0.0.1:3001"
    }
  }
}

전체 코드

실습 프로젝트의 코드는 아래에서 직접 확인하고 실행해볼 수 있습니다.

마치면서

이상으로 간단한 실습을 통해서 NestJS 앱에 서비스 생존 여부와 가용 여부를 확인하기 위한 엔드포인트를 구현해보았습니다. 본 포스팅에서는 다루지 않았지만 @nestjs/terminus 패키지에는 디스크(disk) 사용량과 같은 다른 검사도 할 수 있으니 참고 바라겠습니다.

liveness/readiness 엔드포인트를 잘 활용하셔서 운영하시는 서비스의 전반적인 가용성(availability)을 향상시키고 하드웨어 자원을 탄력적으로 프로비저닝(provisioning)하는데 도움이 되셨으면 좋겠습니다.

NestJS에 관련된 다른 포스팅은 관련 태그를 참고 바라겠습니다.