- 실행 중이던 job이 재시작을 견디느냐는 실행 상태가 어디에 얼마나 자주 디스크로 내려가 있느냐로 갈린다. 재개(resume)는 마법이 아니라 직렬화된 상태를 다시 읽어 들이는 것이다.
- 그래서 job 종류에 따라 답이 완전히 다르다. Freestyle job은 재개되지 않는다. 빌드 컨텍스트를 복원할 방법이 없어서, controller가 죽으면 실행 중이던 빌드는 그대로 중단된다. 재개를 논할 수 있는 것은 Pipeline(
workflow-job)뿐이다. - Pipeline이 재개되려면 두 곳의 상태가 모두 살아 있어야 한다. 축이 다르므로 따로 봐야 한다.
- controller 쪽: 파이프라인 프로그램이 어느 스텝까지 진행됐는지(CPS 실행 상태). 빌드 디렉터리에 직렬화된다. → durability 설정의 관할.
- agent 쪽: 그 스텝이 띄운 실제 프로세스. controller와 별개로 살아 있을 수 있다.

controller가 재시작될 때#
- 종료 방식이 먼저다. graceful shutdown이면 Jenkins가 상태를 flush할 기회를 얻어 durability 설정과 무관하게 파이프라인 상태가 보존된다.
- SIGTERM·SIGINT도 graceful이다. 그리고
/safeExit까지 갈 필요가 없다 — 문서가 “실행 중 파이프라인이 완료될 필요는 없다"고 못박고 있다. 어차피 재개되기 때문이다. - dirty shutdown은 SIGKILL로 프로세스를 죽이거나 컨테이너·VM 자체를 죽이는 경우다. 컨테이너를 단순히 stop하거나 pause하는 것은 해당하지 않는다. Jenkins 프로세스가 나중에 이어서 실행될 수만 있으면 된다. OS의 OOM Killer가 java 프로세스를 죽이는 것은 dirty에 들어간다.
- dirty shutdown이라고 무조건 빌드를 잃는 것은 아니다. 잃는 쪽은 원자적 쓰기를 하지 않는 durability 설정이고,
MAX_SURVIVABILITY는 이 경우에도 재개된다. 바로 아래에서 나눠 본다. - 관리용 엔드포인트는 네 가지다.
safe~ 계열은 quiet mode로 들어가 새 빌드를 받지 않고 실행 중인 빌드가 끝나기를 기다린다.
| 엔드포인트 | 동작 |
|---|
/quietDown | 새 빌드만 막고 계속 실행 (/cancelQuietDown으로 해제) |
/safeRestart | quiet mode → 실행 중 빌드 완료 대기 → 재시작 |
/safeExit | quiet mode → 실행 중 빌드 완료 대기 → 종료 |
/restart, /exit | 즉시. 실행 중 빌드를 기다리지 않는다 |
- 빌드 큐는 살아남는다. 대기 중인 항목은
queue.xml에 저장되므로 재시작 후 그대로 다시 스케줄된다. 재개 문제는 어디까지나 “이미 실행이 시작된” 빌드의 이야기다. - 재개 시 Pipeline은 저장된 프로그램 상태를 로드한 뒤
node 블록을 이어가려 하고, 그동안 빌드 화면에 Waiting to resume part of ...: Waiting for next available executor on <agent>가 뜬다. 원래 붙어 있던 agent가 다시 online 되기를 기다리는 상태다. - 이 기다림에 타임아웃이 하나 있다. agent가 Jenkins 설정에서 삭제되면(
RemovedNodeListener) 같은 이름으로 다시 등록되지 않으리라 보고 기본 5분 뒤 빌드를 취소한다.- 상수는
ExecutorStepExecution.TIMEOUT_WAITING_FOR_NODE_MILLIS, 시스템 프로퍼티는 옛 클래스 이름을 그대로 쓰는 org.jenkinsci.plugins.workflow.support.pickles.ExecutorPickle.timeoutForNodeMillis. - 단, cloud agent처럼 일회성(
OnceRetentionStrategy) 노드는 5분을 기다리지 않고 즉시 취소한다. 어차피 돌아오지 않기 때문.
- 모든 스텝이 재개 가능한 것은 아니다.
checkout, junit처럼 보통 금방 끝나지만 재시작을 견디도록 설계되지 않은 스텝이 있다. 재시작 순간 이런 스텝을 실행 중이었다면 그 스텝은 실패한다 — 뒤에 나올 nonresumable() 재시도 조건이 이 경우를 위한 것이다. - 재개를 원치 않으면 Declarative에서 명시적으로 끈다. 재개된 파이프라인이 오히려 위험한 배포 job 등에 쓴다.
options { disableResume() }
durability — FlowDurabilityHint#
- durability(내구성) 설정은 파이프라인 실행 상태를 얼마나 자주·얼마나 안전하게 디스크에 쓸지의 트레이드오프다. 안전할수록 디스크 I/O가 늘어 파이프라인이 느려진다.
| 값 | 디스크 쓰기 | 원자적 쓰기 | 프로세스 강제 종료 | OS 장애 |
|---|
MAX_SURVIVABILITY | 스텝마다 | O | 재개 | 재개 |
SURVIVABLE_NONATOMIC | 스텝마다 | X | 재개 | 유실 위험 |
PERFORMANCE_OPTIMIZED | 완료돼야 씀 | X | 재개 불가 | 재개 불가 |
- 두 축으로 갈린다.
FlowDurabilityHint가 실제로 들고 있는 boolean 플래그 이름 그대로다.persistWithEveryStep: 언제 쓰는가. false면 “파이프라인이 어떤 식으로든 완료돼야 저장한다”.atomicWrite: 어떻게 쓰는가. true면 임시 파일에 다 쓰고 rename, false면 대상 파일에 직접.
- 원자적 쓰기를 하는 것은
MAX_SURVIVABILITY뿐이다. 문서 표현으로 “All settings except maximum durability currently avoid atomic writes”. 나머지 둘은 OS 자체가 죽으면(호스트 정전, VM halt, 스토리지 분리) 쓰기 버퍼에 있던 데이터가 flush되지 못해 사라지고, 드물게는 빌드를 아예 로드하지 못하게 된다. PERFORMANCE_OPTIMIZED는 “빠르지만 깨끗하게 종료할 때만 살아남는다"로 읽으면 된다. 짧고 자주 도는 job에는 이득이 크지만, controller가 kill 당하는 환경(쿠버네티스 노드 축출 등)에서는 실행 중이던 빌드를 통째로 잃는다.- 공식 권장은 전역 기본값을
PERFORMANCE_OPTIMIZED로 두는 것이다. 그리고 아래 경우에만 job이나 브랜치 단위로 MAX_SURVIVABILITY를 지정한다. 기본값(MAX_SURVIVABILITY)을 그대로 쓰는 것이 무난하다는 통념과 반대다.- 실행 기록 자체가 감사 대상인 경우
- 프로덕션 배포
- 핵심 인프라의 상태를 바꾸는 파이프라인
- 종료 방식과 durability를 합치면 재개 여부가 이렇게 갈린다.

