🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Jenkins 트러블슈팅 가이드 완벽 정리 - 슬라이드 1/13
A

AI Generated

2025. 11. 2. · 12 Views

Jenkins 트러블슈팅 완벽 가이드

Jenkins 운영 중 자주 발생하는 문제들과 효과적인 해결 방법을 소개합니다. 빌드 실패, 플러그인 충돌, 성능 저하 등 실무에서 바로 적용할 수 있는 트러블슈팅 노하우를 담았습니다.


목차

  1. 빌드 실패 디버깅 - 로그 분석과 원인 파악
  2. 플러그인 충돌 해결 - 의존성 문제와 버전 관리
  3. 메모리 부족 에러 - 힙 메모리와 GC 튜닝
  4. 파이프라인 타임아웃 - 무한 대기와 데드락 해결
  5. 디스크 공간 부족 - 워크스페이스와 아티팩트 관리
  6. 권한 및 인증 오류 - 보안 설정과 크레덴셜 관리
  7. 에이전트 연결 실패 - 노드 관리와 네트워크 문제
  8. 파이프라인 문법 오류 - Groovy 스크립트 디버깅
  9. 빌드 큐 정체 - Executor 부족과 리소스 할당
  10. 환경별 설정 관리 - 다중 환경 배포 문제

1. 빌드 실패 디버깅 - 로그 분석과 원인 파악

시작하며

여러분이 Jenkins 파이프라인을 실행했는데 갑자기 빌드가 실패하고, 에러 메시지만 수백 줄이 쏟아지는 상황을 겪어본 적 있나요? 급하게 배포해야 하는데 어디서부터 문제를 찾아야 할지 막막한 그 순간 말이죠.

이런 문제는 실제 개발 현장에서 가장 자주 발생합니다. 의존성 변경, 환경 설정 오류, 네트워크 문제 등 다양한 원인이 복합적으로 작용하면서 빌드 실패를 일으킵니다.

특히 여러 팀이 함께 사용하는 Jenkins 환경에서는 문제의 원인을 찾기가 더욱 어렵습니다. 바로 이럴 때 필요한 것이 체계적인 로그 분석 방법입니다.

Jenkins 콘솔 출력을 효율적으로 읽고, 핵심 에러를 빠르게 찾아내는 기술을 익히면 문제 해결 시간을 10분의 1로 줄일 수 있습니다.

개요

간단히 말해서, Jenkins 빌드 실패 디버깅은 콘솔 로그를 체계적으로 분석하여 근본 원인을 찾아내는 과정입니다. 왜 이 방법이 필요한지 실무 관점에서 설명하자면, 무작정 로그를 처음부터 끝까지 읽는 것은 시간 낭비입니다.

대부분의 에러 정보는 로그의 특정 위치에 집중되어 있고, 진짜 원인은 첫 번째 에러 메시지에 있습니다. 예를 들어, 100개의 테스트가 실패했다고 해도 실제 원인은 단 하나의 의존성 문제일 수 있습니다.

전통적인 방법과 비교해봅시다. 기존에는 로그 전체를 복사해서 텍스트 에디터에 붙여넣고 수동으로 검색했다면, 이제는 Jenkins의 Blue Ocean 인터페이스나 로그 파싱 도구를 활용하여 효율적으로 분석할 수 있습니다.

이 방법의 핵심 특징은 첫째, 에러의 우선순위를 파악하는 것입니다. 둘째, 스택 트레이스의 맨 위(root cause)를 먼저 확인하는 것입니다.

셋째, 타임스탬프를 활용하여 문제가 발생한 정확한 시점을 특정하는 것입니다. 이러한 특징들이 중요한 이유는 빠른 문제 해결이 곧 배포 지연 최소화로 이어지기 때문입니다.

코드 예제

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                script {
                    try {
                        // 주석: 빌드 실행 - 여기서 에러가 발생할 수 있음
                        sh 'mvn clean install'
                    } catch (Exception e) {
                        // 주석: 에러 정보를 상세하게 로깅
                        echo "Build failed with error: ${e.getMessage()}"
                        echo "Stack trace: ${e.printStackTrace()}"
                        // 주석: 빌드 로그를 별도 파일로 저장
                        sh 'mvn clean install > build.log 2>&1 || true'
                        archiveArtifacts artifacts: 'build.log', allowEmptyArchive: true
                        throw e
                    }
                }
            }
        }
    }
}

설명

이것이 하는 일: 이 파이프라인은 빌드 실패를 감지하고, 상세한 에러 정보를 수집하여 디버깅을 쉽게 만듭니다. 첫 번째로, try-catch 블록이 빌드 실행을 감싸고 있습니다.

이렇게 하는 이유는 Maven 빌드가 실패하더라도 파이프라인이 즉시 중단되지 않고, 에러 정보를 수집할 수 있는 기회를 주기 위함입니다. 일반적으로 빌드가 실패하면 Jenkins는 즉시 실행을 멈추지만, 이 방식을 사용하면 후처리 작업을 할 수 있습니다.

두 번째로, catch 블록 내부에서 여러 가지 디버깅 정보를 출력합니다. 에러 메시지와 스택 트레이스를 명시적으로 로깅하면, Jenkins 콘솔에서 해당 정보를 쉽게 찾을 수 있습니다.

또한 mvn 명령어를 다시 실행하되 출력을 별도 파일로 리다이렉션하여, 나중에 상세 분석이 가능하도록 합니다. 세 번째로, archiveArtifacts를 사용하여 빌드 로그 파일을 Jenkins 아티팩트로 저장합니다.

마지막으로 throw e를 통해 에러를 다시 던져서 빌드를 실패 상태로 표시합니다. 이렇게 하면 빌드는 실패하지만, 디버깅에 필요한 모든 정보는 보존됩니다.

여러분이 이 코드를 사용하면 빌드 실패 시 자동으로 상세 로그가 저장되어, 나중에 언제든지 다운로드하여 분석할 수 있습니다. 또한 팀원들과 로그 파일을 쉽게 공유할 수 있고, CI/CD 파이프라인의 투명성이 높아집니다.

실전 팁

💡 Jenkins Blue Ocean 인터페이스를 사용하면 로그를 스테이지별로 구분하여 볼 수 있어 문제 구간을 빠르게 특정할 수 있습니다

💡 흔한 실수: 로그 전체를 읽으려 하지 마세요. Ctrl+F로 "ERROR", "FAILED", "Exception" 키워드를 검색하여 문제 지점으로 바로 이동하세요

💡 타임스탬프를 활성화하세요 (Manage Jenkins > Configure System > Enable timestamps). 문제가 발생한 정확한 시간을 알면 서버 로그나 모니터링 도구와 대조할 수 있습니다

💡 빌드 로그가 너무 길다면 ansiColor 플러그인을 설치하여 색상으로 에러를 강조하면 가독성이 크게 향상됩니다

💡 반복적인 실패 패턴이 보인다면 Pipeline Syntax Generator를 사용하여 retry 블록을 추가하세요. 일시적인 네트워크 오류는 자동으로 재시도하여 해결할 수 있습니다


2. 플러그인 충돌 해결 - 의존성 문제와 버전 관리

시작하며

여러분이 Jenkins에 새로운 플러그인을 설치했는데, 갑자기 기존에 잘 작동하던 다른 플러그인들이 오류를 내기 시작한 경험 있으신가요? 혹은 Jenkins 업데이트 후 일부 기능이 작동하지 않아 당황한 적이 있을 겁니다.

이런 문제는 Jenkins의 느슨한 플러그인 아키텍처에서 비롯됩니다. 600개 이상의 플러그인이 서로 다른 개발자들에 의해 관리되고, 각각 다른 라이브러리 버전에 의존하기 때문에 충돌이 자주 발생합니다.

특히 Java 의존성 지옥(Dependency Hell)이라 불리는 현상이 Jenkins 환경에서 더욱 심각하게 나타납니다. 바로 이럴 때 필요한 것이 체계적인 플러그인 의존성 관리입니다.

플러그인 간의 의존 관계를 파악하고, 호환되는 버전을 선택하는 방법을 알면 안정적인 Jenkins 환경을 유지할 수 있습니다.

개요

간단히 말해서, 플러그인 충돌은 여러 플러그인이 같은 라이브러리의 서로 다른 버전을 요구할 때 발생하는 문제입니다. 왜 이 기술이 필요한지 실무 관점에서 설명하자면, Jenkins는 단일 JVM에서 모든 플러그인을 로드하기 때문에 버전 충돌이 발생하면 전체 시스템이 불안정해집니다.

예를 들어, Git 플러그인과 GitHub 플러그인이 서로 다른 버전의 Apache HttpClient를 요구한다면, 둘 중 하나는 제대로 작동하지 않게 됩니다. 전통적인 방법과 비교해봅시다.

