← 글 목록

notes

Jenkins - Pipeline 자동배포(CICD)

Jenkins Pipeline으로 CI/CD 자동배포를 구성하는 단계·스크립트 메모.

  • free style 이랑 달리 pipeline 은 스크립트에 트리거를 직접 써야 webhook 을 받는다
  • webhook 빌드인지 수동 빌드인지 보고 브랜치를 정한다
  • 스테이지는 체크아웃, 의존성 설치, 빌드 세 개다

플러그인 준비

free style 작업은 설정 화면에서 webhook 트리거를 체크하면 끝인데 pipeline 은 플러그인이 따로 있어야 한다. GitHub plugin 은 기본으로 깔려 있고, GitHub Integration Plugin 은 저장소 연결이랑 트리거 쪽을 맡는다.

webhook 트리거

플러그인을 깔고 나서 스크립트에 triggers 블록을 넣어야 Jenkins 가 push 이벤트를 알아듣는다. 저장소 쪽 webhook Payload URL 은 Jenkins 주소 뒤에 /github-webhook/ 을 붙인 거다.

// 이걸 넣어야 webhook 을 인식한다

triggers {
    githubPush()  // push 이벤트
}

브랜치 고르기

스크립트는 SCM 에서 읽지 않고 작업 설정의 Pipeline script 칸에 바로 넣었다. 제일 신경 쓴 부분은 빌드가 왜 시작됐는지 보는 거다. 빌드 원인에 GitHubPushCause 가 있으면 webhook 이고, 아니면 내가 직접 누른 거다.

webhook 이면 현재 커밋이 어느 브랜치에 있는지 git ls-remote 로 찾고, prod/smartq 나 prod/cmng 가 아니면 바로 error 로 끊는다. 수동 빌드는 SELECT_PROJECT 값으로 브랜치를 정한다.

스테이지 구성

브랜치를 체크아웃하고 나면 node_modules 가 없을 때만 pnpm install 을 돌리고, 브랜치에 맞는 빌드 명령을 실행한다. 끝나면 성공이든 실패든 cleanWs 로 작업 공간을 비운다.

전체 파이프라인 스크립트
// Jenkins 파이프라인 정의
// PR merge 이벤트와 수동 빌드를 모두 지원하는 빌드 파이프라인
pipeline {
    // 모든 Jenkins 에이전트에서 실행 가능
    agent any
    
    // 수동 빌드 시 사용할 파라미터 정의
    parameters {
        choice(
            name: 'SELECT_PROJECT',          // 파라미터 이름
            choices: ['smartq', 'cmng'],     // 선택 가능한 프로젝트 목록
            description: '빌드할 프로젝트 선택'  // 파라미터 설명
        )
    }

    // GitHub webhook 트리거 설정
    // PR merge 시 자동으로 빌드를 시작하기 위한 설정
    triggers {
        githubPush()
    }
    
    // 파이프라인 스테이지 정의
    stages {
        // Git 저장소 체크아웃 스테이지
        stage("Repository") {
            steps {
                script {
                    // 현재 빌드가 시작된 원인 확인
                    def cause = currentBuild.getBuildCauses()
                    echo "Build Causes: ${cause}"  // 빌드 원인 로깅
                    
                    // GitHub webhook에 의한 빌드인지 확인
                    def isWebhook = cause.toString().contains('GitHubPushCause')
                    echo "Is Webhook Build: ${isWebhook}"  // webhook 여부 로깅
                    
                    if (isWebhook) {
                        // webhook(PR merge)에 의한 자동 빌드 처리
                        echo "Webhook 빌드 시작"
                        
                        // git 명령어를 사용하여 현재 커밋의 브랜치 정보 가져오기
                        def branch = sh(
                            script: 'git ls-remote --heads origin | grep $(git rev-parse HEAD) | cut -d "/" -f 3-',
                            returnStdout: true
                        ).trim()
                        
                        echo "감지된 브랜치: ${branch}"  // 감지된 브랜치 정보 로깅
                        
                        // prod/smartq 또는 prod/cmng 브랜치인지 확인
                        if (branch.contains('prod/smartq') || branch.contains('prod/cmng')) {
                            // 해당 브랜치 체크아웃
                            git branch: branch,
                                credentialsId: 'cpt4567',  // GitHub 인증 정보
                                url: 'https://github.com/<org>/<repo>.git'
                        } else {
                            // 지원하지 않는 브랜치인 경우 에러 발생
                            error "지원하지 않는 브랜치: ${branch}"
                        }
                    } else {
                        // 수동 빌드 처리
                        // 선택된 프로젝트에 따라 브랜치 결정
                        def branch = params.SELECT_PROJECT == 'smartq' ? 'prod/smartq' : 'prod/cmng'
                        echo "수동 빌드 - 선택된 브랜치: ${branch}"
                        
                        // 선택된 브랜치 체크아웃
                        git branch: branch,
                            credentialsId: 'cpt4567',
                            url: 'https://github.com/<org>/<repo>.git'
                    }
                    
                    // 체크아웃된 파일 목록 확인
                    sh 'ls -la'
                }
            }
        }

        // 프로젝트 의존성 설치 스테이지
        stage('Install Dependencies') {
            steps {
                script {
                    // node_modules 디렉토리 존재 여부 확인
                    def nodeModulesExists = fileExists 'node_modules'
                    if (!nodeModulesExists) {
                        echo "node_modules 없음, 의존성 설치 시작"
                        // pnpm을 사용하여 의존성 설치
                        sh 'pnpm install'
                    } else {
                        echo "node_modules 이미 있음"
                    }
                }
            }
        }
        
        // 프로젝트 빌드 스테이지
        stage('Build Project') {
            steps {
                script {
                    // 현재 체크아웃된 브랜치 확인
                    def currentBranch = sh(
                        script: 'git rev-parse --abbrev-ref HEAD',
                        returnStdout: true
                    ).trim()
                    
                    echo "빌드 대상 브랜치: ${currentBranch}"
                    
                    // 브랜치에 따라 적절한 빌드 명령 실행
                    if (currentBranch.contains('prod/smartq')) {
                        echo "Smart 프로젝트 빌드 시작"
                        sh 'pnpm build:smart'
                    } else if (currentBranch.contains('prod/cmng')) {
                        echo "Cmng 프로젝트 빌드 시작"
                        sh 'pnpm build:cmng'
                    } else {
                        error "지원하지 않는 브랜치: ${currentBranch}"
                    }
                }
            }
        }
    }
    
    // 빌드 완료 후 실행되는 작업들
    post {
        // 빌드 성공 시 실행
        success {
            echo '빌드 성공!'
        }
        // 빌드 실패 시 실행
        failure {
            echo '빌드 실패!'
        }
        // 빌드 결과와 관계없이 항상 실행
        always {
            // 작업 공간 정리 (다음 빌드를 위해)
            cleanWs()
        }
    }
}

커밋 브랜치를 ls-remote 로 찾는 방식은 같은 커밋이 두 브랜치에 동시에 있으면 둘 다 걸린다. 프로젝트별로 브랜치를 따로 운영해서 문제는 없었는데, 구조가 바뀌면 작업을 프로젝트마다 나누는 게 더 깔끔하다.