Published on

Jenkins Declarative Pipeline when+matrix 조건 누락 해결

Authors

서론

Jenkins Declarative Pipeline의 matrix는 멀티 플랫폼/멀티 JDK/멀티 환경 조합을 깔끔하게 병렬화해주는 강력한 기능입니다. 하지만 실무에서 when과 결합하면 “분명 조건을 걸었는데 특정 조합에서 스테이지가 실행된다/반대로 실행되지 않는다” 같은 조건 누락(혹은 무시) 문제를 종종 만납니다.

이 글은 다음 상황을 집중적으로 다룹니다.

  • matrix 내부 stagewhen을 걸었는데, 일부 axis 조합에서 조건이 기대와 다르게 동작
  • when { expression { ... } }에서 env.*/params.*/BRANCH_NAME 등을 참조했는데 평가가 엇갈림
  • matrixexclude/when을 섞었더니 특정 조합만 “새는(leak)” 현상 발생

핵심은 Declarative의 평가 시점과 스코프(특히 matrix axis 환경변수 주입 시점) 를 이해하고, 조건을 “어디에” 두어야 안정적으로 동작하는지 패턴화하는 것입니다.


문제 증상: when이 matrix 축 조합을 제대로 반영하지 못함

가장 흔한 형태는 아래와 같습니다.

  • 기대: axis OS=windows일 때만 특정 스테이지 실행
  • 실제: OS=linux에서도 실행되거나, 반대로 OS=windows에서도 스킵됨

또는 브랜치 기반 조건이 섞이면 더 헷갈립니다.

  • 기대: main 브랜치에서만 deploy 실행
  • 실제: matrix 일부 조합에서만 실행/스킵이 뒤섞임

이런 문제는 대개 “조건식 자체가 틀렸다”기보다, 조건이 평가되는 시점에 axis 값이 아직 준비되지 않았거나, 혹은 조건이 stage 레벨이 아닌 잘못된 레벨에 위치해서 발생합니다.


원인 1: matrix axis 환경변수는 각 조합 실행 컨텍스트에서 주입됨

matrix의 axis 값은 각 조합(예: OS=linux, JDK=17)이 실행될 때 해당 조합 컨텍스트에 주입됩니다. 즉,

  • matrix 바깥(상위 stage, pipeline 전역)에서 axis 환경변수를 기대하면 안 됨
  • when이 axis 값을 참조하더라도, when이 평가되는 타이밍에 axis 값이 확정되어 있어야 함

특히 다음과 같은 경우가 위험합니다.

  • matrix 바깥 stage에 when을 걸고 axis를 참조
  • when에서 env.OS 같은 값을 보는데 실제로는 아직 전역 env에 없음

원인 2: when의 평가 시점(특히 beforeAgent)과 agent 할당

Declarative의 when은 기본적으로 stage 진입 시 평가되지만, beforeAgent true를 주면 agent 할당 전에 평가됩니다.

  • 장점: 불필요한 agent 점유를 줄임
  • 단점: 조건식에서 필요한 컨텍스트(워크스페이스, 체크아웃 결과, 일부 환경)가 없을 수 있음

matrix 조합에서 agent { label ... }을 쓰는 경우, beforeAgent 설정에 따라 조건 평가가 달라져 “어떤 조합에서는 조건이 먹고 어떤 조합에서는 안 먹는” 것처럼 보일 수 있습니다.


재현 예제: 잘못된 when 위치/스코프

아래는 흔히 보는 실수 패턴입니다. matrix 밖에서 axis를 기준으로 필터링하려고 합니다.

pipeline {
  agent none

  stages {
    stage('Test Matrix') {
      matrix {
        axes {
          axis {
            name 'OS'
            values 'linux', 'windows'
          }
        }
        stages {
          stage('Run tests') {
            steps {
              echo "OS=${env.OS}" // 조합에서는 찍힘
            }
          }
        }
      }
    }

    stage('Windows-only stage (WRONG)') {
      when {
        expression { env.OS == 'windows' } // matrix 축을 전역에서 기대
      }
      steps {
        echo 'Do something for windows'
      }
    }
  }
}

Windows-only stage는 matrix 이후의 전역 stage이므로, env.OS는 의도대로 존재하지 않습니다. 결과는 Jenkins 버전/플러그인/실행 경로에 따라 다르게 보일 수 있으나, 요지는 axis 값은 matrix 조합 컨텍스트에만 유효하다는 점입니다.


해결 1: axis 기반 조건은 matrix 내부 stage의 when으로 이동

가장 안전한 해결책은 “axis 기반 조건은 axis가 존재하는 곳에서 평가”하는 것입니다.