기존에는 문제가 생기면 플러그인을 무작정 업데이트하거나 재설치했다면, 이제는 Plugin Manager의 의존성 그래프를 분석하고, 호환성 매트릭스를 참고하여 체계적으로 해결할 수 있습니다. 이 방법의 핵심 특징은 첫째, 의존성 트리를 시각화하여 충돌 지점을 찾는 것입니다.

둘째, 최소 필요 버전(minimum required version)과 권장 버전을 구분하는 것입니다. 셋째, 업데이트 전에 항상 백업과 테스트 환경을 준비하는 것입니다.

이러한 특징들이 중요한 이유는 프로덕션 Jenkins의 다운타임을 최소화하고, 롤백이 가능한 안전한 업데이트를 보장하기 때문입니다.

코드 예제

// 주석: Groovy 스크립트로 플러그인 의존성 확인
import jenkins.model.Jenkins

def jenkins = Jenkins.instance
def pm = jenkins.pluginManager

println "=== Plugin Dependency Check ==="

pm.plugins.each { plugin ->
    // 주석: 각 플러그인의 기본 정보 출력
    println "\nPlugin: ${plugin.shortName} (${plugin.version})"

    // 주석: 의존하는 다른 플러그인들을 확인
    plugin.dependencies.each { dep ->
        def depPlugin = pm.getPlugin(dep.shortName)
        def status = depPlugin ? "OK" : "MISSING"
        println "  - Depends on: ${dep.shortName} (${dep.version}) [${status}]"

        // 주석: 버전 불일치 감지
        if (depPlugin && depPlugin.version != dep.version) {
            println "    WARNING: Version mismatch! Installed: ${depPlugin.version}"
        }
    }
}

설명

이것이 하는 일: 이 Groovy 스크립트는 설치된 모든 Jenkins 플러그인의 의존성을 분석하고, 버전 불일치를 자동으로 감지합니다. 첫 번째로, Jenkins 인스턴스와 PluginManager를 가져옵니다.

이렇게 하는 이유는 Jenkins 내부 API를 통해 플러그인 메타데이터에 접근하기 위함입니다. Script Console에서 이 코드를 실행하면 즉시 결과를 볼 수 있습니다.

두 번째로, 모든 플러그인을 순회하면서 각 플러그인의 이름과 버전을 출력합니다. 그 다음 각 플러그인이 의존하는 다른 플러그인들의 목록을 확인합니다.

의존성이 설치되어 있는지(OK), 누락되었는지(MISSING)를 표시하여 문제를 즉시 파악할 수 있게 합니다. 세 번째로, 가장 중요한 부분인 버전 불일치 감지가 실행됩니다.

플러그인이 요구하는 의존성 버전과 실제 설치된 버전을 비교하여, 다르면 경고 메시지를 출력합니다. 마지막으로 이 정보를 바탕으로 어떤 플러그인을 업데이트해야 할지 판단할 수 있습니다.

여러분이 이 스크립트를 사용하면 플러그인 업데이트 전에 미리 충돌 가능성을 확인할 수 있습니다. 수백 개의 플러그인이 설치된 환경에서도 몇 초 만에 전체 의존성 상태를 파악할 수 있고, 문제가 될 만한 플러그인을 사전에 식별할 수 있습니다.

또한 이 결과를 문서화하여 팀과 공유하면, 플러그인 관리 정책을 수립하는 데도 도움이 됩니다.

실전 팁

💡 Plugin Manager에서 "Available" 탭이 아닌 "Updates" 탭을 먼저 확인하세요. 보안 패치가 포함된 업데이트는 최대한 빨리 적용해야 합니다

💡 흔한 실수: 모든 플러그인을 한꺼번에 업데이트하지 마세요. 핵심 플러그인(workflow-aggregator, git, credentials)부터 하나씩 업데이트하고 테스트하세요

💡 Jenkins LTS(Long-Term Support) 버전을 사용하고, 플러그인도 LTS와 호환되는 버전을 선택하면 안정성이 크게 향상됩니다

💡 정기적으로 사용하지 않는 플러그인을 제거하세요. 플러그인이 많을수록 충돌 가능성도 높아지고, Jenkins 시작 시간도 느려집니다

💡 업데이트 전에 $JENKINS_HOME을 백업하고, Configuration as Code 플러그인으로 설정을 코드화하면 문제 발생 시 빠르게 복구할 수 있습니다


3. 메모리 부족 에러 - 힙 메모리와 GC 튜닝

시작하며

여러분이 Jenkins를 사용하다가 갑자기 "java.lang.OutOfMemoryError: Java heap space" 에러를 마주한 적 있나요? 특히 큰 프로젝트를 빌드하거나 많은 파이프라인을 동시에 실행할 때 이런 문제가 발생합니다.

이런 문제는 Jenkins가 기본적으로 할당받는 메모리가 충분하지 않을 때 발생합니다. Java 애플리케이션인 Jenkins는 JVM 힙 메모리 안에서 모든 작업을 처리하는데, 빌드 로그, 워크스페이스 데이터, 플러그인 객체들이 메모리를 계속 소비합니다.

특히 Garbage Collection(GC)이 제때 실행되지 않으면 메모리가 금방 고갈됩니다. 바로 이럴 때 필요한 것이 적절한 JVM 메모리 설정과 GC 튜닝입니다.

Jenkins의 메모리 사용 패턴을 이해하고, 환경에 맞는 최적의 설정을 찾는 방법을 배워봅시다.

개요

간단히 말해서, Jenkins 메모리 튜닝은 JVM 힙 크기를 적절히 설정하고, Garbage Collector를 최적화하여 안정적인 운영 환경을 만드는 과정입니다. 왜 이 작업이 필요한지 실무 관점에서 설명하자면, 메모리 부족으로 인한 Jenkins 다운타임은 전체 개발팀의 생산성에 직접적인 영향을 미칩니다.

빌드가 실패하면 배포가 지연되고, 개발자들은 작업을 멈추고 기다려야 합니다. 예를 들어, 100명의 개발자가 사용하는 Jenkins가 1시간 다운되면, 그것은 곧 100시간의 생산성 손실을 의미합니다.

전통적인 방법과 비교해봅시다. 기존에는 메모리 문제가 발생하면 무작정 서버를 재시작하고 힙 크기를 2배로 늘렸다면, 이제는 메모리 사용 패턴을 분석하고, 적절한 GC 알고리즘을 선택하며, 메모리 누수를 사전에 감지할 수 있습니다.

이 방법의 핵심 특징은 첫째, 최소 힙(-Xms)과 최대 힙(-Xmx)을 같은 크기로 설정하여 동적 할당 오버헤드를 제거하는 것입니다. 둘째, G1GC 같은 현대적인 Garbage Collector를 사용하여 중단 시간을 최소화하는 것입니다.

셋째, 힙 덤프를 자동으로 생성하도록 설정하여 문제 발생 시 근본 원인을 분석할 수 있게 하는 것입니다. 이러한 특징들이 중요한 이유는 예측 가능하고 안정적인 Jenkins 성능을 보장하기 때문입니다.

코드 예제

#!/bin/bash
# 주석: Jenkins 시작 스크립트에 추가할 JVM 옵션

# 주석: 힙 메모리 설정 - 최소/최대를 동일하게 (4GB 예시)
JAVA_OPTS="-Xms4g -Xmx4g"

# 주석: G1 Garbage Collector 사용 (Java 8 이상 권장)
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC"

# 주석: GC 로그 활성화 (Java 11+ 문법)
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:file=/var/log/jenkins/gc.log:time,uptime:filecount=5,filesize=10M"

# 주석: 메모리 부족 시 힙 덤프 자동 생성
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/var/log/jenkins/heapdump.hprof"

# 주석: 큰 페이지 메모리 사용으로 성능 향상
JAVA_OPTS="$JAVA_OPTS -XX:+UseLargePages"

# 주석: Jenkins 시작
export JAVA_OPTS
java $JAVA_OPTS -jar jenkins.war

설명

이것이 하는 일: 이 스크립트는 Jenkins를 시작할 때 JVM에 최적화된 메모리 옵션을 전달하여, 메모리 관련 문제를 사전에 예방합니다. 첫 번째로, -Xms와 -Xmx 옵션으로 힙 메모리의 최소/최대 크기를 설정합니다.

이렇게 하는 이유는 둘을 같은 값으로 설정하면 JVM이 런타임 중에 힙 크기를 동적으로 조정할 필요가 없어져서, 메모리 할당/해제 오버헤드가 사라지기 때문입니다. 4GB는 중소규모 Jenkins 환경에 적합하며, 사용자 수와 빌드 빈도에 따라 조정해야 합니다.

두 번째로, G1GC(Garbage First Garbage Collector)를 명시적으로 활성화합니다. G1GC는 큰 힙 메모리에서도 예측 가능한 중단 시간을 제공하며, Jenkins처럼 장시간 실행되는 애플리케이션에 최적화되어 있습니다.

