- Published on
Jenkins Declarative Pipeline에서 env가 안 먹는 6가지 이유
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Jenkins Declarative Pipeline을 쓰다 보면 env가 분명히 설정됐는데도 쉘에서 비어 있거나, when/environment에서 기대대로 평가되지 않거나, 스테이지마다 값이 달라지는 문제가 자주 발생합니다. 대부분은 Declarative와 Scripted의 평가 시점 차이, Groovy 문자열 처리, 스텝이 만드는 서브 프로세스 경계, withEnv/environment의 스코프, Credentials 바인딩 방식, 에이전트/컨테이너 분리 같은 “경계”에서 생깁니다.
아래는 현장에서 가장 많이 만나는 “env 안 먹는” 원인 6가지와, 각 원인별로 바로 적용 가능한 해결 패턴입니다.
1) env.FOO = ...를 environment {}에서 동적으로 만들려고 함
Declarative의 environment {} 블록은 기본적으로 파이프라인 로딩/컴파일 단계에서 해석되는 성격이 강합니다. 즉, 런타임에 계산되는 값(예: sh 결과, 파일 읽기, API 호출)을 그대로 넣으려고 하면 안 먹거나, 예상과 다르게 동작합니다.
흔한 실패 예
pipeline {
agent any
environment {
// 런타임 계산을 넣고 싶어도 Declarative 환경에서는 제약이 큼
VERSION = "${sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()}"
}
stages {
stage('Print') {
steps {
sh 'echo $VERSION'
}
}
}
}
위는 Jenkins 버전/플러그인 조합에 따라 아예 에러가 나거나, 빈 값/이상한 값이 들어갈 수 있습니다.
해결 패턴
런타임 계산이 필요하면 script {}에서 계산하고 env에 대입한 뒤 사용합니다.
pipeline {
agent any
stages {
stage('Init') {
steps {
script {
env.VERSION = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
}
}
}
stage('Print') {
steps {
sh 'echo VERSION=$VERSION'
}
}
}
}
2) Groovy 싱글쿼트 때문에 변수 치환이 안 됨 (특히 sh 인자)
sh에 넘기는 문자열은 Groovy 문자열 규칙을 그대로 따릅니다. 싱글쿼트는 문자열 보간(interpolation) 이 일어나지 않아서, ${env.FOO} 같은 표현이 그대로 문자열로 전달됩니다.
흔한 실패 예
pipeline {
agent any
stages {
stage('Bad') {
steps {
script {
env.IMAGE = 'myapp:1.0.0'
}
sh 'echo ${env.IMAGE}'
}
}
}
}
위 코드는 쉘에 ${env.IMAGE} 그대로 전달됩니다(쉘 변수도 아니어서 비어 보이거나 그대로 출력).
해결 패턴 1: 쉘 변수로 접근하기
env는 Jenkins가 쉘 환경변수로 주입하므로, 쉘에서는 $IMAGE로 읽는 게 가장 단순합니다.
sh 'echo $IMAGE'
해결 패턴 2: Groovy 보간이 필요하면 더블쿼트 사용
sh "echo ${env.IMAGE}"
3) sh 내부에서 export한 값이 다음 sh에 안 이어짐 (프로세스 경계)
Declarative에서 sh 스텝은 호출될 때마다 별도 프로세스로 실행됩니다. 그래서 한 번의 sh에서 export FOO=bar를 해도, 다음 sh에서는 사라집니다. 이걸 env가 안 먹는 것으로 오해하는 경우가 매우 많습니다.
흔한 실패 예
pipeline {
agent any
stages {
stage('Bad') {
steps {
sh 'export FOO=bar'
sh 'echo $FOO' // 비어 있음
}
}
}
}
해결 패턴 1: 한 번의 sh로 묶기
sh '''
export FOO=bar
echo $FOO
'''
해결 패턴 2: Jenkins env로 올리기
script {
env.FOO = 'bar'
}
sh 'echo $FOO'
4) withEnv/environment 스코프를 벗어나서 사용함
withEnv([...])는 해당 블록 내부에서만 환경변수를 주입합니다. 블록 밖에서 $FOO를 기대하면 안 먹습니다. environment {}도 마찬가지로, 선언 위치/스테이지 경계에 따라 적용 범위가 달라집니다.
흔한 실패 예: withEnv 범위 밖
pipeline {
agent any
stages {
stage('Bad') {
steps {
withEnv(['FOO=bar']) {
sh 'echo inside=$FOO'
}
sh 'echo outside=$FOO' // 비어 있음
}
}
}
}
해결 패턴
- 블록 밖에서도 필요하면
env.FOO에 대입하거나 - 필요한 구간 전체를
withEnv로 감싸거나 - 스테이지 레벨
environment {}로 올립니다.
pipeline {
agent any
stages {
stage('Good') {
environment {
FOO = 'bar'
}
steps {
sh 'echo $FOO'
}
}
}
}
5) Credentials 바인딩/마스킹 때문에 “안 먹는 것처럼” 보임
withCredentials로 주입된 값은 로그에 그대로 찍히지 않도록 마스킹되며, 경우에 따라 빈 값처럼 보이거나 ****로 표시됩니다. 또한 environment {}에서 credentials('id')로 주입하는 방식은 타입(Secret text, Username/Password 등)에 따라 변수명이 달라져 혼동이 생깁니다.
흔한 실패 예: 로그에 안 찍혀서 비어 보임
pipeline {
agent any
stages {
stage('Cred') {
steps {
withCredentials([string(credentialsId: 'my-token', variable: 'TOKEN')]) {
sh 'echo $TOKEN' // 마스킹되어 **** 또는 빈 것처럼 보일 수 있음
}
}
}
}
}
해결 패턴
- 토큰이 “존재하는지”만 확인하려면 길이/해시 등으로 검증합니다.
- Jenkins 로그 마스킹 정책을 존중하고, 원문 출력은 피합니다.
withCredentials([string(credentialsId: 'my-token', variable: 'TOKEN')]) {
sh 'test -n "$TOKEN" && echo "TOKEN is set" || (echo "TOKEN missing"; exit 1)'
sh 'echo "len=${#TOKEN}"'
}
추가로, Username/Password는 보통 USERNAME/PASSWORD 두 변수를 만들므로 변수명을 착각하지 않게 주의합니다.
6) 에이전트/도커 컨테이너/병렬 실행으로 실행 환경이 갈라짐
Declarative에서 agent를 스테이지별로 다르게 쓰거나, docker/kubernetes 에이전트로 컨테이너가 바뀌면 “같은 파이프라인”이라도 실행 환경이 완전히 분리됩니다. 이때 env는 Jenkins 레벨에서 전달되지만, 컨테이너 진입 방식이나 스텝 실행 위치에 따라 기대와 다른 결과가 나올 수 있습니다.
대표 케이스:
- 스테이지 A는
agent any, 스테이지 B는agent { docker { ... } }로 컨테이너가 바뀜 parallel에서 각 브랜치가 서로 다른 워크스페이스/노드에서 실행- Kubernetes 플러그인에서 컨테이너별로
sh가 실행되는 컨테이너가 다름
흔한 실패 예: 컨테이너가 바뀌며 기대가 틀어짐
pipeline {
agent any
stages {
stage('Init') {
steps {
script { env.FOO = 'bar' }
}
}
stage('Docker') {
agent {
docker { image 'alpine:3.19' }
}
steps {
sh 'echo $FOO'
}
}
}
}
대부분은 동작하지만, 플러그인/설정에 따라 컨테이너 실행 래퍼가 환경 전달을 제한하거나, 다른 셸/엔트리포인트로 인해 값이 누락된 것처럼 보일 수 있습니다.
해결 패턴
- 컨테이너/노드가 바뀌는 구간에서는 필요한 값을 명시적으로 전달합니다.
withEnv로 스테이지 내부에서 재주입하거나, 아예 커맨드 인자로 넘깁니다.
stage('Docker') {
agent { docker { image 'alpine:3.19' } }
steps {
withEnv(["FOO=${env.FOO}"]) {
sh 'echo $FOO'
}
}
}
병렬에서는 브랜치별로 env 변경이 섞일 수 있으니(공유 상태) branch 내부 로컬 변수 또는 withEnv를 우선 고려합니다.
디버깅 체크리스트: “env가 안 먹는다”를 빨리 분해하는 법
문제를 재현했을 때 아래 순서로 확인하면 원인을 빠르게 좁힐 수 있습니다.
- Groovy 레벨 확인
script {
echo "FOO(groovy)=${env.FOO}"
}
- 쉘 레벨 확인
sh 'printenv | sort | sed -n "1,120p"'
sh 'echo "FOO(shell)=$FOO"'
- 스코프 확인
withEnv/withCredentials블록 밖에서 읽고 있지 않은지- 스테이지별
environment {}선언 위치가 의도한 범위인지
- 실행 경계 확인
- 다른
agent/컨테이너/노드로 넘어갔는지 parallel로 동시에env를 덮어쓰고 있지 않은지
운영에서 자주 같이 터지는 이슈들
CI에서 환경변수 문제는 종종 “네트워크/권한/인프라 문제”와 함께 나타납니다. 예를 들어 특정 노드에서만 env가 비어 보인다면, 사실은 에이전트의 접속 불안정이나 세션 종료로 스텝이 재시도/재스케줄링되면서 실행 컨텍스트가 바뀐 것일 수 있습니다. 이런 경우는 리눅스 SSH 접속 끊김·Timeout 원인 10가지 같은 관점으로 에이전트 안정성도 같이 점검하는 편이 좋습니다.
또한 Kubernetes 기반 Jenkins 에이전트에서 권한 문제로 특정 커맨드가 실패하고, 그 결과로 env 설정 로직이 스킵되며 “env가 안 먹는 것처럼” 보이는 경우가 있습니다. AWS KMS/IRSA 권한 이슈가 얽히면 증상이 더 헷갈리니 EKS IRSA는 되는데 KMS Decrypt 403 해결법 같은 케이스도 참고할 만합니다.
정리
Declarative Pipeline에서 env가 안 먹는 문제는 대체로 다음 6가지로 귀결됩니다.
environment {}에 런타임 계산을 넣으려 함(평가 시점 문제)- Groovy 싱글쿼트로 보간이 안 됨(문자열 처리 문제)
sh간export가 이어진다고 착각함(프로세스 경계)withEnv/environment스코프 밖에서 사용함(범위 문제)- Credentials 마스킹/바인딩 방식으로 “안 보이는” 착시(보안/표현 문제)
- 에이전트/컨테이너/병렬로 실행 환경이 갈라짐(실행 컨텍스트 문제)
위에서 제시한 재현 코드로 본인 파이프라인을 하나씩 대조해 보면, 대부분은 특정 경계(시점/스코프/프로세스/컨테이너)에서 원인을 바로 찾을 수 있습니다.