pipeline {
  agent none

  stages {
    stage('Test Matrix') {
      matrix {
        axes {
          axis {
            name 'OS'
            values 'linux', 'windows'
          }
          axis {
            name 'JDK'
            values '17', '21'
          }
        }

        agent { label "builder-${OS}" }

        stages {
          stage('Unit Test') {
            steps {
              echo "Run unit test on OS=${OS}, JDK=${JDK}"
            }
          }

          stage('Windows-only Integration') {
            when {
              expression { OS == 'windows' }
            }
            steps {
              echo "Integration test only for windows: OS=${OS} JDK=${JDK}"
            }
          }
        }
      }
    }
  }
}

포인트:

  • OS, JDKenv.OS 대신 바로 OS, JDK로 참조(Declarative에서 axis는 변수처럼 노출되는 경우가 많음)
  • whenmatrix 내부 stage에 둬서 조합 컨텍스트에서 평가

이 패턴은 “조건 누락”을 가장 확실하게 줄입니다.


해결 2: exclude를 적극 활용해 조합 자체를 제거

조건이 “특정 조합은 아예 실행하면 안 된다”라면 when보다 exclude가 더 명확합니다. exclude는 조합 생성 단계에서 제거되므로, 스테이지 조건 평가 타이밍 문제를 회피할 수 있습니다.

pipeline {
  agent none

  stages {
    stage('Build Matrix') {
      matrix {
        axes {
          axis {
            name 'OS'
            values 'linux', 'windows'
          }
          axis {
            name 'JDK'
            values '17', '21'
          }
        }

        excludes {
          exclude {
            axis {
              name 'OS'
              values 'windows'
            }
            axis {
              name 'JDK'
              values '21'
            }
          }
        }

        stages {
          stage('Build') {
            steps {
              echo "Build OS=${OS} JDK=${JDK}"
            }
          }
        }
      }
    }
  }
}

권장 기준:

  • “조합 자체가 불필요/금지” → exclude
  • “조합은 필요하지만 특정 단계만 스킵” → when

해결 3: beforeAgent 사용 시 조건식 의존성 점검

beforeAgent true는 비용 최적화에 좋지만, 조건식에서 다음을 참조하면 흔들릴 수 있습니다.

  • fileExists(...) (워크스페이스/체크아웃 필요)
  • sh(...) 결과
  • SCM 체크아웃 이후에만 생기는 파일/환경

예를 들어, 특정 파일이 있을 때만 실행하려고 아래처럼 쓰면:

stage('Optional Step') {
  when {
    beforeAgent true
    expression { fileExists('ci/enable_optional') }
  }
  agent { label "builder-${OS}" }
  steps {
    echo 'Run optional step'
  }
}

beforeAgent true라서 워크스페이스가 준비되지 않으면 fileExists가 기대대로 동작하지 않습니다.

해결 방법은 2가지입니다.

  1. beforeAgent를 제거(가장 단순)
when {
  expression { fileExists('ci/enable_optional') }
}
  1. 체크아웃을 보장하거나, 조건을 파일 기반이 아닌 파라미터/브랜치 기반으로 바꾸기

해결 4: 브랜치 조건 + matrix 조건을 AND로 묶을 때는 allOf 사용

브랜치 조건과 axis 조건을 함께 걸 때는 allOf로 명시적으로 묶는 것이 안전합니다.

stage('Deploy (main + linux only)') {
  when {
    allOf {
      branch 'main'
      expression { OS == 'linux' }
    }
  }
  steps {
    echo "Deploy from main on linux only: OS=${OS}"
  }
}

이렇게 하면 조건이 분산되어 “한쪽만 적용된 것처럼 보이는” 상황을 줄일 수 있습니다.


디버깅 팁: 조건 평가에 필요한 값을 강제로 출력하기

조건 누락을 잡을 때는 “조합에서 실제로 어떤 값이 들어왔는지”를 먼저 확인해야 합니다.

matrix 내부에 진단 스테이지를 하나 둡니다.

stage('Debug Context') {
  steps {
    echo "BRANCH_NAME=${env.BRANCH_NAME}"
    echo "CHANGE_ID=${env.CHANGE_ID}"
    echo "OS=${OS} JDK=${JDK}"
    echo "NODE_NAME=${env.NODE_NAME}"
  }
}

그리고 when 조건식에서 참조하는 값이 정말 그 시점에 존재하는지 확인합니다.

  • env.BRANCH_NAME는 Multibranch Pipeline에서만 기대대로 들어옵니다.
  • PR 빌드라면 CHANGE_ID, CHANGE_TARGET 같은 값이 더 중요할 수 있습니다.