또한 GC 로그를 파일로 기록하도록 설정하여, 나중에 성능 분석 도구(GCViewer, GCeasy)로 분석할 수 있게 합니다. 세 번째로, 가장 중요한 안전장치인 HeapDumpOnOutOfMemoryError를 설정합니다.

이 옵션은 메모리 부족 에러가 발생하는 순간의 메모리 스냅샷을 파일로 저장합니다. 마지막으로 UseLargePages 옵션으로 운영체제의 huge page 기능을 활용하여 메모리 액세스 성능을 향상시킵니다.

여러분이 이 설정을 사용하면 Jenkins가 더 이상 갑작스럽게 메모리 부족으로 죽지 않습니다. GC 로그를 분석하여 메모리 사용 추세를 파악할 수 있고, 만약 문제가 발생하더라도 힙 덤프 파일로 메모리 누수의 정확한 원인을 찾아낼 수 있습니다.

또한 GC 중단 시간이 줄어들어 빌드 성능도 향상됩니다.

실전 팁

💡 Jenkins 모니터링 플러그인(Monitoring Plugin)을 설치하면 실시간으로 메모리 사용량과 GC 활동을 웹 UI에서 확인할 수 있습니다

💡 흔한 실수: 서버의 전체 RAM을 모두 힙에 할당하지 마세요. OS와 다른 프로세스를 위해 최소 25%는 남겨두어야 합니다

💡 힙 덤프 파일은 힙 크기만큼 디스크 공간을 차지합니다. 4GB 힙이면 4GB 파일이 생성되므로 충분한 디스크 여유 공간을 확보하세요

💡 Eclipse MAT(Memory Analyzer Tool)를 사용하면 힙 덤프를 분석하여 메모리를 가장 많이 사용하는 객체와 메모리 누수를 쉽게 찾을 수 있습니다

💡 컨테이너 환경(Docker/Kubernetes)에서는 -XX:+UseContainerSupport 옵션을 추가하여 JVM이 컨테이너의 메모리 제한을 인식하도록 하세요


4. 파이프라인 타임아웃 - 무한 대기와 데드락 해결

시작하며

여러분이 Jenkins 파이프라인을 실행했는데, 특정 단계에서 멈춰서 몇 시간째 진행되지 않는 상황을 경험해본 적 있나요? 빌드 큐에서도 "Running"으로 표시되는데 실제로는 아무 작업도 하지 않는 좀비 빌드 말이죠.

이런 문제는 외부 시스템의 응답 지연, 네트워크 타임아웃, 잘못된 입력 대기 등 다양한 원인으로 발생합니다. 특히 파이프라인에 타임아웃 설정이 없으면 이런 빌드들이 Jenkins 리소스를 계속 점유하면서 다른 빌드를 방해합니다.

심한 경우 모든 executor가 멈춘 빌드로 가득 차서 새로운 빌드를 전혀 실행할 수 없게 됩니다. 바로 이럴 때 필요한 것이 적절한 타임아웃 설정과 데드락 감지 메커니즘입니다.

각 단계에 합리적인 제한 시간을 설정하고, 문제가 발생하면 자동으로 복구하는 방법을 배워봅시다.

개요

간단히 말해서, 파이프라인 타임아웃은 각 작업이 일정 시간 내에 완료되지 않으면 자동으로 중단시켜서 리소스 낭비를 방지하는 안전장치입니다. 왜 이 기능이 필요한지 실무 관점에서 설명하자면, Jenkins의 제한된 executor는 귀중한 리소스입니다.

10개의 executor가 있는데 5개가 멈춘 빌드로 점유되어 있다면, 실제 사용 가능한 리소스는 절반으로 줄어듭니다. 예를 들어, 외부 API 호출이 네트워크 문제로 응답하지 않는 경우, 타임아웃이 없으면 그 빌드는 영원히 대기 상태로 남습니다.

전통적인 방법과 비교해봅시다. 기존에는 멈춘 빌드를 발견하면 수동으로 중단 버튼을 클릭했다면, 이제는 timeout 블록을 사용하여 자동으로 처리할 수 있습니다.

또한 activity와 absolute 타임아웃을 구분하여 더 정교한 제어가 가능합니다. 이 방법의 핵심 특징은 첫째, 단계별로 다른 타임아웃을 설정할 수 있다는 것입니다.

빌드 단계는 10분, 테스트 단계는 30분처럼 각 작업의 특성에 맞게 조정할 수 있습니다. 둘째, activity 타임아웃을 사용하면 출력이 없는 시간을 기준으로 판단하여 실제로 멈춘 경우만 중단시킵니다.

셋째, 타임아웃 발생 시 정리 작업을 수행할 수 있어 리소스를 안전하게 해제할 수 있습니다. 이러한 특징들이 중요한 이유는 Jenkins의 가용성을 극대화하고, 인적 개입 없이도 자동으로 복구할 수 있기 때문입니다.

코드 예제

pipeline {
    agent any
    options {
        // 주석: 전체 파이프라인에 대한 기본 타임아웃 (1시간)
        timeout(time: 1, unit: 'HOURS')
    }
    stages {
        stage('Build') {
            steps {
                // 주석: 빌드 단계는 15분 제한
                timeout(time: 15, unit: 'MINUTES') {
                    sh 'mvn clean package'
                }
            }
        }
        stage('Test') {
            steps {
                // 주석: activity 타임아웃 - 5분간 출력이 없으면 중단
                timeout(activity: 5, unit: 'MINUTES') {
                    sh 'mvn test'
                }
            }
        }
        stage('Deploy') {
            steps {
                script {
                    // 주석: 타임아웃과 재시도를 함께 사용
                    retry(3) {
                        timeout(time: 10, unit: 'MINUTES') {
                            sh 'kubectl apply -f deployment.yaml'
                            sh 'kubectl rollout status deployment/myapp'
                        }
                    }
                }
            }
        }
    }
    post {
        aborted {
            // 주석: 타임아웃으로 중단된 경우 알림
            echo 'Pipeline was aborted due to timeout'
            // 주석: 정리 작업 수행
            sh 'docker system prune -f'
        }
    }
}

설명

이것이 하는 일: 이 파이프라인은 여러 레벨의 타임아웃을 설정하여, 각 작업이 예상 시간 내에 완료되지 않으면 자동으로 중단하고 리소스를 회수합니다. 첫 번째로, options 블록에서 전체 파이프라인에 대한 안전장치로 1시간 타임아웃을 설정합니다.

이렇게 하는 이유는 어떤 단계에서 예상치 못한 문제가 발생하더라도 최대 1시간 이상은 리소스를 점유하지 않도록 보장하기 위함입니다. 이는 최후의 방어선 역할을 합니다.

두 번째로, Build 단계에서는 절대 시간 기준 타임아웃을 사용합니다. Maven 빌드는 보통 5-10분이면 끝나므로 15분은 충분히 여유 있는 설정입니다.

Test 단계에서는 activity 타임아웃을 사용하는데, 이는 더 영리한 방식입니다. 테스트 전체는 30분이 걸려도 각 테스트는 계속 출력을 생성하므로, 5분간 완전히 조용하다면 그것은 진짜 문제가 있다는 신호입니다.

세 번째로, Deploy 단계에서는 timeout과 retry를 조합합니다. Kubernetes 배포는 네트워크 이슈로 가끔 실패할 수 있으므로, 10분 타임아웃으로 최대 3번까지 재시도합니다.

마지막으로 post 블록의 aborted 섹션에서 타임아웃으로 인한 중단을 감지하고, Docker 정리 명령어를 실행하여 사용하지 않는 이미지를 제거합니다. 여러분이 이 패턴을 사용하면 더 이상 멈춘 빌드 때문에 Jenkins가 마비되는 일이 없습니다.

각 단계가 예측 가능한 시간 내에 완료되고, 문제가 발생하면 자동으로 중단되어 다음 빌드를 위한 리소스를 확보합니다. 또한 activity 타임아웃 덕분에 긴 작업도 안전하게 실행할 수 있으면서, 실제 데드락은 빠르게 감지됩니다.

실전 팁

💡 타임아웃 값은 과거 빌드 이력을 분석하여 설정하세요. Blue Ocean의 Pipeline Runs 페이지에서 각 단계의 평균 소요 시간을 확인할 수 있습니다

💡 흔한 실수: 너무 짧은 타임아웃을 설정하면 정상적인 빌드도 실패합니다. 평균 소요 시간의 2-3배로 설정하는 것이 안전합니다

💡 activity 타임아웃을 사용할 때는 스크립트가 정기적으로 출력을 생성하도록 하세요. 긴 작업 중간에 echo 명령어로 진행 상황을 출력하면 타임아웃을 방지할 수 있습니다

💡 Lockable Resources 플러그인과 함께 사용하면 공유 리소스에 대한 타임아웃도 설정할 수 있어, 한 빌드가 리소스를 독점하는 것을 방지합니다

