- Published on
Jenkins 파이프라인 AbortException 원인·해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 실패가 모두 AbortException으로 뭉뚱그려 보이면, 원인 파악이 늦어지고 재시도/알림 정책도 꼬입니다. Jenkins Pipeline에서 AbortException은 흔히 “빌드가 중단됨”을 나타내는 예외로, 실제로는 사용자/시스템이 의도적으로 파이프라인을 끊었거나, Pipeline 코드가 명시적으로 중단을 선언했거나, 특정 스텝이 실패를 Abort로 래핑했을 때 발생합니다.
이 글에서는 AbortException이 터지는 대표 패턴을 분류하고, 로그에서 빠르게 갈라내는 방법과, Declarative/Scripted Pipeline에서의 해결책을 코드로 정리합니다.
AbortException을 먼저 “분류”해야 하는 이유
Jenkins에서 실패는 대개 hudson.AbortException, FlowInterruptedException, InterruptedException 계열로 나타납니다. 문제는 UI나 콘솔 로그의 상단 요약이 AbortException: script returned exit code 1처럼 뭉개져서 보일 때가 많다는 점입니다.
실무적으로는 아래 3가지를 먼저 결정해야 합니다.
- 이 중단은 의도된 것인가 (예:
error()호출,timeout만료,input취소) - 재시도 대상인가 (예: 네트워크/레지스트리 일시 오류는
retry가치가 큼) - 빌드 결과를 실패로 둘 것인가/중단으로 둘 것인가 (예: 사용자가 취소한 배포는
ABORTED로 남기는 편이 자연스러움)
원인 1) sh/bat가 0이 아닌 종료 코드를 반환
가장 흔한 케이스입니다. sh 스텝은 기본적으로 종료 코드가 0이 아니면 빌드를 실패 처리하면서 AbortException을 던집니다.
전형적인 로그
AbortException: script returned exit code 1- 또는
script returned exit code 127(명령 없음),2(사용법 오류) 등
해결 1: 실패를 “값”으로 받아서 분기
종료 코드를 직접 받아서 제어하면, 불필요한 AbortException을 피하고 메시지도 명확히 만들 수 있습니다.
pipeline {
agent any
stages {
stage('Test') {
steps {
script {
int code = sh(script: 'npm test', returnStatus: true)
if (code != 0) {
error("테스트 실패: 종료 코드=${code}")
}
}
}
}
}
}
해결 2: 표준 출력도 함께 수집해 진단 강화
script {
def out = sh(script: 'node -v && npm -v', returnStdout: true).trim()
echo "env info: ${out}"
}
해결 3: Docker 빌드/푸시에서 자주 나는 Abort를 캐시/레이어로 줄이기
빌드 시간이 길어질수록 타임아웃/에이전트 끊김/레지스트리 오류로 AbortException이 연쇄적으로 증가합니다. Docker 빌드가 느려서 간헐적 실패가 잦다면 아래 글의 BuildKit 캐시/레이어 최적화도 같이 점검하는 편이 좋습니다.
원인 2) error() 스텝으로 명시적 중단
Pipeline에서 의도적으로 중단시키는 가장 단순한 방법이 error('message')입니다. 이때도 내부적으로 AbortException이 발생합니다.
stage('Guard') {
steps {
script {
if (!env.BRANCH_NAME?.startsWith('release/')) {
error('release 브랜치에서만 배포 가능합니다.')
}
}
}
}
해결 포인트
- “실패”가 맞다면
error()가 가장 명확합니다. - 다만 사용자 취소/정책상 중단을 실패로 남기고 싶지 않다면
currentBuild.result를ABORTED로 바꾸는 패턴을 고려합니다.
script {
currentBuild.result = 'ABORTED'
error('정책상 배포를 중단합니다(ABORTED 처리).')
}
주의: error() 자체는 실패로 던지므로, 결과를 ABORTED로 남기려면 후속 처리(예: post 블록)와 함께 의도대로 기록되는지 확인하세요.
원인 3) input 단계에서 사용자가 취소하거나 타임아웃
승인 게이트로 input을 쓰는 경우, 사용자가 “Abort”를 누르거나 승인 대기 중 타임아웃이 걸리면 중단 예외가 발생합니다.
전형적인 증상
- 승인 대기 중 파이프라인이 끊기고
AbortException또는FlowInterruptedException형태로 종료
해결: timeout과 예외 처리로 “정상적인 취소”를 구분
아래는 승인 대기 10분 후 자동 중단, 또는 사용자가 취소한 경우를 ABORTED로 분류하는 예시입니다.
stage('Approve') {
steps {
script {
try {
timeout(time: 10, unit: 'MINUTES') {
input message: '배포를 진행할까요?', ok: 'Deploy'
}
} catch (e) {
currentBuild.result = 'ABORTED'
echo "승인 단계에서 중단됨: ${e}"
// 이후 스테이지를 실행하지 않도록 명시 중단
error('사용자 취소 또는 승인 타임아웃')
}
}
}
}
핵심은 “실패”와 “취소”를 구분해 알림/지표를 다르게 가져가는 것입니다.
원인 4) timeout 스텝 만료로 강제 중단
timeout은 내부적으로 실행 중인 스텝을 인터럽트하고 파이프라인을 중단시킵니다. 이때도 AbortException처럼 보일 수 있습니다.
options {
timeout(time: 30, unit: 'MINUTES')
}
해결 체크리스트
- 타임아웃이 너무 짧지 않은지(특히 Docker build, 대형 테스트)
- 에이전트 성능 저하로 처리 시간이 늘어나지 않았는지
- 병렬 스테이지에서 특정 가지가 느려 전체가 끌려가지 않는지
병렬 처리에서 특정 작업이 장시간 걸려 전체가 묶이는 경우, 스테이지별 timeout을 걸어 “어디서” 시간이 소모되는지 분리하는 게 좋습니다.
stage('Parallel') {
parallel {
stage('unit') {
options { timeout(time: 10, unit: 'MINUTES') }
steps { sh 'npm test' }
}
stage('e2e') {
options { timeout(time: 20, unit: 'MINUTES') }
steps { sh 'npm run e2e' }
}
}
}
원인 5) catchError, warnError 사용 방식으로 Abort가 “가려짐”
catchError는 실패를 잡아 빌드/스테이지 결과를 조정할 수 있지만, 잘못 쓰면 진짜 원인(예: 특정 커맨드 exit code)이 숨겨지고 마지막에 AbortException만 남을 수 있습니다.
권장 패턴: 실패는 잡되 로그를 남기고 결과를 명확히
stage('Lint') {
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
sh 'npm run lint'
}
}
}
UNSTABLE과FAILURE를 구분해두면 “배포는 막되, 전체 파이프라인은 계속 진행” 같은 정책을 구현하기 쉽습니다.
원인 6) 에이전트(노드) 끊김, 컨테이너 종료, OOM 등 인프라 이슈
Jenkins 에이전트가 중간에 죽거나 연결이 끊기면, 실행 중이던 스텝이 중단되며 예외가 AbortException 계열로 귀결될 수 있습니다.
흔한 시나리오
- Kubernetes 에이전트 Pod가 OOMKilled
- Spot 인스턴스 종료
- Docker-in-Docker 컨테이너가 종료
- 네트워크 단절로 에이전트가 오프라인
해결 포인트
- Jenkins 콘솔 로그만 보지 말고, 에이전트 런타임 로그(K8s 이벤트, 노드 로그)를 함께 확인
- 빌드 머신 자원(CPU/메모리/디스크) 모니터링 및 제한값 조정
Kubernetes 환경에서 Pod가 재시작되거나 프로브 실패로 내려가는 경우는 CI에서도 동일하게 발생합니다. 파이프라인이 뜬금없이 끊긴다면 아래 가이드의 접근(이벤트/로그/Probe 순서)이 그대로 도움이 됩니다.
원인 7) 외부 API 레이트리밋/쿼터로 실패 후 Abort
배포/테스트 과정에서 외부 API(예: LLM API, 패키지 레지스트리, 사내 게이트웨이)를 호출하다가 429나 일시 장애가 나면, 커맨드가 비정상 종료하며 AbortException으로 이어집니다.
해결: 재시도(backoff)와 “재시도 가능한 실패” 분리
retry(n)는 단순하지만, 원인별 backoff가 필요하면 스크립트로 제어합니다.
stage('Call API') {
steps {
script {
int max = 5
for (int i = 1; i <= max; i++) {
int code = sh(script: 'curl -sS -o /tmp/resp.json -w "%{http_code}" https://api.example.com', returnStatus: true)
if (code == 0) {
echo 'API call ok'
break
}
if (i == max) {
error("API 호출 실패(재시도 초과). exit=${code}")
}
sleep time: (i * 3), unit: 'SECONDS'
}
}
}
}
외부 API의 429 대응은 CI에서 특히 중요합니다. 레이트리밋을 만나면 “코드 문제”가 아닌데도 빌드가 빨갛게 쌓이기 때문입니다. 아래 글의 쿼터/레이트리밋 대응 전략을 참고해, Jenkins 단계에도 동일한 재시도/지수 백오프/서킷 브레이커 개념을 적용하세요.
로그에서 원인을 빨리 찾는 실전 팁
1) “첫 번째 실패 지점”을 찾고, 마지막 Abort 메시지는 버린다
AbortException은 종종 최종 요약에 가깝습니다. 콘솔에서 다음을 우선 찾으세요.
+로 시작하는 실제 실행 커맨드(셸 trace)- 실패 직전의 stderr 출력
ERROR:라인Caused by:체인
2) 병렬 실행이면 가장 먼저 실패한 브랜치부터 본다
병렬 스테이지에서는 한 브랜치의 실패가 전체 중단으로 보입니다. 각 브랜치 로그를 펼쳐서 “첫 실패”를 찾는 게 중요합니다.
3) post { always { ... } }에 진단 정보 수집을 넣는다
실패 시점에만 필요한 정보를 자동으로 모으도록 해두면, 다음 번부터 AbortException이 떠도 조사 시간이 크게 줄어듭니다.
post {
always {
echo "result=${currentBuild.currentResult}"
sh(script: 'df -h', returnStatus: true)
sh(script: 'free -m', returnStatus: true)
}
}
재발 방지용 파이프라인 템플릿(요약)
아래는 AbortException을 “원인별로 구분”하고, 재시도/타임아웃/취소 처리를 기본 탑재한 뼈대입니다.
pipeline {
agent any
options {
timestamps()
ansiColor('xterm')
timeout(time: 40, unit: 'MINUTES')
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
script {
retry(2) {
int code = sh(script: 'npm ci && npm run build', returnStatus: true)
if (code != 0) {
error("빌드 실패: exit=${code}")
}
}
}
}
}
stage('Approve') {
when { branch 'main' }
steps {
script {
try {
timeout(time: 5, unit: 'MINUTES') {
input message: '프로덕션 배포 승인', ok: 'Deploy'
}
} catch (e) {
currentBuild.result = 'ABORTED'
error('승인 취소 또는 타임아웃')
}
}
}
}
}
post {
always {
echo "final=${currentBuild.currentResult}"
}
failure {
echo '실패: 원인 스텝의 stderr/exit code를 확인하세요.'
}
aborted {
echo '중단: 사용자 취소/타임아웃/정책 중단 가능성이 큽니다.'
}
}
}
마무리: AbortException은 “에러 메시지”가 아니라 “결과”다
AbortException 자체는 대개 결론에 가깝고, 진짜 원인은 그 이전의 스텝 실패(종료 코드), 승인 취소, 타임아웃, 에이전트 종료 같은 이벤트입니다.
sh의 종료 코드를returnStatus로 받아 원인을 명확히 만들고input/timeout은ABORTED로 분류해 알림을 분리하고- 인프라 끊김은 K8s/노드 이벤트까지 확장해 조사하며
- 외부 API는 재시도와 backoff로 “일시 장애”를 흡수
이 네 가지를 기본값으로 깔아두면, AbortException 때문에 CI가 불안정해 보이는 문제를 상당 부분 줄일 수 있습니다.