- Published on
Jenkins Declarative 환경변수 스코프 꼬임 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 스테이지에서 env 값이 갑자기 바뀌거나, environment 블록에서 설정한 값이 script 안에서 기대대로 유지되지 않거나, withEnv 를 썼는데 다음 스테이지까지 영향이 남는 것처럼 보이는 문제는 Jenkins Declarative Pipeline에서 자주 겪는 함정입니다. 특히 멀티 브랜치, 병렬 실행, 재시도, 에이전트 분리(agent none 후 stage별 agent) 같은 구성이 붙으면 “환경변수 스코프가 꼬였다”는 느낌을 강하게 받습니다.
이 글은 Declarative에서 환경변수가 적용되는 범위(스코프) 규칙을 정리하고, 실제로 꼬임이 발생하는 대표 패턴과 해결책(재현 가능한 코드 포함)을 제공합니다.
문제의 전형적인 증상
아래 중 하나라도 겪었다면 거의 같은 원인군입니다.
environment { FOO = 'a' }로 설정했는데sh 'echo $FOO'에서 비어 있거나 다른 값이 출력됨script { env.FOO = 'b' }로 바꿨더니 이후 스테이지까지 값이 전파되어 의도치 않게 빌드가 오염됨withEnv(['FOO=b']) { ... }로 감싼 블록 밖에서도FOO가b로 남아 있는 것처럼 보임parallel에서 서로 다른 브랜치가env를 건드린 뒤 결과가 섞임when { expression { env.BRANCH_NAME == ... } }가 어떤 스테이지에서는 맞고 어떤 스테이지에서는 틀림
CI에서 이런 문제는 “재현이 어렵고, 한 번 터지면 원인 추적이 오래 걸리는” 유형입니다. 비슷한 맥락의 복구/추적 관점은 Git rebase 후 히스토리 꼬임 복구 - reflog 글의 접근(관측 가능한 로그를 기반으로 원인을 좁히기)과도 닮아 있습니다.
Declarative에서 환경변수의 3가지 레이어
Jenkins Pipeline에서 환경변수는 대체로 아래 레이어로 이해하면 덜 헷갈립니다.
1) Declarative environment 블록
- 위치에 따라 스코프가 다릅니다.
pipeline { environment { ... } }는 파이프라인 전역(모든 stage)에 적용stage { environment { ... } }는 해당 stage 내부에서만 적용
- 실제로는 스텝 실행 시점에 환경을 주입하는 형태에 가깝습니다.
2) withEnv([...]) 스텝
- 블록 내부에서만 유효한 “임시 환경”을 제공합니다.
- 블록 밖으로 나가면 원칙적으로 원복됩니다.
- 다만
env객체를 직접 수정(env.FOO = 'x')하면 이 원칙과 섞이면서 혼란이 생깁니다.
3) env 객체 직접 수정 (env.KEY = 'value')
- 가장 강력하지만 가장 위험합니다.
- “현재 빌드의 전역 상태”에 가깝게 동작할 수 있어, 이후 stage에 영향을 줄 수 있습니다.
- 병렬 실행에서 특히 위험합니다. 공유 상태를 동시에 변경하는 레이스 컨디션이 발생할 수 있습니다.
정리하면, Declarative environment 와 withEnv 는 “스코프형”, env.KEY = ... 는 “전역 상태 변경형” 으로 보는 것이 안전합니다.
재현: 스코프가 꼬인 것처럼 보이는 Jenkinsfile
아래 예시는 일부러 안 좋은 패턴을 섞어 “왜 꼬이는지”를 보여줍니다.
pipeline {
agent any
environment {
TARGET_ENV = 'dev'
IMAGE_TAG = 'v1'
}
stages {
stage('A: show') {
steps {
sh 'echo A1 TARGET_ENV=$TARGET_ENV IMAGE_TAG=$IMAGE_TAG'
}
}
stage('B: mutate env (bad)') {
steps {
script {
// 전역 상태를 바꿔버림: 이후 stage까지 영향을 줄 수 있음
env.TARGET_ENV = 'prod'
env.IMAGE_TAG = "v2"
}
sh 'echo B1 TARGET_ENV=$TARGET_ENV IMAGE_TAG=$IMAGE_TAG'
}
}
stage('C: expect dev?') {
steps {
sh 'echo C1 TARGET_ENV=$TARGET_ENV IMAGE_TAG=$IMAGE_TAG'
}
}
stage('D: withEnv') {
steps {
withEnv(['TARGET_ENV=staging']) {
sh 'echo D1 TARGET_ENV=$TARGET_ENV'
}
sh 'echo D2 TARGET_ENV=$TARGET_ENV'
}
}
}
}
많은 분들이 C 에서 dev 가 나오길 기대하지만, B 에서 env.TARGET_ENV 를 바꿔버렸기 때문에 C 는 prod 를 보게 됩니다. withEnv 는 D1 에서만 staging 을 보장하고, D2 는 다시 prod 로 돌아옵니다. 이 자체는 Jenkins 관점에서 “정상”이지만, 운영 관점에서는 “스코프가 꼬였다”로 체감됩니다.
원인 1: env.KEY = ... 를 설정값(설정)처럼 사용
Declarative를 구성 파일처럼 쓰고 싶을 때, env.KEY = ... 를 “지역 변수 대체재”로 쓰는 경우가 많습니다. 하지만 이건 전역 환경을 수정하는 행위에 가깝고, 파이프라인이 길어질수록 부작용이 누적됩니다.
해결 패턴: def 로 지역 변수 사용, 필요한 구간에만 주입
- 계산/분기 결과는 Groovy 변수로 들고 간 뒤
- 쉘 실행 직전에
withEnv로 주입
pipeline {
agent any
environment {
DEFAULT_ENV = 'dev'
}
stages {
stage('Compute') {
steps {
script {
// 지역 변수로 보관
def targetEnv = (env.BRANCH_NAME == 'main') ? 'prod' : env.DEFAULT_ENV
def imageTag = "${env.BUILD_NUMBER}" // 예시
// 필요한 범위에만 주입
withEnv(["TARGET_ENV=${targetEnv}", "IMAGE_TAG=${imageTag}"]) {
sh 'echo TARGET_ENV=$TARGET_ENV IMAGE_TAG=$IMAGE_TAG'
sh 'make deploy'
}
}
}
}
}
}
이 패턴의 장점은 “값이 어디서 바뀌었는지”가 명확해지고, 다음 stage로 오염이 덜 전파된다는 점입니다.
원인 2: stage environment 의 평가 시점/문맥 착각
Declarative environment 는 Groovy 코드가 실행되는 시점과 동일하게 동작하지 않습니다. 특히 아래처럼 “다른 변수에 의존하는 값”을 environment 에서 만들려 하면 기대와 다르게 동작할 수 있습니다.
environment { FOO = "${bar}" }형태sh결과를 넣고 싶어서FOO = sh(...)같은 시도
Declarative의 environment 는 “파이프라인 선언”에 가깝고, 동적 계산은 script 에서 하는 것이 안전합니다.
해결 패턴: 동적 값은 script 에서 계산 후 withEnv
stage('Dynamic env safely') {
steps {
script {
def gitSha = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
withEnv(["GIT_SHA=${gitSha}"]) {
sh 'echo GIT_SHA=$GIT_SHA'
}
}
}
}
원인 3: parallel 에서 env 공유 상태 경쟁
parallel 브랜치가 동시에 env 를 수정하면, 마지막에 쓴 값이 이겨버리거나, 로그 상으로는 섞여 보입니다.
나쁜 예
stage('Parallel bad') {
parallel {
stage('A') {
steps {
script { env.COLOR = 'red' }
sh 'echo A COLOR=$COLOR'
}
}
stage('B') {
steps {
script { env.COLOR = 'blue' }
sh 'echo B COLOR=$COLOR'
}
}
}
}
해결 패턴: 병렬 브랜치 내부는 지역 변수 + withEnv
stage('Parallel good') {
parallel {
stage('A') {
steps {
script {
def color = 'red'
withEnv(["COLOR=${color}"]) {
sh 'echo A COLOR=$COLOR'
}
}
}
}
stage('B') {
steps {
script {
def color = 'blue'
withEnv(["COLOR=${color}"]) {
sh 'echo B COLOR=$COLOR'
}
}
}
}
}
}
병렬 실행에서 공유 상태를 없애는 것이 핵심입니다. 운영에서도 비슷하게 “공유 자원 경쟁”은 장애를 만듭니다. 쿠버네티스에서의 반복 재시작 원인 추적과 관점이 닮아 있어, 필요하면 EKS CrashLoopBackOff 진단 - Pod 재시작 원인 추적 처럼 “관측 포인트를 먼저 세우고” 좁혀가는 방식이 도움이 됩니다.
원인 4: agent none 와 stage별 에이전트에서 생기는 착시
pipeline { agent none } 후 stage마다 다른 노드/컨테이너에서 실행하면, 다음과 같은 착시가 생깁니다.
- 같은
env를 쓰는 것 같은데, 실제로는 서로 다른 실행 환경(워크스페이스, 도구, PATH) sh가 다른 쉘/이미지에서 돌면서 기대한 변수가 없거나 명령이 없음
이 경우 “환경변수 스코프” 문제처럼 보이지만, 사실은 실행 환경 자체가 다름이 원인인 경우가 많습니다.
해결 패턴: 진단 스텝을 넣어 실행 환경을 고정/검증
stage('Diag') {
agent any
steps {
sh 'pwd'
sh 'env | sort | sed -n "1,40p"'
sh 'which bash || true'
sh 'which git || true'
}
}
또한 Docker/권한 이슈가 섞이면 “변수 문제”로 오인하기 쉽습니다. 러너/에이전트 권한 문제 가능성이 있다면 GitLab CI Runner에서 Docker 권한 오류 해결 가이드 같은 체크리스트식 접근을 Jenkins 에이전트에도 적용해보는 것이 좋습니다.
실전 처방전: 스코프 꼬임을 막는 규칙 7가지
env.KEY = ...는 최후의 수단으로만 사용합니다. 가능하면 지역 변수와withEnv로 제한합니다.- 동적 값(명령 결과, 조건 분기)은
environment블록이 아니라script에서 계산합니다. - 병렬 실행에서는
env수정 금지를 팀 규칙으로 둡니다. withEnv는 “주입 범위를 명확히” 하기 위해 블록을 짧게 유지합니다. 배포 커맨드 직전/직후 정도로.- stage
environment와 pipelineenvironment를 혼용할 때는, 같은 키를 중복 정의하지 않습니다. 중복이 필요하면 키를 분리합니다(TARGET_ENV와STAGE_TARGET_ENV). when조건은 가능하면params또는 변경되지 않는env값에만 의존하게 하고, 중간에env를 바꾸는 설계를 피합니다.- 로그에
env를 찍을 때는 민감정보를 피하고, 필요한 키만 선택 출력합니다.
추천하는 “안전한 기본 템플릿”
팀에서 Jenkinsfile을 표준화할 때 아래 템플릿을 베이스로 두면 스코프 꼬임이 크게 줄어듭니다.
pipeline {
agent any
parameters {
choice(name: 'DEPLOY_ENV', choices: ['dev', 'staging', 'prod'], description: 'Deploy target')
}
environment {
APP_NAME = 'my-service'
}
stages {
stage('Build') {
steps {
sh 'make build'
}
}
stage('Deploy') {
steps {
script {
// 동적 값은 지역 변수로
def deployEnv = params.DEPLOY_ENV
def imageTag = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
// 필요한 범위에만 환경 주입
withEnv([
"DEPLOY_ENV=${deployEnv}",
"IMAGE_TAG=${imageTag}",
"APP_NAME=${env.APP_NAME}"
]) {
sh 'echo Deploying $APP_NAME to $DEPLOY_ENV with tag $IMAGE_TAG'
sh 'make deploy'
}
}
}
}
}
}
핵심은 “값의 생명주기”를 짧게 만들고, 전역 변경을 줄이는 것입니다.
마무리: 꼬임은 대부분 ‘전역 상태’에서 시작한다
Jenkins Declarative에서 환경변수 스코프가 꼬인 것처럼 보이는 문제의 상당수는 env 를 전역 상태처럼 바꿔버리면서 시작합니다. Declarative environment 는 선언적으로, withEnv 는 범위 제한적으로, 동적 값은 script 의 지역 변수로 들고 가는 방식으로 역할을 분리하면 재현 어려운 변동성 문제가 크게 줄어듭니다.
운영에서 중요한 건 “한 번 고치고 끝”이 아니라, 다시는 같은 유형이 터지지 않도록 팀 규칙과 템플릿으로 굳히는 것입니다. 이 글의 템플릿을 기준으로 Jenkinsfile을 리팩터링하면, 환경변수로 인한 배포 오염과 병렬 레이스를 체계적으로 제거할 수 있습니다.