💡 타임아웃으로 자주 실패하는 단계가 있다면 근본 원인을 해결하세요. 외부 API 호출이 느리다면 캐싱을 추가하거나, 테스트가 느리다면 병렬 실행을 고려하세요


5. 디스크 공간 부족 - 워크스페이스와 아티팩트 관리

시작하며

여러분이 Jenkins 빌드를 실행하려는데 "No space left on device" 에러가 발생한 경험 있으신가요? 분명 큰 디스크를 사용하고 있는데도 어느새 꽉 차버린 상황 말이죠.

이런 문제는 Jenkins의 특성상 필연적으로 발생합니다. 각 빌드마다 소스 코드를 체크아웃하고, 의존성을 다운로드하며, 빌드 산출물을 생성합니다.

특히 Docker 이미지를 빌드하는 경우 수 GB씩 디스크를 소비합니다. 오래된 빌드 아티팩트와 로그 파일들이 계속 쌓이면 아무리 큰 디스크도 금방 가득 찹니다.

바로 이럴 때 필요한 것이 체계적인 디스크 공간 관리 전략입니다. 오래된 빌드를 자동으로 정리하고, 워크스페이스를 효율적으로 관리하며, 불필요한 파일을 주기적으로 제거하는 방법을 배워봅시다.

개요

간단히 말해서, Jenkins 디스크 관리는 빌드 히스토리, 워크스페이스, 아티팩트를 자동으로 정리하여 디스크 공간을 확보하는 전략입니다. 왜 이 작업이 필요한지 실무 관점에서 설명하자면, 디스크가 가득 차면 Jenkins 전체가 멈춥니다.

새로운 빌드를 시작할 수 없을 뿐만 아니라, 실행 중인 빌드도 실패하고, 심지어 Jenkins 자체가 시작되지 않을 수 있습니다. 예를 들어, 100GB 디스크를 사용하는데 매일 5GB씩 빌드 데이터가 쌓인다면, 20일 만에 디스크가 가득 찹니다.

전통적인 방법과 비교해봅시다. 기존에는 디스크가 가득 차면 수동으로 SSH 접속하여 오래된 빌드 폴더를 찾아 삭제했다면, 이제는 Discard Old Builds 정책을 설정하고, Workspace Cleanup 플러그인을 사용하여 자동화할 수 있습니다.

이 방법의 핵심 특징은 첫째, 빌드 히스토리를 날짜와 개수 기준으로 자동 삭제하는 것입니다. 둘째, 빌드 시작 전후로 워크스페이스를 정리하여 이전 빌드의 잔여 파일이 영향을 주지 않도록 하는 것입니다.

셋째, Docker 이미지나 Maven 캐시처럼 대용량 파일을 주기적으로 정리하는 것입니다. 이러한 특징들이 중요한 이유는 Jenkins를 장기간 안정적으로 운영하기 위해서는 지속 가능한 디스크 관리가 필수이기 때문입니다.

코드 예제

pipeline {
    agent any
    options {
        // 주석: 빌드 히스토리 보관 정책 - 최근 30일 또는 최대 50개
        buildDiscarder(logRotator(
            numToKeepStr: '50',
            daysToKeepStr: '30',
            artifactNumToKeepStr: '10',
            artifactDaysToKeepStr: '7'
        ))
        // 주석: 빌드 시작 전 워크스페이스 정리
        skipDefaultCheckout(true)
    }
    stages {
        stage('Prepare') {
            steps {
                // 주석: 명시적으로 워크스페이스 정리
                cleanWs()
                // 주석: 소스 코드 체크아웃
                checkout scm
            }
        }
        stage('Build') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('Archive') {
            steps {
                // 주석: 필요한 아티팩트만 선택적으로 보관
                archiveArtifacts artifacts: 'target/*.jar',
                                 fingerprint: true,
                                 onlyIfSuccessful: true
            }
        }
    }
    post {
        always {
            // 주석: 빌드 완료 후 대용량 파일 정리
            sh '''
                # Maven 로컬 저장소에서 7일 이상 된 파일 삭제
                find ~/.m2/repository -type f -mtime +7 -delete
                # Docker 미사용 이미지 정리
                docker image prune -a -f --filter "until=24h"
            '''
        }
    }
}

설명

이것이 하는 일: 이 파이프라인은 여러 단계에서 디스크 공간을 관리하여, 불필요한 파일이 쌓이지 않도록 합니다. 첫 번째로, buildDiscarder 옵션으로 빌드 히스토리 보관 정책을 설정합니다.

이렇게 하는 이유는 Jenkins가 각 빌드의 로그와 메타데이터를 영구적으로 보관하려고 하기 때문입니다. numToKeepStr은 최대 50개의 빌드를 유지하고, daysToKeepStr은 30일 이후의 빌드를 삭제합니다.

더 중요한 것은 artifactNumToKeepStr과 artifactDaysToKeepStr로, 용량이 큰 빌드 산출물은 더 짧은 기간(7일, 10개)만 보관합니다. 두 번째로, Prepare 단계에서 cleanWs() 함수로 워크스페이스를 완전히 정리합니다.

이전 빌드의 잔여 파일이 새 빌드에 영향을 주는 것을 방지하고, 깨끗한 상태에서 시작합니다. skipDefaultCheckout(true)로 자동 체크아웃을 비활성화하고, 명시적으로 checkout scm을 호출하여 정리 후에만 코드를 가져옵니다.

세 번째로, Archive 단계에서 onlyIfSuccessful: true 옵션을 사용하여 성공한 빌드의 산출물만 보관합니다. 실패한 빌드의 불완전한 파일을 저장하는 것은 디스크 낭비입니다.

마지막으로 post always 블록에서 추가 정리 작업을 수행합니다. Maven 저장소의 오래된 파일과 사용하지 않는 Docker 이미지를 삭제하여 디스크 공간을 회수합니다.

여러분이 이 설정을 사용하면 Jenkins가 자동으로 디스크를 관리하여, 수동 개입 없이도 장기간 안정적으로 운영할 수 있습니다. 오래된 빌드가 자동으로 정리되고, 각 빌드가 깨끗한 환경에서 시작되며, 대용량 파일이 주기적으로 정리됩니다.

또한 디스크 사용량이 예측 가능해져서 용량 계획을 세우기도 쉬워집니다.

실전 팁

💡 Disk Usage 플러그인을 설치하면 각 프로젝트와 워크스페이스가 사용하는 디스크 공간을 시각적으로 확인하고, 가장 많이 차지하는 항목을 찾을 수 있습니다

💡 흔한 실수: cleanWs()를 post always 블록에 넣으면 빌드 실패 시 디버깅이 어려워집니다. 실패한 빌드의 워크스페이스는 일정 기간 보존하는 것이 좋습니다

💡 Docker를 사용한다면 docker system df로 정기적으로 디스크 사용량을 모니터링하고, BuildKit 캐시도 정리해야 합니다 (docker builder prune)

