배포 후 헬스체크 자동화 - 안전한 배포를 위한 검증 스크립트
들어가며
배포 자동화를 구축하면서 가장 중요하게 고민했던 부분은 “배포는 성공했는데, 서비스가 정상적으로 동작하지 않는다면?” 이었다.
애플리케이션을 재시작한 후:
- 프로세스는 떠 있지만 내부 초기화 실패
- 포트는 열렸지만 요청 처리 불가
- 일부 기능만 동작하는 부분 장애
이런 상황을 방지하기 위해 배포 후 자동 헬스체크 스크립트를 도입했다.
1. 문제 상황
기존 배포 프로세스의 문제점
1. 애플리케이션 종료
2. 새 버전 배포
3. 애플리케이션 시작
4. 배포 완료 (실제 동작 여부 미확인)
실제로 겪었던 문제들:
1. 프로세스만 떠있고 서비스는 죽은 경우
- Spring Boot 기동 중 Exception 발생
- 프로세스는 살아있지만 요청 처리 불가
2. DB 연결 실패
- 커넥션 풀 초기화 실패
- 서비스는 떠 있지만 모든 요청이 500 에러
3. 외부 연동 초기화 실패
- 외부 API 연동 실패
- 일부 기능만 동작하는 상태
필요했던 것
배포 완료 후 실제로 서비스가 정상 동작하는지 자동 검증하는 메커니즘
2. 해결 방법: 헬스체크 스크립트
설계 원칙
1. 충분한 대기 시간 제공
- Spring Boot 애플리케이션이 완전히 기동될 때까지 대기
- 최대 120초 동안 반복 체크
2. 실제 요청 기반 검증
- 프로세스 존재 여부가 아닌 HTTP 요청으로 검증
/monitor/health엔드포인트 호출
3. 실패 시 배포 중단
- 헬스체크 실패 시 배포 프로세스 종료
- 롤백 등 후속 조치 가능
스크립트 구현
#!/bin/sh
# 배포 이후 헬스체크 목적으로 사용
SERVICE_PORT=$1
# 서비스가 건강한가? 0(참) or 1(거짓)
function hasServiceHealthy() {
# 최대 120초 동안 1초 간격으로 헬스체크
for ((i=1; i<=120; i++)); do
local status=$(curl -s -m 1 -o /dev/null -w "%{http_code}" \
"http://localhost:${SERVICE_PORT}/monitor/health")
if [ $status -eq 200 ]; then
echo "[SUCCESS] 헬스체크 성공 (${i}초 소요)"
return 0; # 서비스 정상
fi
echo "[${i}/120] 헬스체크 대기 중... (HTTP ${status})"
sleep 1
done
echo "[FAILED] 헬스체크 실패: 120초 내 응답 없음"
return 1; # 서비스 비정상
}
###############################################################
# Main
###############################################################
if [ -z "$SERVICE_PORT" ]; then
echo "[ERROR] 사용법: $0 <SERVICE_PORT>"
exit 1
fi
echo "=========================================="
echo "헬스체크 시작: PORT=${SERVICE_PORT}"
echo "=========================================="
if ! hasServiceHealthy; then
echo "[ERROR] 배포 실패: 서비스가 정상 기동되지 않음"
exit 1 # 오류
fi
echo "[SUCCESS] 배포 완료: 서비스 정상 동작"
exit 0 # 정상
스크립트 설명
파라미터:
$1: 서비스 포트 (예: 8080)
hasServiceHealthy 함수:
- 최대 120초 동안 1초 간격으로 헬스체크
- curl로
/monitor/health엔드포인트 호출 - HTTP 200 응답 시 성공
- 120초 내 응답 없으면 실패
curl 옵션:
-s: 진행 상황 숨김 (silent)-m 1: 최대 대기 1초 (timeout)-o /dev/null: 응답 본문 버림-w "%{http_code}": HTTP 상태 코드만 출력
Main 로직:
- 포트 파라미터 검증
- 헬스체크 실행
- 실패 시 exit 1 (배포 중단)
- 성공 시 exit 0 (배포 계속)
3. Health Check API 구현
Controller
@RestController
@RequestMapping("/monitor")
public class MonitorController {
private final HealthChecker healthChecker;
@GetMapping("/health")
public ResponseEntity<HealthCheckResponse> health() {
HealthCheckResponse response = healthChecker.check();
if (response.isHealthy()) {
return ResponseEntity.ok(response);
}
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(response);
}
}
HealthChecker
@Component
@RequiredArgsConstructor
public class HealthChecker {
private final DataSource dataSource;
// private final ExternalApiClient externalApiClient;
public HealthCheckResponse check() {
List<String> errors = new ArrayList<>();
// 1. DB 연결 체크
if (!checkDatabase()) {
errors.add("DB 연결 실패");
}
// 2. 외부 API 연결 체크 (선택)
// if (!checkExternalApi()) {
// errors.add("외부 API 연결 실패");
// }
boolean healthy = errors.isEmpty();
return HealthCheckResponse.of(healthy, errors);
}
private boolean checkDatabase() {
try (Connection conn = dataSource.getConnection()) {
return conn.isValid(1); // 1초 타임아웃
} catch (Exception e) {
log.error("DB 헬스체크 실패", e);
return false;
}
}
}
Response DTO
@Getter
@AllArgsConstructor
public class HealthCheckResponse {
private boolean healthy;
private List<String> errors;
private LocalDateTime checkTime;
public static HealthCheckResponse of(boolean healthy, List<String> errors) {
return new HealthCheckResponse(healthy, errors, LocalDateTime.now());
}
}
4. Jenkins 배포 파이프라인 통합
Jenkinsfile 예시
pipeline {
agent any
stages {
stage('Build') {
steps {
sh './gradlew clean build'
}
}
stage('Deploy') {
steps {
sh '''
# 기존 프로세스 종료
./shutdown.sh
# 새 버전 배포
cp build/libs/app.jar /app/
# 애플리케이션 시작
./startup.sh
'''
}
}
stage('Health Check') {
steps {
script {
def result = sh(
script: './deploy_health_check.sh 8080',
returnStatus: true
)
if (result != 0) {
error("헬스체크 실패: 배포 중단")
}
}
}
}
}
post {
failure {
// 헬스체크 실패 시 롤백
sh './rollback.sh'
}
}
}
핵심:
- Health Check 단계에서 스크립트 실행
- exit 1 반환 시 배포 중단
- 실패 시 자동 롤백 가능
5. 사용 방법
기본 사용
# 8080 포트로 기동된 서비스 헬스체크
$ ./deploy_health_check.sh 8080
==========================================
헬스체크 시작: PORT=8080
==========================================
[1/120] 헬스체크 대기 중... (HTTP 000)
[2/120] 헬스체크 대기 중... (HTTP 000)
[3/120] 헬스체크 대기 중... (HTTP 200)
[SUCCESS] 헬스체크 성공 (3초 소요)
[SUCCESS] 배포 완료: 서비스 정상 동작
실패 케이스
$ ./deploy_health_check.sh 8080
==========================================
헬스체크 시작: PORT=8080
==========================================
[1/120] 헬스체크 대기 중... (HTTP 000)
[2/120] 헬스체크 대기 중... (HTTP 500)
...
[120/120] 헬스체크 대기 중... (HTTP 500)
[FAILED] 헬스체크 실패: 120초 내 응답 없음
[ERROR] 배포 실패: 서비스가 정상 기동되지 않음
6. 주의사항 및 개선 포인트
타임아웃 조정
애플리케이션 특성에 따라 120초는 부족할 수 있다.
조정이 필요한 경우:
- 대용량 캐시 로딩
- 배치 데이터 초기화
- 복잡한 외부 연동 초기화
# 타임아웃 180초로 증가
for ((i=1; i<=180; i++)); do
헬스체크 엔드포인트 보안
// IP 기반 접근 제어
@GetMapping("/health")
public ResponseEntity<HealthCheckResponse> health(HttpServletRequest request) {
String remoteAddr = request.getRemoteAddr();
// 로컬호스트만 허용
if (!"127.0.0.1".equals(remoteAddr) && !"0:0:0:0:0:0:0:1".equals(remoteAddr)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// ...
}
Spring Boot Actuator 활용
Spring Boot Actuator를 사용하면 더 풍부한 헬스체크가 가능하다.
# application.yml
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
show-details: always
# Actuator 헬스체크
curl http://localhost:8080/actuator/health
7. 설계 과정에서 배운 것
1. 배포 != 서비스 정상 동작
- 프로세스가 떠있다고 서비스가 정상인 것은 아님
- 실제 요청 기반 검증 필수
2. 충분한 대기 시간 제공
- Spring Boot 기동 시간을 고려
- 너무 짧으면 false negative (정상인데 실패 판정)
3. 배포 파이프라인에 통합
- 헬스체크 실패 시 배포 자동 중단
- 롤백 등 후속 조치 가능
4. 로깅 중요성
- 몇 초 만에 성공했는지 기록
- 실패 시 어떤 상태였는지 추적
8. 정리
핵심 요약
구조는 단순:
배포 → 시작 → 헬스체크 → 성공/실패
검증은 확실:
- HTTP 200 응답 확인
- 최대 120초 대기
- 실패 시 배포 중단
운영은 안전:
- 자동화된 검증
- 배포 파이프라인 통합
- 롤백 가능
실무에 적용하면서 느낀 점
배포 자동화에서 가장 중요한 것은 “배포 후 검증”이었다.
스크립트 하나 추가했을 뿐인데:
- 배포 실패를 즉시 발견
- 장애 시간 대폭 감소
- 배포에 대한 신뢰도 상승
“배포했으니까 정상이겠지”라는 가정을 버리고, “배포 후 검증까지가 배포다”라는 원칙을 세웠다.
댓글남기기