운영 관점: 조건 누락이 장애로 번지는 대표 시나리오

조건이 새면 단순히 “불필요한 테스트가 한 번 더 돈다”로 끝나지 않습니다.

  • 특정 조합에서만 배포가 실행되어 잘못된 아티팩트가 프로덕션에 반영
  • 특정 노드 라벨에서만 자격증명/네트워크가 달라 외부 시스템 호출 실패
  • 병렬로 돌아야 할 검증이 누락되어 품질 게이트가 무력화

특히 클라우드 권한/토큰과 결합되면 원인 파악이 더 어려워집니다. 예를 들어 CI 러너에서 AWS 권한이 꼬이면 “어떤 조합에서만 AccessDenied”가 나기도 합니다. 이런 류의 이슈를 다룬 글로는 EKS Pod에서 S3 403 AccessDenied 원인 10가지 같은 체크리스트가 진단에 도움이 됩니다(비록 Jenkins 주제는 아니지만, 권한/환경 분기 문제의 접근법은 유사합니다).


권장 템플릿: matrix + 조건을 안정적으로 설계하는 패턴

실무에서 재사용하기 좋은 형태로 정리하면 아래처럼 가져갈 수 있습니다.

pipeline {
  agent none
  options {
    timestamps()
    ansiColor('xterm')
  }
  parameters {
    booleanParam(name: 'RUN_INTEGRATION', defaultValue: false, description: 'Run integration tests')
  }

  stages {
    stage('CI Matrix') {
      matrix {
        axes {
          axis {
            name 'OS'
            values 'linux', 'windows'
          }
          axis {
            name 'JDK'
            values '17', '21'
          }
        }

        // 조합 자체 제거가 필요하면 excludes를 우선 고려
        excludes {
          exclude {
            axis { name 'OS'; values 'windows' }
            axis { name 'JDK'; values '21' }
          }
        }

        agent { label "builder-${OS}" }

        stages {
          stage('Debug') {
            steps {
              echo "BRANCH=${env.BRANCH_NAME} OS=${OS} JDK=${JDK}"
            }
          }

          stage('Build') {
            steps {
              echo "Build on ${OS}/${JDK}"
            }
          }

          stage('Integration') {
            when {
              allOf {
                expression { params.RUN_INTEGRATION }
                expression { OS == 'linux' }
              }
            }
            steps {
              echo "Integration on ${OS}/${JDK}"
            }
          }

          stage('Deploy') {
            when {
              allOf {
                branch 'main'
                expression { OS == 'linux' && JDK == '21' }
              }
            }
            steps {
              echo "Deploy from main on ${OS}/${JDK}"
            }
          }
        }
      }
    }
  }
}

이 템플릿의 의도:

  • axis 기반 조건은 matrix 내부에서만 평가
  • 조합 제거는 exclude로 처리
  • 브랜치/파라미터/axis 조건은 allOf로 명확히 결합
  • beforeAgent는 정말 필요할 때만 사용(조건식 의존성 검증 후)

체크리스트: when+matrix 조건 누락을 막는 7가지 점검

  1. axis 값을 matrix 밖에서 참조하지 않았는가? (env.OS 같은 전역 접근 금지)
  2. 조건은 matrix 내부 stage에 있는가? (조합 컨텍스트에서 평가)
  3. 조합 제거가 목적이라면 exclude를 썼는가? (when 대신 exclude)
  4. when에 beforeAgent true를 썼다면, 조건식이 워크스페이스/체크아웃에 의존하지 않는가?
  5. 브랜치 조건 + axis 조건을 allOf로 묶었는가?
  6. 디버그 출력으로 BRANCH/OS/JDK가 실제로 무엇인지 확인했는가?
  7. Jenkins/플러그인 버전 차이로 동작이 달라질 수 있음을 감안했는가? (특히 Pipeline Model Definition 관련)

결론

Jenkins Declarative Pipeline에서 whenmatrix를 함께 쓸 때 발생하는 조건 누락 문제는 대부분 조건이 평가되는 시점과 스코프를 잘못 잡아서 생깁니다. 해결의 핵심은 간단합니다.

  • axis 기반 조건은 matrix 내부에서 평가하라
  • 조합 자체를 막아야 하면 exclude를 써라
  • beforeAgent는 비용 최적화 도구일 뿐, 조건식의 의존성이 충족되는지 먼저 확인하라

이 원칙만 지켜도 “왜 어떤 조합만 새지?” 같은 디버깅 시간을 크게 줄일 수 있습니다.