💡 대용량 프로젝트는 Shallow Clone을 사용하세요 (checkout([$class: 'GitSCM', extensions: [[$class: 'CloneOption', depth: 1, shallow: true]]]). Git 히스토리를 모두 가져오지 않아 디스크와 시간을 절약합니다

💡 Jenkins 서버에 디스크 모니터링 알림을 설정하세요 (Manage Jenkins > Configure System > Disk Space Monitoring). 디스크 사용률이 90%를 넘으면 경고를 받아 사전에 대응할 수 있습니다


6. 권한 및 인증 오류 - 보안 설정과 크레덴셜 관리

시작하며

여러분이 Jenkins 파이프라인에서 Git 저장소를 클론하려는데 "Permission denied" 에러가 발생하거나, Docker 레지스트리에 이미지를 푸시하려는데 "authentication required" 에러를 만난 적 있나요? 로컬에서는 잘 되던 작업이 Jenkins에서만 실패하는 당황스러운 상황 말이죠.

이런 문제는 Jenkins가 독립적인 사용자 계정으로 실행되기 때문에 발생합니다. 개발자의 개인 SSH 키나 AWS 자격증명이 Jenkins에는 없습니다.

또한 보안을 위해 비밀번호나 API 토큰을 파이프라인 코드에 직접 작성할 수도 없습니다. 잘못 관리하면 민감한 정보가 Git 히스토리에 남거나 빌드 로그에 노출될 위험이 있습니다.

바로 이럴 때 필요한 것이 Jenkins Credentials 시스템입니다. 안전하게 자격증명을 저장하고, 파이프라인에서 안전하게 사용하는 방법을 배워봅시다.

개요

간단히 말해서, Jenkins Credentials는 비밀번호, API 토큰, SSH 키 같은 민감한 정보를 암호화하여 저장하고, 파이프라인에서 안전하게 참조할 수 있게 해주는 중앙화된 보안 저장소입니다. 왜 이 시스템이 필요한지 실무 관점에서 설명하자면, 보안은 타협할 수 없는 요소입니다.

한 번의 실수로 AWS 키가 노출되면 수백만 원의 피해가 발생할 수 있고, 데이터베이스 비밀번호가 유출되면 고객 정보가 위험에 처합니다. 예를 들어, GitHub 토큰이 빌드 로그에 출력되면 누구나 볼 수 있고, 악의적으로 사용될 수 있습니다.

전통적인 방법과 비교해봅시다. 기존에는 비밀번호를 파이프라인 스크립트에 하드코딩하거나 환경 변수 파일에 평문으로 저장했다면, 이제는 Jenkins Credentials에 암호화하여 저장하고, ID로만 참조하여 실제 값은 노출되지 않도록 할 수 있습니다.

이 시스템의 핵심 특징은 첫째, 여러 종류의 Credential을 지원한다는 것입니다(Username/Password, Secret Text, SSH Key, Certificate 등). 둘째, Credential에 대한 접근 권한을 세밀하게 제어할 수 있습니다.

특정 폴더나 프로젝트에서만 사용 가능하도록 제한할 수 있습니다. 셋째, 파이프라인에서 사용할 때 자동으로 마스킹되어 로그에 노출되지 않습니다.

이러한 특징들이 중요한 이유는 보안 사고를 예방하고, 감사(audit) 추적이 가능하며, 규정 준수 요구사항을 충족시킬 수 있기 때문입니다.

코드 예제

pipeline {
    agent any
    environment {
        // 주석: Docker Hub 자격증명을 환경 변수로 바인딩
        DOCKER_CREDS = credentials('dockerhub-credentials')
        // 주석: DOCKER_CREDS_USR과 DOCKER_CREDS_PSW로 자동 분리됨
    }
    stages {
        stage('Build Image') {
            steps {
                sh 'docker build -t myapp:latest .'
            }
        }
        stage('Push Image') {
            steps {
                script {
                    // 주석: 안전하게 Docker 로그인 (비밀번호는 로그에 마스킹됨)
                    sh 'echo $DOCKER_CREDS_PSW | docker login -u $DOCKER_CREDS_USR --password-stdin'
                    sh 'docker push myapp:latest'
                }
            }
        }
        stage('Deploy to AWS') {
            steps {
                // 주석: withCredentials 블록으로 AWS 키 사용
                withCredentials([
                    string(credentialsId: 'aws-access-key', variable: 'AWS_ACCESS_KEY_ID'),
                    string(credentialsId: 'aws-secret-key', variable: 'AWS_SECRET_ACCESS_KEY')
                ]) {
                    sh 'aws s3 cp build/ s3://my-bucket/ --recursive'
                }
            }
        }
        stage('SSH Deploy') {
            steps {
                // 주석: SSH 키를 사용한 안전한 배포
                sshagent(['production-server-ssh']) {
                    sh '''
                        ssh user@prod-server "cd /app && git pull"
                        ssh user@prod-server "sudo systemctl restart myapp"
                    '''
                }
            }
        }
    }
}

설명

이것이 하는 일: 이 파이프라인은 여러 종류의 자격증명을 안전하게 사용하여 외부 시스템에 인증하고, 민감한 정보가 노출되지 않도록 보호합니다. 첫 번째로, environment 블록에서 credentials() 헬퍼 함수를 사용합니다.

이렇게 하는 이유는 Jenkins가 자동으로 Username/Password 타입의 Credential을 두 개의 환경 변수로 분리해주기 때문입니다. 'dockerhub-credentials'라는 ID를 가진 Credential이 DOCKER_CREDS_USR(사용자명)과 DOCKER_CREDS_PSW(비밀번호)로 자동 매핑되며, 실제 값은 암호화된 상태로 전달됩니다.

두 번째로, Push Image 단계에서 --password-stdin 옵션을 사용하여 Docker 로그인을 수행합니다. 이는 비밀번호를 커맨드라인 인자로 전달하지 않는 안전한 방법입니다.

커맨드라인 인자는 프로세스 목록에 노출될 수 있지만, stdin으로 전달하면 그런 위험이 없습니다. Jenkins는 $DOCKER_CREDS_PSW 값을 자동으로 마스킹하여 빌드 로그에는 '****'로 표시합니다.

세 번째로, withCredentials 블록으로 AWS 자격증명을 사용합니다. 이 방식은 해당 블록 내에서만 환경 변수가 유효하므로, 범위를 최소화하여 보안을 강화합니다.

마지막으로 sshagent 블록으로 SSH 키를 사용하는데, 이는 SSH agent에 키를 임시로 추가하여 ssh 명령어가 자동으로 인증되도록 합니다. 키 파일 자체는 워크스페이스에 저장되지 않아 안전합니다.

여러분이 이 패턴을 사용하면 더 이상 민감한 정보를 코드에 작성할 필요가 없습니다. 모든 자격증명은 Jenkins에 한 번만 등록하고, 파이프라인에서는 ID로만 참조합니다.

팀원이 퇴사하거나 키가 유출되면 Jenkins에서 한 번만 업데이트하면 모든 파이프라인에 즉시 반영됩니다. 또한 빌드 로그를 외부에 공유해도 안전합니다.

실전 팁

💡 Credentials는 최소 권한 원칙으로 관리하세요. 전역(Global) 대신 특정 폴더나 프로젝트 범위로 제한하면 권한이 없는 파이프라인에서는 접근할 수 없습니다

💡 흔한 실수: 빌드 로그에 $CREDENTIAL_VAR를 echo로 출력하지 마세요. Jenkins가 자동으로 마스킹하지만, 완벽하지 않습니다. Base64 인코딩이나 다른 변환을 거치면 노출될 수 있습니다

💡 AWS나 Azure는 IAM Role 기반 인증을 사용하면 자격증명을 저장할 필요가 없습니다. EC2 인스턴스에 IAM Role을 할당하면 SDK가 자동으로 인증합니다

💡 Credentials를 주기적으로 로테이션하세요. 만료 정책을 설정하고, 90일마다 비밀번호와 토큰을 변경하는 것이 보안 모범 사례입니다

💡 HashiCorp Vault 플러그인을 사용하면 Jenkins Credentials 대신 외부 보안 저장소를 사용할 수 있어, 더 강력한 감사와 접근 제어가 가능합니다


7. 에이전트 연결 실패 - 노드 관리와 네트워크 문제

시작하며

여러분이 Jenkins에서 빌드를 시작했는데 "Agent is offline" 메시지가 나타나거나, 빌드 큐에서 계속 대기만 하는 상황을 경험해본 적 있나요? 분산 빌드 환경을 구축했는데 원격 노드가 자꾸 끊기는 문제 말이죠.

이런 문제는 Jenkins가 마스터-에이전트 아키텍처로 작동하기 때문에 발생합니다. 마스터는 스케줄링과 UI를 담당하고, 실제 빌드는 에이전트 노드에서 실행됩니다.

네트워크 지연, 방화벽 설정, SSH 키 문제, 리소스 부족 등 다양한 이유로 에이전트 연결이 실패하거나 불안정해질 수 있습니다. 특히 클라우드 환경에서 동적으로 노드를 추가하는 경우 더욱 복잡합니다.

바로 이럴 때 필요한 것이 체계적인 에이전트 관리와 트러블슈팅 방법입니다. 연결 상태를 모니터링하고, 문제를 빠르게 진단하며, 자동 복구 메커니즘을 구축하는 방법을 배워봅시다.

개요

간단히 말해서, 에이전트 연결 관리는 마스터와 원격 노드 간의 안정적인 통신을 보장하고, 연결 문제를 자동으로 감지하여 복구하는 과정입니다. 왜 이 작업이 필요한지 실무 관점에서 설명하자면, 에이전트가 오프라인이 되면 해당 노드에 할당된 빌드는 실행되지 않습니다.

만약 10개의 빌드가 특정 라벨(예: docker)을 가진 노드를 기다리고 있는데 그 노드가 다운되면, 모든 빌드가 멈춥니다. 예를 들어, iOS 앱 빌드는 Mac 에이전트가 필수인데, 그 에이전트가 오프라인이면 iOS 배포가 완전히 중단됩니다.

전통적인 방법과 비교해봅시다. 기존에는 에이전트가 오프라인이 되면 수동으로 재연결하거나 서버를 재시작했다면, 이제는 자동 재연결 설정, 헬스 체크 스크립트, 그리고 Docker/Kubernetes 기반 동적 에이전트를 활용하여 자동화할 수 있습니다.

이 방법의 핵심 특징은 첫째, JNLP나 SSH 같은 다양한 연결 방식의 장단점을 이해하고 적절히 선택하는 것입니다. 둘째, 에이전트의 리소스(CPU, 메모리, 디스크)를 모니터링하여 과부하를 사전에 감지하는 것입니다.

셋째, 일시적인 네트워크 오류에 대비하여 자동 재연결을 설정하는 것입니다. 이러한 특징들이 중요한 이유는 고가용성(High Availability) 빌드 환경을 구축하고, 인적 개입을 최소화하여 24/7 운영이 가능하기 때문입니다.

코드 예제

// 주석: Groovy 스크립트로 오프라인 에이전트 자동 복구
import jenkins.model.Jenkins
import hudson.node_monitors.*
import hudson.slaves.*

def jenkins = Jenkins.instance

// 주석: 모든 에이전트 노드를 확인
jenkins.nodes.each { node ->
    def computer = node.toComputer()

    // 주석: 오프라인 상태인지 확인
    if (computer.isOffline()) {
        println "Node ${node.name} is OFFLINE"

        // 주석: 오프라인 원인 분석
        def cause = computer.getOfflineCause()
        println "  Cause: ${cause}"

        // 주석: 일시적 오류면 자동 재연결 시도
        if (cause instanceof OfflineCause.ChannelTermination) {
            println "  Attempting to reconnect..."
            computer.connect(true)
        }
    } else {
        // 주석: 온라인 노드의 리소스 상태 확인
        def monitors = computer.getMonitorData()
        def diskSpace = monitors.get(DiskSpaceMonitor.class)
        def responseTime = monitors.get(ResponseTimeMonitor.class)

        if (diskSpace?.size && diskSpace.size < 5 * 1024 * 1024 * 1024) {
            println "WARNING: Node ${node.name} has low disk space: ${diskSpace.size / (1024*1024*1024)} GB"
        }

        if (responseTime?.average && responseTime.average > 5000) {
            println "WARNING: Node ${node.name} has high response time: ${responseTime.average} ms"
        }
    }
}

설명

이것이 하는 일: 이 스크립트는 모든 Jenkins 에이전트의 상태를 점검하고, 오프라인 노드를 자동으로 복구하며, 온라인 노드의 리소스 문제를 조기에 감지합니다. 첫 번째로, jenkins.nodes를 순회하여 모든 등록된 에이전트 노드를 가져옵니다.

이렇게 하는 이유는 Jenkins 환경에 여러 개의 빌드 노드가 있고, 각각 독립적으로 관리되기 때문입니다. node.toComputer()를 호출하여 실제 실행 상태 정보에 접근합니다.

두 번째로, 오프라인 노드를 발견하면 getOfflineCause()로 원인을 분석합니다. 오프라인이 되는 이유는 다양한데, 관리자가 수동으로 오프라인 상태로 변경한 경우, 네트워크 연결이 끊긴 경우, 에이전트 프로세스가 죽은 경우 등이 있습니다.

ChannelTermination은 네트워크 문제로 연결이 끊긴 경우를 의미하므로, 이 경우 computer.connect(true)로 자동 재연결을 시도합니다. 세 번째로, 온라인 상태인 노드에 대해서는 리소스 모니터링을 수행합니다.

DiskSpaceMonitor로 디스크 여유 공간을 확인하여 5GB 미만이면 경고를 출력합니다. ResponseTimeMonitor로 마스터와의 통신 지연을 측정하여 5초 이상이면 네트워크 문제나 과부하를 의심할 수 있습니다.

마지막으로 이러한 정보를 바탕으로 사전에 문제를 인지하고 조치할 수 있습니다. 여러분이 이 스크립트를 주기적으로 실행하면(예: Jenkins 크론 잡으로 10분마다) 에이전트 문제를 빠르게 감지하고 자동으로 복구할 수 있습니다.

수동으로 노드 상태를 확인할 필요 없이, 문제가 발생하면 알림을 받고, 간단한 네트워크 오류는 자동으로 해결됩니다. 또한 리소스 추세를 분석하여 노드 증설 시기를 결정하는 데도 활용할 수 있습니다.

실전 팁

💡 에이전트 설정에서 "Keep this agent online as much as possible" 옵션을 활성화하면 Jenkins가 자동으로 끊긴 연결을 재시도합니다

💡 흔한 실수: SSH 연결 방식에서 known_hosts 문제가 자주 발생합니다. "Host Key Verification Strategy"를 "Non verifying"으로 설정하면 해결되지만, 보안이 중요한 환경에서는 권장하지 않습니다

💡 클라우드 환경에서는 Jenkins Kubernetes Plugin이나 Amazon EC2 Plugin을 사용하여 수요에 따라 에이전트를 자동으로 생성/삭제하면 관리 부담이 크게 줄어듭니다

💡 에이전트의 /tmp 디렉토리가 가득 차면 JNLP 연결이 실패할 수 있습니다. 에이전트 시작 스크립트에 정리 명령어를 추가하세요

💡 각 에이전트에 적절한 라벨(Label)을 부여하세요. docker, linux, windows, maven 같은 라벨로 구분하면 파이프라인에서 agent { label 'docker' }로 적절한 노드를 선택할 수 있습니다


8. 파이프라인 문법 오류 - Groovy 스크립트 디버깅

시작하며

여러분이 Jenkins 파이프라인을 작성했는데 "WorkflowScript: Unexpected input" 같은 문법 오류가 나타난 경험 있으신가요? 혹은 파이프라인이 시작조차 하지 못하고 빨간 에러 메시지만 보여주는 상황 말이죠.

이런 문제는 Jenkins 파이프라인이 Groovy 언어 기반의 DSL(Domain Specific Language)이기 때문에 발생합니다. Declarative Pipeline과 Scripted Pipeline 두 가지 문법이 있고, 각각 사용 가능한 구문이 다릅니다.

괄호 하나, 들여쓰기 하나만 잘못되어도 전체 파이프라인이 실패합니다. 특히 동적으로 변수를 사용하거나 복잡한 로직을 구현할 때 문법 오류가 자주 발생합니다.

바로 이럴 때 필요한 것이 Pipeline Syntax 도구와 체계적인 디버깅 방법입니다. 문법을 검증하고, 오류를 빠르게 찾아내며, 올바른 구문을 생성하는 방법을 배워봅시다.

개요

간단히 말해서, 파이프라인 문법 디버깅은 Groovy 구문 오류를 식별하고, Pipeline Syntax Generator를 활용하여 올바른 코드를 작성하는 과정입니다. 왜 이 기술이 필요한지 실무 관점에서 설명하자면, 문법 오류는 파이프라인 실행을 완전히 차단합니다.

빌드 단계에서 실패하는 것과 달리, 문법 오류는 파이프라인이 시작조차 하지 못하므로 모든 개발이 멈춥니다. 예를 들어, 100줄짜리 Jenkinsfile에서 한 곳의 문법 오류 때문에 전체 파이프라인을 수정하고 다시 커밋해야 합니다.

전통적인 방법과 비교해봅시다. 기존에는 오류 메시지를 읽고 어림짐작으로 수정한 후 커밋-푸시-테스트를 반복했다면, 이제는 Pipeline Syntax Generator로 정확한 구문을 생성하고, Replay 기능으로 Git 커밋 없이 즉시 테스트할 수 있습니다.

이 방법의 핵심 특징은 첫째, Declarative와 Scripted 문법의 차이를 이해하는 것입니다. Declarative는 구조화되어 있지만 제한적이고, Scripted는 유연하지만 복잡합니다.

둘째, Pipeline Syntax 페이지에서 Snippet Generator를 사용하여 복잡한 구문을 자동 생성하는 것입니다. 셋째, Blue Ocean 에디터를 사용하여 시각적으로 파이프라인을 구축하는 것입니다.

이러한 특징들이 중요한 이유는 학습 곡선을 낮추고, 실수를 줄이며, 생산성을 극대화할 수 있기 때문입니다.

코드 예제

// 주석: Declarative Pipeline - 구조화된 문법
pipeline {
    agent any
    parameters {
        // 주석: 파라미터 타입은 string, choice, boolean 등 정확히 명시
        choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'prod'], description: 'Deployment target')
        string(name: 'VERSION', defaultValue: '1.0.0', description: 'Version to deploy')
    }
    stages {
        stage('Dynamic Stage') {
            when {
                // 주석: when 조건은 declarative 방식으로만 가능
                expression { params.ENVIRONMENT == 'prod' }
            }
            steps {
                script {
                    // 주석: script 블록 안에서는 Groovy 코드 자유롭게 사용
                    def branches = [:]
                    for (int i = 0; i < 3; i++) {
                        def index = i  // 주석: 클로저를 위해 로컬 변수 필요
                        branches["Task ${index}"] = {
                            echo "Running task ${index}"
                        }
                    }
                    parallel branches  // 주석: 병렬 실행
                }
            }
        }
    }
}

