- Published on
Jenkins Declarative Pipeline에서 sh 127 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Jenkins Declarative Pipeline을 운영하다 보면 sh 스텝이 갑자기 script returned exit code 127로 실패하는 경우가 자주 있습니다. 127은 리눅스/유닉스 계열에서 명령을 찾지 못했을 때 가장 흔히 반환되는 종료 코드입니다. 문제는 Jenkins에서는 같은 명령이 로컬에서는 잘 되고, 어떤 에이전트에서는 되고, 어떤 도커 이미지에서는 안 되는 식으로 재현 조건이 복잡해진다는 점입니다.
이 글에서는 sh 127을 단순히 “PATH 문제”로 뭉뚱그리지 않고, Declarative Pipeline에서 실제로 자주 발생하는 케이스를 원인별로 분류한 뒤, 각각을 확인하는 방법과 고치는 방법을 코드 예제와 함께 정리합니다.
sh 127의 의미와 Jenkins에서 흔한 착시
- 종료 코드
127: 보통command not found를 의미합니다. - Jenkins Pipeline의
sh는 기본적으로sh -xe같은 형태로 실행되며, 실행 환경은 다음 요인에 크게 좌우됩니다.- Jenkins 에이전트 노드(머신/컨테이너)의 기본 이미지
- 로그인 셸 여부(대부분 비로그인)
PATH및 환경 변수 주입 방식- 워크스페이스 체크아웃 여부
- 권한/실행 비트
즉, 같은 sh 'node -v'라도 “내 PC”가 아니라 “에이전트 컨테이너 내부”에서 실행된다고 생각해야 합니다.
먼저 해야 할 최소 진단 스니펫
127이 났을 때 감으로 고치기 전에, 실패한 스테이지 근처에 아래를 잠깐 넣어 환경을 고정적으로 출력해보면 원인 분류가 쉬워집니다.
pipeline {
agent any
stages {
stage('debug env') {
steps {
sh '''
set -eux
echo "SHELL=$SHELL"
echo "PATH=$PATH"
whoami
pwd
ls -la
command -v bash || true
command -v sh || true
command -v python || true
command -v node || true
'''
}
}
}
}
여기서 command -v가 비어 있으면 “그 명령은 그 환경에 없다”가 확정입니다.
원인 1: 에이전트에 명령이 설치되어 있지 않음(가장 흔함)
증상
sh: 1: node: not foundsh: git: not founddocker: not found
왜 Jenkins에서 더 자주 터지나
- 로컬에는 설치되어 있지만, Jenkins 에이전트 노드에는 없음
- Kubernetes 플러그인이나 Docker agent를 쓰면, 이미지에 없는 바이너리는 당연히 실행 불가
해결
1) 에이전트 이미지를 바꾸거나 필요한 패키지를 설치
Kubernetes 기반이라면 도구가 포함된 빌드 이미지를 쓰는 것이 정석입니다.
pipeline {
agent {
docker {
image 'node:20-bullseye'
args '-u root:root'
}
}
stages {
stage('build') {
steps {
sh 'node -v'
sh 'npm ci'
}
}
}
}
2) Jenkins Tool 설정을 쓰되, PATH 주입을 확인
Jenkins의 NodeJS/Gradle/Maven tool을 쓰는 경우에도, Declarative에서 올바르게 래핑하지 않으면 PATH에 안 잡힐 수 있습니다.
예: NodeJS 플러그인을 쓴다면 tools와 withEnv 사용 여부를 점검하세요.
pipeline {
agent any
tools {
nodejs 'node-20'
}
stages {
stage('check') {
steps {
sh '''
set -eux
node -v
npm -v
'''
}
}
}
}
원인 2: PATH가 기대와 다름(비로그인 셸)
증상
npm은 안 보이는데node는 보임pyenv/nvm로 설치한 바이너리가 Jenkins에서는 전혀 인식되지 않음
핵심
Jenkins의 sh는 대개 로그인 셸이 아니므로 ~/.profile, ~/.bashrc 같은 초기화 파일이 자동으로 적용되지 않습니다. 특히 nvm/pyenv는 셸 초기화에 의존하므로 그대로 쓰면 실패하기 쉽습니다.
해결 1) 명시적으로 PATH를 주입
pipeline {
agent any
environment {
PATH = "/usr/local/bin:/usr/bin:/bin:${env.PATH}"
}
stages {
stage('run') {
steps {
sh 'command -v node && node -v'
}
}
}
}
해결 2) bash 로그인 셸로 실행(필요한 경우)
기본 sh는 dash일 수 있고, source 같은 bash 문법이 깨질 수 있습니다. 이때는 bash로 명시하세요.
pipeline {
agent any
stages {
stage('use bash login') {
steps {
sh '''
bash -lc 'set -eux; command -v nvm || true; node -v'
'''
}
}
}
}
여기서 bash -lc는 로그인 셸 환경을 최대한 재현하려는 목적입니다. 다만 근본적으로는 빌드 도구를 이미지에 포함시키는 쪽이 더 재현성이 좋습니다.
원인 3: 워크스페이스에 파일이 없거나 체크아웃이 안 됨
증상
./gradlew: not found./scripts/build.sh: not found- 특정 스테이지에서만 파일이 없다고 나옴
주요 패턴
agent none를 쓰고 스테이지마다 에이전트를 붙였는데, 어떤 스테이지에는checkout scm이 없음stash/unstash없이 다른 노드로 넘어감
해결 1) Declarative의 기본 체크아웃 동작 확인
Declarative는 기본적으로 checkout scm을 자동 수행하지만, 옵션에 따라 꺼질 수 있습니다.
pipeline {
agent any
options {
skipDefaultCheckout(false)
}
stages {
stage('build') {
steps {
sh 'ls -la'
sh './gradlew --version'
}
}
}
}
해결 2) 멀티 에이전트면 stash/unstash 사용
pipeline {
agent none
stages {
stage('checkout') {
agent any
steps {
checkout scm
stash name: 'src', includes: '**/*'
}
}
stage('build on docker') {
agent {
docker {
image 'gradle:8-jdk17'
}
}
steps {
unstash 'src'
sh 'ls -la'
sh 'gradle -v'
}
}
}
}
원인 4: 실행 권한(Executable bit) 또는 줄바꿈(CRLF) 문제
증상
./gradlew: Permission denied가 아니라not found로 보이는 경우도 있음./script.sh: not found인데 파일은 존재
특히 CRLF로 저장된 셸 스크립트는 인터프리터 경로가 .../bash^M처럼 인식되어 not found로 떨어질 수 있습니다.
해결 1) 실행 비트 부여
sh '''
set -eux
chmod +x ./gradlew
./gradlew tasks
'''
해결 2) CRLF 제거
sh '''
set -eux
sed -i 's/\r$//' ./scripts/build.sh
chmod +x ./scripts/build.sh
./scripts/build.sh
'''
Git 설정으로는 core.autocrlf와 .gitattributes를 점검해 재발을 막는 게 좋습니다.
원인 5: shebang 인터프리터가 이미지에 없음
증상
- 파이썬 스크립트 첫 줄이
#!/usr/bin/env python인데python이 없음 #!/bin/bash인데 컨테이너에bash가 없음(알파인 등)
이 경우 실행 시도 결과가 not found로 나타날 수 있습니다.
해결
- 스크립트가 요구하는 인터프리터를 이미지에 설치
- 또는 스크립트를 명시적으로 인터프리터로 실행
sh '''
set -eux
command -v python3
python3 ./tools/gen.py
'''
알파인 기반 이미지는 bash가 기본 탑재가 아닐 때가 많으니, sh 문법으로 통일하거나 bash를 설치하세요.
원인 6: docker/kubectl 같은 외부 바이너리 의존
증상
docker: not foundkubectl: not found
해결 방향
- 도구가 포함된 빌드 이미지 사용
- Jenkins 노드에 바이너리 설치
- Kubernetes에서는 사이드카 컨테이너 패턴 고려
kubectl이 필요한 파이프라인이라면, 클러스터 접근 문제까지 같이 엮여 장애가 커지기 쉽습니다. 네트워크/권한 이슈로 넘어가면 다음 글도 함께 참고해두면 좋습니다.
재발 방지 체크리스트(운영 관점)
운영에서는 “한 번 고치고 끝”이 아니라, 같은 유형의 127을 구조적으로 줄이는 게 중요합니다.
1) 빌드 환경을 이미지로 고정
에이전트 노드에 수동 설치를 늘릴수록, 노드 교체/스케일 시 재현성이 깨집니다. 가능한 한 Docker/Kubernetes 환경에서는 빌드 도구를 이미지에 포함시키세요.
2) 파이프라인 초반에 필수 커맨드 검증
빌드가 10분 돌고 나서 127로 죽는 것보다, 초반에 빠르게 실패시키는 편이 비용이 훨씬 적습니다.
stage('preflight') {
steps {
sh '''
set -eux
for c in git curl bash; do
command -v $c
done
'''
}
}
3) 스테이지 간 노드 이동 시 워크스페이스 전달을 명시
멀티 에이전트 구조에서는 stash/unstash를 기본 패턴으로 두는 것이 안전합니다.
4) 장애 대응 문서화
127은 “명령이 없다”로 끝나지 않고, PATH, 체크아웃, CRLF, 인터프리터 부재 등으로 분기됩니다. 장애 대응을 체계화하려면 원인 분류표를 만들어두는 게 좋습니다. 비슷한 결로, 원인을 좁혀가는 트러블슈팅 방식은 다음 글에서도 다룹니다.
실전 예시: ./gradlew not found를 단계적으로 해결
아래는 실제로 자주 보는 케이스를 “진단 → 해결” 흐름으로 정리한 예시입니다.
실패 파이프라인
pipeline {
agent any
stages {
stage('build') {
steps {
sh './gradlew test'
}
}
}
}
1단계: 파일 존재 확인
sh '''
set -eux
ls -la
ls -la ./gradlew || true
'''
- 여기서
gradlew가 없다면 체크아웃/워크스페이스 문제 - 있다면 다음 단계
2단계: 실행 비트 및 줄바꿈 확인
sh '''
set -eux
chmod +x ./gradlew
sed -i 's/\r$//' ./gradlew
./gradlew --version
'''
3단계: 인터프리터 확인
gradlew는 보통 sh로 동작하지만, 컨테이너/환경에 따라 필요한 유틸리티가 없을 수 있습니다. 이때는 JDK/필수 패키지가 포함된 이미지를 쓰는 것이 가장 빠릅니다.
pipeline {
agent {
docker {
image 'gradle:8-jdk17'
}
}
stages {
stage('build') {
steps {
sh 'gradle -v'
sh 'chmod +x ./gradlew'
sh './gradlew test'
}
}
}
}
마무리: 127은 “환경 드리프트”의 신호다
Jenkins Declarative Pipeline에서 sh 127은 대부분 “명령이 없다”이지만, 그 뒤에는 환경 드리프트가 숨어 있는 경우가 많습니다. 특히 도커/쿠버네티스 기반으로 갈수록, 로컬과 CI 환경의 차이가 커져서 재현이 어려워집니다.
정리하면 다음 순서로 접근하면 빠르게 해결됩니다.
command -v로 정말 명령이 없는지 확정- 에이전트 이미지/노드에 설치 여부 확인
PATH와 로그인 셸 차이 점검- 체크아웃/워크스페이스/
stash흐름 점검 - 실행 비트, CRLF, shebang 인터프리터 점검
이 순서대로만 체크해도, 대부분의 script returned exit code 127은 원인을 짧은 시간 안에 특정하고 재발까지 줄일 수 있습니다.