- 설정 위치는 네 군데다. 아래로 갈수록 좁은 범위이며 좁은 쪽이 이긴다.
- 전역:
Manage Jenkins → System → Pipeline Speed/Durability Settings - job별: job 설정 상단의
Custom Pipeline Speed/Durability Level - 파이프라인 코드:
properties 스텝 (다음 실행부터 적용된다) - Multibranch: SCM의 Branch Property Strategy로 브랜치별 지정
properties([durabilityHint('PERFORMANCE_OPTIMIZED')])
agent가 재시작되거나 끊길 때#
- 이 절은 Pipeline 전제다. 아래의 복원력은 전부 Pipeline 스텝의 것이다. Freestyle은 순단조차 못 견디는데, 이유는 절 끝에서 따로 본다.
- 핵심은
sh/bat이 채널에 매달려 있지 않다는 것이다. 스크립트는 agent JVM의 자식이 아니라 분리된 세션으로 실행되고, 진행 상황과 종료 코드는 agent 로컬에 기록된다. controller는 그 기록을 들여다볼 뿐이다. 그래서 채널이 끊겨도 프로세스는 계속 돈다. - 층위를 나눠 보면 무엇이 죽었을 때 무엇이 같이 죽는지가 보인다.

- 그래서 상황을 세 가지로 나눠야 한다. “agent 재시작"이라는 한 단어에 서로 다른 결과가 섞여 있다. 아래 번호는 위 그림의 층위다.
- ③만 끊겼다 붙는 경우(네트워크 순단, agent JVM은 살아 있음): ①과 기록이 그대로이므로 재접속 후 아무 일 없었다는 듯 이어진다. 가장 잘 견디는 경우다.
- ②나 agent 머신이 재시작된 경우: ①이 함께 죽는다. 종료 코드가 영영 남지 않으므로 그 스텝은 복구할 수 없고 결국 실패한다. 워크스페이스 파일 자체는 남아 있으니 재시도의 출발점은 된다.
- agent가 통째로 소멸한 경우(Kubernetes pod 삭제·축출, EC2 종료): 워크스페이스까지 통째로 사라진다. 재개는 원천적으로 불가능하고, 새 agent를 받아 처음부터 다시 하는 것이 유일한 답이다. 여기서
retry가 필요해진다. - 반대로 ④만 죽는 경우는 이 절이 아니라 앞의 controller 재시작 문제다. ①은 계속 돌고 있으므로, controller가 돌아와 재개하면 그동안의 진행분을 그대로 물려받는다.
- agent가 executor 목록에서 제거되면 진행 중이던
node 블록은 실패로 처리된다.
Freestyle은 순단조차 못 견딘다#
- Freestyle의 “Execute shell"은
hudson.tasks.CommandInterpreter가 처리하는데, 실행 방식이 근본적으로 다르다.
Launcher.ProcStarter procStarter = launcher.launch();
procStarter.cmds(buildCommandLine(script))
.stdout(listener) // 출력을 채널로 실시간 스트리밍
.pwd(ws);
Proc proc = procStarter.start();
r = join(proc); // 채널 위에서 종료를 블로킹 대기
.stdout(listener)는 프로세스 출력을 채널을 타고 controller로 그때그때 흘려보낸다. agent 로컬 파일에 남기지 않는다. proc.join()은 controller가 채널 너머의 프로세스가 끝나기를 붙들고 기다리는 호출이다.- 채널이 끊기면 이 둘이 동시에 무너진다.
join()이 IOException을 던지고 종료 코드가 -1로 남아 빌드가 실패한다. 이 클래스에는 ChannelClosedException을 특별 취급하는 코드와 함께 “A common error there is channel losing a connection”(JENKINS-5073)이라는 주석이 달려 있다.
| Freestyle (CommandInterpreter) | Pipeline (sh 스텝) |
|---|
| 프로세스 위치 | agent JVM의 자식, 채널에 매달림 | 분리된 세션 |
| 출력 경로 | 채널로 실시간 스트리밍 | agent 로컬에 기록 |
| 완료 감지 | proc.join() 블로킹 대기 | controller가 기록을 확인 |
| 채널 단절 시 | 빌드 실패 | 재접속 후 이어감 |
- 위 층위 그림에서 ①이 ②·③과 분리돼 있다는 것이 Pipeline의 특징이다. Freestyle에는 그 분리가 없어서 ③이 끊기는 순간 ①의 결과를 회수할 길이 사라진다.
- 대안도 없다. 재개도 없고 스텝 단위
retry도 없다 — retry와 ErrorCondition은 전부 Pipeline 스텝 API 위에 있다. 남는 선택지는 빌드 전체를 다시 큐에 넣는 것뿐이고, 그것도 별도 플러그인이나 사람 손에 의존한다. - 프로세스 자체는 agent JVM이 살아 있는 한 계속 돌 수 있다. 다만 controller가 결과를 받을 방법이 없으므로 아무도 거두지 않는 프로세스로 남는다.
retry — 재개가 안 될 때의 차선#
retry(count)는 블록에서 예외가 나면 다시 실행한다. count는 재시도 횟수가 아니라 총 시도 횟수다 — retry(3)이면 최초 1회 + 재시도 2회.- 조건을 주지 않으면 사용자 abort를 제외한 모든 오류에 대해 재시도하므로, 컴파일 에러 같은 진짜 실패까지 반복하게 된다.
conditions를 주면 인프라 원인일 때만 재시도한다. 조건 중 하나라도 맞아야 하고, 하나도 안 맞으면 그대로 실패시킨다.conditions를 주는 순간 “실제 인터럽트는 재시도하지 않는다"는 기본 검사가 통째로 우회되고 오직 조건만 본다. timeout과 조합할 때 걸린다 — agent()는 timeout으로 인한 중단을 인프라 원인으로 인정하지 않으므로 재시도하지 않는다.
| 조건 | 대상 |
|---|
agent() | 채널 단절, 사용 중 agent가 executor 목록에서 제거되는 등 빌드 자체가 아닌 인프라 원인 |
kubernetesAgent() | 위와 같되 Kubernetes cloud 전용. OOMKilled처럼 파이프라인 작성자 책임인 pod 종료 사유는 제외한다. handleNonKubernetes: true를 주면 비K8s agent에는 agent()처럼 동작 |
nonresumable() | controller 재시작 때 재개 불가 스텝(checkout, junit 등)이 실행 중이어서 죽은 경우 |
retry는 node 바깥에 둬야 한다. 안쪽에 두면 죽은 agent에서 다시 시도하게 되어 의미가 없다. 새 agent를 할당받으려면 node 블록 전체를 감싸야 한다.
retry(count: 3, conditions: [agent(), nonresumable()]) {
node('linux') {
checkout scm
sh './build.sh'
}
}
- Kubernetes pod 소멸에 대비하는 조합은
kubernetesAgent() + nonresumable()이다. pod 유실이라는 원인에만 반응해 같은 pod 정의로 블록 전체를 다시 돌린다. - 재시도는 재개가 아니라 처음부터 재실행이다. 실패한 시도가 이미 해놓은 일은 그대로 남으므로, 블록 안에 멱등하지 않은 작업(배포·마이그레이션)이 있으면 위험하다.
- 라벨에 맞는 agent가 하나뿐이면 그 agent가 다시 online 될 때까지 큐에서 기다린다. 빨리 실패하지 않는다.
Declarative의 retries#
- Declarative에서는
retry를 직접 쓰지 않고 agent 지시어의 retries 를 쓴다. agent 재시작 대비로 먼저 손이 가야 할 쪽이다.
agent {
node {
label 'linux'
retries 3
}
}
- 실행 시점에 아래 코드로 펼쳐진다(
Label 클래스가 RetryableDeclarativeAgent를 상속한다). retry가 node 바깥이라 재시도마다 agent를 새로 할당받고, checkout scm이 안쪽이라 체크아웃도 다시 한다.
retry(count: 3, conditions: [agent(), nonresumable()]) {
node('linux') {
checkout scm
// ...본문...
}
}
- 조건은
agent() + nonresumable()로 고정이라 바꿀 수 없다. 대신 인프라 원인이 아닌 실패는 재시도하지 않는다. retries도 총 시도 횟수다. 기본값이 1이고 코드가 retries > 1로 분기하므로 1이면 retry 래퍼가 아예 안 붙는다. 재시도를 2번 시키려면 retries 3이라고 써야 한다.- 지원하는 agent 타입이 제한적이다.
| agent 타입 | retries | 쓰이는 조건 |
|---|
label, node | 지원 | agent() + nonresumable() |
kubernetes | 지원 | kubernetesAgent() + nonresumable() |
docker, dockerfile | 미지원 | — |
label과 node는 같은 Label 클래스(@Symbol({"label","node"}))가 처리하지만, agent { label 'linux' }는 중괄호 블록이 없어 retries를 붙일 수 없다. agent { node { ... } } 형태로 써야 한다.- 공식 Pipeline Syntax 문서에
retries가 없다. options의 retry만 실려 있어서 문서만 보면 존재를 알 수 없다. 확인하려면 소스나 Declarative Directive Generator를 봐야 한다. - 도입은
pipeline-model-definition 2.2114(2022년 7월). 그보다 오래된 Jenkins에는 없다.
options { retry(n) }과의 차이#
options { retry(n) }은 conditions를 받지 않는다. 그래서 컴파일 에러나 테스트 실패까지 n번 반복한다. agent 재시작 대비로는 잘못된 도구다.- 감싸는 범위는 더 넓다. stage에 두면 그 stage 전체, 파이프라인 최상위에 두면 파이프라인 전체를 다시 돌린다.
- stage의
options는 agent 할당과 when 조건 확인보다 먼저 실행되므로, stage-level retry도 재시도할 때 agent를 새로 잡기는 한다.
| options { retry(n) } | agent { node { retries n } } |
|---|
| 조건 | 없음 — 모든 오류 | agent() + nonresumable() 고정 |
| 감싸는 범위 | stage 또는 파이프라인 전체 | agent 할당 + 체크아웃 + 본문 |
| 컴파일·테스트 실패 | 재시도함 | 재시도 안 함 |
상황별 정리#
| 상황 | 결과 |
|---|
| Freestyle, controller 재시작 | 항상 중단. 재개 불가 |
| Pipeline, graceful shutdown | durability 설정과 무관하게 재개 |
Pipeline, 강제 종료 + MAX_SURVIVABILITY | 재개. 마지막 스텝 경계까지 복원. OS 장애에도 원자적 쓰기로 방어 |
Pipeline, 강제 종료 + PERFORMANCE_OPTIMIZED | 재개 불가. 빌드 유실 |
재개 시점에 checkout 등 실행 중이었음 | 해당 스텝 실패 → nonresumable()로 재시도 |
| agent 채널 순단 후 재접속 | sh는 그대로 이어짐. 프로세스가 살아 있으므로 |
| agent 프로세스·머신 재시작 | 자식 프로세스 사망 → 스텝 실패 → agent()(Declarative는 retries)로 재시도 |
| cloud agent 소멸(pod 삭제 등) | 워크스페이스까지 유실. kubernetesAgent()로 새 agent 받아 재실행 |
| agent를 설정에서 삭제 | 정적 agent는 5분 뒤, 일회성 agent는 즉시 빌드 취소 |
참고 자료#
retries는 공식 문서에 없어서 아래가 사실상 전부다.