설명

이것이 하는 일: 이 파이프라인은 Declarative 문법의 핵심 요소들과 자주 발생하는 실수를 피하는 패턴을 보여줍니다. 첫 번째로, parameters 블록에서 파라미터를 정의할 때 정확한 타입을 사용합니다.

이렇게 하는 이유는 choice()나 string() 같은 함수의 파라미터 이름(name, choices, description)을 정확히 맞춰야 문법 오류가 발생하지 않기 때문입니다. 순서가 바뀌거나 필수 파라미터가 빠지면 "No signature of method" 오류가 발생합니다.

두 번째로, when 블록에서 조건을 표현할 때 declarative 방식인 expression을 사용합니다. Declarative Pipeline에서는 if 문을 직접 사용할 수 없고, 반드시 when 블록 안에 expression, branch, environment 같은 지정된 조건자를 사용해야 합니다.

이를 어기면 "expected one of" 오류가 나타납니다. 세 번째로, 복잡한 로직이 필요한 경우 script 블록으로 감쌉니다.

Declarative Pipeline의 steps 안에서 Groovy 변수나 반복문을 사용하려면 반드시 script 블록이 필요합니다. for 루프에서 def index = i로 로컬 변수를 만드는 것은 Groovy 클로저의 특성 때문인데, 이렇게 하지 않으면 모든 병렬 태스크가 같은 i 값을 참조하는 버그가 발생합니다.

