kimyu0218
  • [CD] health check로 서버 상태 확인하기 (Docker Compose/Shell Script)
    2024년 01월 21일 19시 20분 27초에 업로드 된 글입니다.
    작성자: @kimyu0218
    수정할 게 끝이 없네!! 🤪

    지난 포스팅에서 블루/그린 무중단 배포를 다뤘다. 하지만 무중단 배포임에도 불구하고 신규 어플리케이션에서 에러가 발생하면 중단된다!! (얼레벌레 무중단 배포,,)

    죽은 어플리케이션으로 안내하는 스껄한(?) 아키텍처

    이번 포스팅에서는 죽은 어플리케이션으로 트래픽을 전환하지 않도록, 어플리케이션의 상태를 확인하는 과정을 넣어줄 것이다.
     

    health check

    🔗 지난 포스팅
    - [CD] 블루/그린 무중단 배포 구현하기 1편
    - [CD] 블루/그린 무중단 배포 구현하기 2편

    health check는 서비스가 올바르게 작동하는지 확인하는 프로세스다. 장애나 문제를 미리 감지하고 예방할 수 있어 안정성을 향상시킨다.

    health check의 이점
    • 조기 감지 및 대응 : API의 이상을 조기에 감지할 수 있어, 잠재적인 문제를 신속하게 대응할 수 있다.
    • 사용자 경험 향상 : 정기적인 health check를 통해 안정성이 유지되면서 사용자에게 일관된 경험을 제공할 수 있다.
    • 성능 최적화 : 서버 및 네트워크 성능을 모니터링하고 최적화할 수 있다.

    정기적으로 서버 상태를 모니터링 하는 게 좋지만 급한 문제부터 해결해야 한다. 따라서 이번 포스팅에서는 신규 버전 배포 시 health check만 다루겠다! (미래의 나에게 토스)

    ...
    # 신규 버전의 도커 컨테이너 실행
    DOCKER_COMPOSE_FILE="compose.$RUN_TARGET-deploy.yml"
    sudo docker-compose -f "$DOCKER_COMPOSE_FILE" pull
    sudo docker-compose -f "$DOCKER_COMPOSE_FILE" up -d
    
    # 도커 컨테이너가 실행될 때까지 일정 시간 기다리기
    sleep 30
    
    NGINX_ID=$(sudo docker ps --filter "name=nginx" --quiet)
    NGINX_CONFIG="/etc/nginx/conf.d/default.conf"
    
    # nginx 설정파일 조작 (버전 변경)
    sudo docker exec $NGINX_ID /bin/bash -c "sed -i 's/was-$STOP_TARGET:$WAS_STOP_PORT/was-$RUN_TARGET:$WAS_RUN_PORT/' $NGINX_CONFIG"
    sudo docker exec $NGINX_ID /bin/bash -c "sed -i 's/signal-$STOP_TARGET:$((WAS_STOP_PORT + 1))/signal-$RUN_TARGET:$((WAS_RUN_PORT + 1))/' $NGINX_CONFIG"
    
    # nginx를 리로드하여 트래픽 전환
    sudo docker exec $NGINX_ID /bin/bash -c "nginx -s reload"
    ...

    위의 소스코드는 블루/그린 버전을 전환하는 과정이다. 컨테이너가 시작되기까지 일정 시간 기다릴 뿐, 제대로 실행되었는지 확인하는 부분이 없다. 지금부터 도커를 이용하여 health check를 구현해보자.
     

    health check용 API 만들기

    도커 파일을 수정하기 전에 컨테이너 상태를 확인하기 위한 API를 구현해야 한다. 추후 해당 API로 http 요청을 보내서 어플리케이션의 상태를 확인할 것이다.

    import { Controller, Get } from '@nestjs/common';
    
    @Controller()
    export class AppController {
      @Get('/health-check')
      healthCheck(): boolean {
        return true;
      }
    }

     

    healthcheck 옵션 작성하기

    나는 도커 컴포즈를 이용해서 멀티 컨테이너 어플리케이션을 구축하고 있기 때문에 도커 컴포즈의 `healthcheck` 옵션을 작성했다. (도커 컴포즈를 사용하지 않는다면 Dockerfile의 `HEALTHCHECK` 명령을 사용하면 된다!)

    ...
    services:
      was-blue:
        ...
        healthcheck:
          test: ["CMD", "curl", "-f", "http://localhost:3000/health-check"]
          interval: 10s
          timeout: 10s
          start_period: 20s
          retries: 3
    
      signal-blue:
        ...
        healthcheck:
          test: ["CMD", "curl", "-f", "http://localhost:3001/health-check"]
          interval: 10s
          timeout: 10s
          start_period: 10s
          retries: 3
    ...
    healthcheck 옵션들
    • `test` : health 상태를 확인하기 위한 명령
    • `interval` : health check 실행 간격
    • `timeout` : health check 타임아웃
    • `start_period` : 컨테이너 시작 후 health check를 실행하기 전까지의 간격
    • `retries` : health check 실패 시 재시도 횟수

    `test`에 앞서 작성한 API를 작성해준다. was 서비스의 경우, 데이터베이스에 연결하는 등 모듈이 로드되는 데 어느 정도 시간이 소요되기 때문에 `start_period`를 20초로 설정했다. 어플리케이션 실행 환경에 부하가 발생할 수도 있으므로 3번까지 health 상태를 확인하도록 만들었다.
     

    컨테이너 상태를 기준으로 트래픽 전환 여부 결정하기

    이제 트래픽을 전환하기 전에 컨테이너의 상태의 확인하는 과정을 넣어보자.

    echo "The $STOP_TARGET version is currently running on the server. Starting the $RUN_TARGET version."
    
    DOCKER_COMPOSE_FILE="compose.$RUN_TARGET-deploy.yml"
    sudo docker-compose -f "$DOCKER_COMPOSE_FILE" pull
    sudo docker-compose -f "$DOCKER_COMPOSE_FILE" up -d
    sleep 50

    도커 컨테이너를 실행하고 health check를 진행할 수 있게 50초 정도 기다리도록 만들었다.

    echo "Starting health check for the new version of the application."
    
    HEALTH_CHECK_PASSED=true
    RUN_CONTAINER_IDS=$(sudo docker ps --filter "name=$RUN_TARGET" --quiet --all)
    
    for CONTAINER_ID in $RUN_CONTAINER_IDS; do
      HEALTH_STATUS=$(sudo docker inspect --format "{{.State.Health.Status}}" $CONTAINER_ID)
      if [ "$HEALTH_STATUS" != "healthy" ]; then
        HEALTH_CHECK_PASSED=false
        break
      fi
    done

    방금 실행한 컨테이너들의 아이디를 조회하고 (`--all` 옵션으로 종료된 컨테이너도 조회하도록!!), `docker inspect` 명령으로 컨테이너 상태를 확인한다.

    if [ "$HEALTH_CHECK_PASSED" = true ]; then
      echo "Health check passed. Reloading nginx to transfer traffic from $STOP_TARGET to $RUN_TARGET."
      
      NGINX_ID=$(sudo docker ps --filter "name=nginx" --quiet)
      NGINX_CONFIG="/etc/nginx/conf.d/default.conf"
    
      sudo docker exec $NGINX_ID /bin/bash -c "sed -i 's/was-$STOP_TARGET:$WAS_STOP_PORT/was-$RUN_TARGET:$WAS_RUN_PORT/' $NGINX_CONFIG"
      sudo docker exec $NGINX_ID /bin/bash -c "sed -i 's/signal-$STOP_TARGET:$((WAS_STOP_PORT + 1))/signal-$RUN_TARGET:$((WAS_RUN_PORT + 1))/' $NGINX_CONFIG"
      sudo docker exec $NGINX_ID /bin/bash -c "nginx -s reload"
    
      echo "Terminating the $STOP_TARGET applications."
    
      STOP_CONTAINER_ID=$(sudo docker ps --filter "name=$STOP_TARGET" --quiet)
      if [ -n "$STOP_CONTAINER_ID" ]; then
        sudo docker stop $STOP_CONTAINER_ID
        sudo docker rm $STOP_CONTAINER_ID
        sudo docker image prune -af
      fi
    else
      echo "Health check failed."
      sudo docker image prune -af
    fi

    모든 컨테이너가 정상적으로 실행된 경우, nginx 설정파일을 수정하여 트래픽이 새로운 어플리케이션을 향하도록 만들고, 구버전의 어플리케이션을 종료한다.
    하나의 컨테이너에서 에러가 나서 종료된 경우에는 기존 어플리케이션을 유지하고 신규 어플리케이션 이미지를 삭제한다.

    서버의 상태를 확인한 후에 트래픽 전환 여부를 결정하기 때문에 어플리케이션 시작 오류로 인해 서비스가 중단되는 일이 발생하지 않는다!


    참고자료

    댓글