마지막으로 branches 맵을 parallel에 전달하여 여러 작업을 동시에 실행합니다. 여러분이 이 패턴을 따르면 가장 흔한 문법 오류들을 피할 수 있습니다.

parameters는 정확한 타입과 이름으로, when은 declarative 조건자로, 복잡한 로직은 script 블록 안에 작성하는 원칙을 지키면 됩니다. 또한 Pipeline Syntax 페이지(Jenkins 홈 > Pipeline Syntax)에서 Snippet Generator를 사용하여 복잡한 구문을 생성하면 실수를 크게 줄일 수 있습니다.

실전 팁

💡 Jenkins Pipeline Linter를 사용하세요. jenkins-cli.jar나 HTTP API(POST /pipeline-model-converter/validate)로 커밋 전에 문법을 검증할 수 있습니다

💡 흔한 실수: Declarative Pipeline에서 steps 밖에 Groovy 코드를 작성하면 안 됩니다. 변수 선언도 script 블록 안에서만 가능합니다

💡 Replay 기능을 적극 활용하세요. 빌드 이력에서 "Replay"를 클릭하면 Jenkinsfile을 수정하고 Git 커밋 없이 즉시 테스트할 수 있습니다

💡 Blue Ocean Pipeline Editor를 사용하면 GUI로 파이프라인을 구축하고 자동으로 Jenkinsfile을 생성할 수 있어, 문법 오류 걱정이 없습니다

💡 복잡한 Groovy 로직은 Shared Library로 분리하세요. vars/ 디렉토리에 함수를 정의하고 Jenkinsfile에서는 간단히 호출하면, 재사용성이 높아지고 디버깅도 쉬워집니다


9. 빌드 큐 정체 - Executor 부족과 리소스 할당

시작하며

여러분이 Jenkins에서 빌드를 실행했는데 "Build is pending - waiting for available executor" 메시지가 계속 표시되며 한참을 기다린 경험 있으신가요? 특히 출근 시간이나 배포 시간대에 수십 개의 빌드가 큐에서 대기하는 상황 말이죠.

이런 문제는 Jenkins의 제한된 executor 리소스 때문에 발생합니다. 각 빌드는 하나의 executor를 점유하는데, 마스터와 에이전트에 설정된 executor 개수보다 많은 빌드가 요청되면 큐에서 대기해야 합니다.

장시간 실행되는 빌드나 멈춘 빌드가 executor를 계속 점유하면 다른 빌드들이 기아 상태(starvation)에 빠집니다. 바로 이럴 때 필요한 것이 효율적인 executor 관리와 빌드 우선순위 설정입니다.

리소스 사용을 최적화하고, 중요한 빌드를 먼저 실행하며, 동적으로 capacity를 조정하는 방법을 배워봅시다.

개요

간단히 말해서, 빌드 큐 관리는 executor 개수를 최적화하고, 빌드 우선순위를 설정하며, 리소스 사용을 모니터링하여 대기 시간을 최소화하는 과정입니다. 왜 이 작업이 필요한지 실무 관점에서 설명하자면, 빌드 대기 시간은 곧 개발자의 대기 시간입니다.

코드를 푸시한 후 빌드 결과를 30분씩 기다려야 한다면 생산성이 크게 떨어집니다. 예를 들어, 핫픽스를 긴급 배포해야 하는데 일반 빌드들 뒤에서 대기해야 한다면 서비스 장애가 지속됩니다.

전통적인 방법과 비교해봅시다. 기존에는 executor 개수를 무작정 늘리거나 서버를 추가했다면, 이제는 빌드를 병렬화하고, 우선순위 큐를 사용하며, 클라우드 기반 동적 에이전트로 필요할 때만 리소스를 확보할 수 있습니다.

이 방법의 핵심 특징은 첫째, 마스터의 executor는 0으로 설정하고 모든 빌드를 에이전트에서 실행하는 것입니다. 마스터는 스케줄링에만 집중해야 합니다.

둘째, Priority Sorter 플러그인으로 긴급 빌드를 우선 처리하는 것입니다. 셋째, throttle을 사용하여 동시 실행 빌드 수를 제한하고 리소스를 보호하는 것입니다.

이러한 특징들이 중요한 이유는 제한된 리소스로 최대의 처리량(throughput)을 달성하고, 중요한 작업을 신속하게 처리할 수 있기 때문입니다.

코드 예제

pipeline {
    // 주석: 라벨로 특정 에이전트 타입 지정
    agent { label 'linux && docker' }

    options {
        // 주석: 동시 실행 방지 - 같은 프로젝트는 한 번에 하나만
        disableConcurrentBuilds()
        // 주석: 빌드 우선순위 설정 (Priority Sorter 플러그인 필요)
        // priority(10)  // 1-100, 숫자가 높을수록 우선
    }

    stages {
        stage('Parallel Builds') {
            // 주석: 병렬 실행으로 시간 단축
            parallel {
                stage('Build Frontend') {
                    agent { label 'nodejs' }
                    steps {
                        sh 'npm run build'
                    }
                }
                stage('Build Backend') {
                    agent { label 'java' }
                    steps {
                        sh 'mvn package'
                    }
                }
            }
        }
        stage('Heavy Task') {
            options {
                // 주석: 이 작업은 최대 3개까지만 동시 실행
                lock(resource: 'database-access', quantity: 1)
            }
            steps {
                sh 'run-integration-tests.sh'
            }
        }
    }
}

설명

이것이 하는 일: 이 파이프라인은 여러 기법을 사용하여 executor를 효율적으로 활용하고, 불필요한 대기를 줄입니다. 첫 번째로, agent에서 라벨을 사용하여 적절한 에이전트를 선택합니다.

이렇게 하는 이유는 모든 빌드가 같은 에이전트에서 실행될 필요가 없기 때문입니다. Node.js 빌드는 'nodejs' 라벨이 있는 에이전트로, Java 빌드는 'java' 라벨이 있는 에이전트로 분산하면 리소스 사용이 균등해집니다.

&& 연산자로 여러 라벨을 조합할 수도 있습니다. 두 번째로, disableConcurrentBuilds() 옵션으로 같은 프로젝트의 빌드가 동시에 실행되는 것을 방지합니다.

이는 빌드 간 간섭을 막고, 배포 순서를 보장합니다. 예를 들어, 새 커밋이 들어와도 이전 빌드가 끝날 때까지 대기하므로 최신 코드가 먼저 배포되는 혼란이 없습니다.

세 번째로, parallel 블록으로 프론트엔드와 백엔드를 동시에 빌드합니다. 각각 10분씩 걸린다면 순차 실행은 20분이지만, 병렬 실행은 10분만에 완료됩니다.

각 stage에서 다른 agent를 지정하여 서로 다른 노드에서 실행되도록 합니다. 마지막으로 lock 옵션으로 공유 리소스(예: 데이터베이스)에 대한 동시 접근을 제한합니다.

resource 이름이 같은 빌드들은 quantity 만큼만 동시 실행되고 나머지는 대기합니다. 여러분이 이 패턴을 사용하면 빌드 시간이 크게 단축되고, executor 활용률이 높아집니다.

병렬화로 전체 실행 시간이 줄어들고, 적절한 에이전트 할당으로 리소스가 효율적으로 분산됩니다. 또한 lock으로 데이터베이스나 공유 환경을 보호하면서도, 가능한 많은 빌드를 동시에 실행할 수 있습니다.

실전 팁

💡 Manage Jenkins > Configure System에서 마스터의 executor 개수를 0으로 설정하세요. 마스터에서 빌드를 실행하면 UI가 느려지고 안정성이 떨어집니다

💡 흔한 실수: 너무 많은 executor를 설정하면 오히려 성능이 떨어집니다. CPU 코어 개수의 1.5-2배가 적정 수준입니다

💡 Priority Sorter 플러그인으로 프로덕션 배포는 높은 우선순위를, 야간 테스트는 낮은 우선순위를 부여하여 중요한 작업이 먼저 실행되도록 하세요

💡 Kubernetes 플러그인을 사용하면 빌드 요청 시 자동으로 Pod을 생성하여 무제한 executor를 확보할 수 있습니다. 빌드 완료 후 Pod는 자동 삭제됩니다

💡 Build Monitor View 플러그인으로 대시보드를 만들어 큐 상태와 executor 사용률을 실시간으로 모니터링하세요. 병목을 빠르게 발견할 수 있습니다


10. 환경별 설정 관리 - 다중 환경 배포 문제

시작하며

여러분이 개발(dev), 스테이징(staging), 프로덕션(production) 환경으로 배포할 때 각각 다른 설정을 사용해야 하는데, 파이프라인 코드가 복잡해지고 실수가 잦아진 경험 있으신가요? 혹은 프로덕션 설정을 스테이징에 잘못 적용해서 큰 사고를 낼 뻔한 적 있으실 겁니다.

이런 문제는 환경마다 API 엔드포인트, 데이터베이스 연결 정보, 리소스 스케일 등이 다르기 때문에 발생합니다. 같은 애플리케이션이지만 환경에 따라 설정이 달라야 하고, 이를 잘못 관리하면 보안 사고나 서비스 장애로 이어집니다.

특히 여러 팀원이 함께 관리하는 환경에서는 누가 어떤 설정을 변경했는지 추적하기도 어렵습니다. 바로 이럴 때 필요한 것이 체계적인 환경 설정 관리 전략입니다.

Configuration as Code로 설정을 버전 관리하고, 환경별 파라미터를 명확히 분리하며, 실수를 방지하는 검증 로직을 구축하는 방법을 배워봅시다.

개요

간단히 말해서, 환경별 설정 관리는 각 배포 환경에 맞는 설정 값을 안전하게 적용하고, 환경 간 혼선을 방지하는 체계적인 구성 관리 방법입니다. 왜 이 기술이 필요한지 실무 관점에서 설명하자면, 잘못된 환경 설정은 치명적인 결과를 초래합니다.

개발용 데이터베이스 설정을 프로덕션에 적용하면 실제 고객 데이터가 손상될 수 있고, 반대로 프로덕션 설정을 개발 환경에 사용하면 실제 서비스에 영향을 줄 수 있습니다. 예를 들어, 프로덕션 API 키를 개발 환경에서 사용하다가 실수로 GitHub에 커밋하면 보안 사고가 발생합니다.

전통적인 방법과 비교해봅시다. 기존에는 각 환경마다 별도의 Jenkinsfile을 유지하거나, if-else 문으로 환경을 구분했다면, 이제는 환경별 설정 파일을 분리하고, Jenkins의 Folder 단위로 credentials를 관리하며, 파라미터로 명시적으로 환경을 선택하도록 할 수 있습니다.

이 방법의 핵심 특징은 첫째, 환경 이름을 파라미터로 받아 명시적으로 선택하게 하는 것입니다. 둘째, 각 환경별 설정을 별도 파일(예: config/dev.yaml, config/prod.yaml)로 관리하고 Git으로 버전 관리하는 것입니다.

셋째, 프로덕션 배포 시 추가 확인 단계(manual approval)를 두어 실수를 방지하는 것입니다. 이러한 특징들이 중요한 이유는 환경 간 혼선을 원천적으로 차단하고, 설정 변경 이력을 추적할 수 있으며, 재현 가능한 배포를 보장할 수 있기 때문입니다.

코드 예제

pipeline {
    agent any
    parameters {
        // 주석: 배포 환경을 명시적으로 선택
        choice(name: 'DEPLOY_ENV', choices: ['dev', 'staging', 'production'],
               description: 'Target deployment environment')
    }
    stages {
        stage('Load Config') {
            steps {
                script {
                    // 주석: 환경별 설정 파일 로드
                    def configFile = "config/${params.DEPLOY_ENV}.yaml"
                    if (!fileExists(configFile)) {
                        error "Configuration file not found: ${configFile}"
                    }

                    // 주석: YAML 파싱하여 설정 로드
                    def config = readYaml file: configFile
                    env.API_URL = config.api.url
                    env.DB_HOST = config.database.host
                    env.REPLICAS = config.kubernetes.replicas

                    echo "Loaded configuration for: ${params.DEPLOY_ENV}"
                    echo "API URL: ${env.API_URL}"
                }
            }
        }
        stage('Approval for Production') {
            when {
                expression { params.DEPLOY_ENV == 'production' }
            }
            steps {
                // 주석: 프로덕션 배포는 수동 승인 필요
                input message: 'Deploy to PRODUCTION?',
                      ok: 'Yes, deploy to production',
                      submitter: 'admin,release-manager'
            }
        }
        stage('Deploy') {
            steps {
                script {
                    // 주석: 환경별 credentials 사용
                    withCredentials([
                        string(credentialsId: "api-key-${params.DEPLOY_ENV}",
                               variable: 'API_KEY')
                    ]) {
                        sh """
                            kubectl config use-context ${params.DEPLOY_ENV}
                            kubectl set image deployment/myapp \
                                myapp=myapp:${env.BUILD_NUMBER} \
                                --namespace=${params.DEPLOY_ENV}
                            kubectl scale deployment/myapp \
                                --replicas=${env.REPLICAS} \
                                --namespace=${params.DEPLOY_ENV}
                        """
                    }
                }
            }
        }
    }
}

설명

이것이 하는 일: 이 파이프라인은 각 환경에 맞는 설정을 안전하게 적용하고, 프로덕션 배포 시 추가 보호 장치를 제공합니다. 첫 번째로, parameters에서 DEPLOY_ENV를 choice 타입으로 정의하여 개발자가 드롭다운에서 명시적으로 환경을 선택하게 합니다.

이렇게 하는 이유는 환경을 실수로 잘못 입력하는 것을 방지하기 위함입니다. 텍스트 입력으로 'prod'를 'pord'로 오타 내는 것보다, 목록에서 선택하는 것이 훨씬 안전합니다.

두 번째로, Load Config 단계에서 환경 이름을 기반으로 설정 파일 경로를 생성하고, 해당 파일이 존재하는지 먼저 확인합니다. 파일이 없으면 즉시 에러로 빌드를 중단하여 잘못된 배포를 방지합니다.

readYaml로 YAML 파일을 파싱하여 필요한 설정값들을 환경 변수로 설정합니다. 이렇게 하면 config/dev.yaml에는 개발 설정이, config/production.yaml에는 프로덕션 설정이 명확히 분리되어 관리됩니다.

세 번째로, Approval 단계에서 when 조건으로 프로덕션 환경일 때만 수동 승인을 요구합니다. input 단계는 빌드를 일시 중지하고, 지정된 사용자(submitter)만 승인할 수 있도록 제한합니다.

마지막으로 Deploy 단계에서 환경 이름을 포함한 credentials ID(예: api-key-production)로 올바른 자격증명을 가져오고, Kubernetes 배포 시 namespace와 replicas를 환경별 설정값으로 적용합니다. 여러분이 이 패턴을 사용하면 환경별 설정이 명확히 분리되고, Git으로 버전 관리되어 언제든지 이전 버전으로 롤백할 수 있습니다.

프로덕션 배포는 승인 과정을 거치므로 실수로 배포하는 일이 없고, 각 환경의 credentials가 자동으로 선택되어 수동으로 바꿀 필요가 없습니다. 또한 설정 변경 이력이 Git 커밋으로 남아 누가 언제 무엇을 변경했는지 추적할 수 있습니다.

실전 팁

💡 Configuration as Code(JCasC) 플러그인으로 Jenkins 자체 설정도 YAML로 관리하면, Jenkins 서버를 재구축할 때 설정을 자동으로 복원할 수 있습니다

💡 흔한 실수: 환경 설정 파일에 비밀번호를 평문으로 저장하지 마세요. 민감한 값은 Jenkins Credentials에 저장하고, 설정 파일에는 credentials ID만 참조하세요

💡 Helm이나 Kustomize를 사용하면 Kubernetes 매니페스트를 환경별로 오버라이드할 수 있어, 더 복잡한 설정 관리가 가능합니다

💡 환경별로 별도의 Jenkins Folder를 만들고 각 폴더에 credentials를 할당하면, 개발팀은 dev credentials만, 운영팀은 production credentials만 접근할 수 있습니다

💡 GitOps 패턴을 도입하면 설정 변경도 Git PR로 관리되어, 코드 리뷰를 거쳐 배포되므로 실수가 더욱 줄어듭니다


#Jenkins#Pipeline#Groovy#CI/CD#Debugging#Python

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.