본문 바로가기
CI. CD/Jenkins

[Jenkins] SonarQube 연동 불량 코드 자동 차단 시스템 Quality Gate

아무리 빠르고 완벽한 CI/CD 배포 파이프라인을 구축했다 한들, 개발자가 푸시한 코드 내부에 심각한 보안 취약점이나 NullPointerException을 유발할 버그가 숨어있다면 무슨 소용일까요? 버그가 듬뿍 담긴 코드를 운영 서버에 가장 빠르고 정확하게 꽂아 넣는 '자동화된 재앙 생성기'로 전락할 뿐입니다. 진정한 데브옵스(DevOps)는 단순히 배포만 해주는 것이 아니라, 품질 기준에 미달하는 쓰레기 코드(Bad Code)가 감지되면 그 즉시 파이프라인의 목을 비틀어 빌드를 강제 중단(Abort)시키는 무자비한 문지기 역할을 해야 합니다.

 

이번 포스트에서는 전 세계 정적 코드 분석 시장의 절대 강자인 SonarQube(소나큐브)를 젠킨스와 연동하여, 완벽한 'Quality Gate(품질 게이트)'를 구축하는 자동화 실무에 대하여 다뤄보겠습니다.

1. 정적 분석과 SonarQube 란?

애플리케이션을 실행(Run)하지 않고, 소스코드 텍스트 자체만 주욱 읽어서 "이 부분 변수 선언해 놓고 한 번도 안 썼네?", "이 암호화 방식은 해킹 뚫릴 확률 100%다", "여기에 if문이 이렇게 중첩되면 나중에 유지보수 절대 못해(코드 스멜)" 등을 찾아내는 기법을 정적 코드 분석(Static Code Analysis)이라고 합니다. 이 분야의 끝판왕 툴이 바로 SonarQube입니다.

SonarQube는 코드를 스캔하여 버그(Bugs), 취약점(Vulnerabilities), 코드 냄새(Code Smells), 중복 코드 비율, 테스트 커버리지(Coverage) 등을 아름다운 대시보드 리포트로 시각화해 줍니다.

 

2. Jenkins 시스템 연동 셋업 가이드

젠킨스가 코드를 빌드하다 말고 SonarQube 서버에 스캔을 요청하려면 두 시스템이 통신해야 합니다.

  1. SonarQube 토큰 발급: SonarQube 서버 웹에 관리자로 접속하여 내 정보 -> Security 탭에서 Generate Token을 눌러 인증 토큰을 발급받습니다.
  2. Jenkins 플러그인 설치: 젠킨스의 [플러그인 관리]에서 SonarQube Scanner for Jenkins를 설치합니다.
  3. 자격 증명 등록: 젠킨스의 [Manage Credentials] 메뉴에서 방금 발급받은 토큰을 Secret text 타입으로 등록하고 ID를 sonar-token으로 명명합니다.
  4. 글로벌 서버 등록: [Jenkins 관리] -> [시스템 설정(Configure System)]으로 들어가 SonarQube servers 항목에 SonarQube 서버 URL과 방금 등록한 sonar-token을 연결해 줍니다. 이때 Name을 my-sonar-server처럼 직관적으로 지어줍니다.

 

3. Quality Gate (품질 게이트): 문지기 소환하기

연동이 끝났다면 파이프라인에 스크립트를 추가할 차례입니다. 실무 파이프라인 구성의 핵심은 단순히 '캔만 하고 넘어가기(X)'가 아니라 '스캔 결과가 품질 통과(Passed)인지 젠킨스가 기다렸다가, 불합격(Failed)이면 배포를 강제 중지하기(O)'입니다. 이 문지기 역할을 하는 스텝이 바로 waitForQualityGate 입니다.

pipeline {
    agent any
    stages {
        stage('Build & Test') {
            steps { sh './gradlew clean build test' }
        }
        
        // 1. 코드 분석 및 결과 전송 스테이지
        stage('SonarQube Static Analysis') {
            steps {
                // 시스템 설정에서 등록한 이름 'my-sonar-server'를 불러옵니다.
                withSonarQubeEnv('my-sonar-server') {
                    // 빌드된 코드와 테스트 결과를 SonarQube 서버로 쏘아 보냅니다.
                    sh './gradlew sonar'
                }
            }
        }
        
        // 2. 심사 결과 대기 및 차단(Quality Gate) 스테이지
        stage('Quality Gate Check') {
            steps {
                // 무한 대기를 막기 위해 timeout을 겁니다. (분석에 5분이 넘어가면 타임아웃)
                timeout(time: 5, unit: 'MINUTES') {
                    // SonarQube 서버가 스캔을 마치고 "합격/불합격"을 젠킨스에게 알려줄 때까지 파이프라인을 멈추고 기다립니다.
                    // abortPipeline: true 옵션은 불합격(Failed) 판정 시 파이프라인을 붉은색(Failure)으로 즉시 폭파시키는 실무 핵심 옵션입니다.
                    waitForQualityGate abortPipeline: true
                }
            }
        }
        
        stage('Deploy to Production') {
            // Quality Gate를 무사히 통과(Passed)한 청정 코드만이 이곳에 도달할 수 있습니다.
            steps { echo ' 무결점 코드 배포 시작!' }
        }
    }
}

 

4. 중급 트러블슈팅: Webhook 설정의 중요성

위 코드를 그대로 돌려보면 waitForQualityGate 단계에서 젠킨스가 5분 내내 하염없이 기다리다가 타임아웃 에러를 뿜고 죽어버리는 현상을 매우 흔하게 겪게 됩니다.

원인 및 해결책: 젠킨스가 SonarQube에게 "코드 가져가서 검사해 줘"라고 던진 후, SonarQube가 검사를 다 마쳤으면 "야, 검사 끝났다 합격이다!"라고 젠킨스에게 다시 신호(Callback)를 주어야 합니다. 그런데 SonarQube에 젠킨스의 주소가 등록되어 있지 않으니 연락을 못 해서 생기는 현상입니다.
반드시 SonarQube 관리자 페이지에 접속하여 [Administration] -> [Configuration] -> [Webhooks] 메뉴로 간 뒤, 젠킨스의 주소 (예: http://jenkins-url.com/sonarqube-webhook/)를 콜백(Callback) 주소로 명시적으로 등록해 주어야만 파이프라인이 정상적으로 넘어가게 됩니다.

 

5. 마무리 및 주요 발전 방향

지금까지 더러운 코드가 운영 서버에 올라가는 끔찍한 사태를 기계적으로 차단해 주는 완벽한 수문장, SonarQube 정적 분석 및 Quality Gate 자동화 구축 방법에 대해 3,000자의 밀도 높은 내용으로 살펴보았습니다. 이제 개발팀은 "테스트 커버리지 80% 미만"이라는 룰을 통과하지 못하면 젠킨스에게 가차 없이 배포를 거절당하게 되며, 이는 전사적인 코드 퀄리티를 자연스럽게 강제 상향시키는 위대한 결과를 낳게 됩니다.

지금까지 우리는 주로 쉘 스크립트나 코드 빌드 위주로 파이프라인을 구성했습니다. 하지만 바야흐로 컨테이너(Container)의 시대입니다. 단순히 .jar 파일을 떨구는 것을 넘어, 코드를 도커 이미지로 굽고(Build) Docker Hub나 AWS ECR에 밀어 넣는(Push) 작업이 현대 CI/CD의 필수 덕목이 되었습니다. 본 설명에서는 복잡한 도커 명령어를 한 줄로 제어하는 궁극의 'Jenkins와 Docker Build & Push 통합 마스터 가이드'에 대해 아주 치밀하게 파헤쳐 보겠습니다!


과거에는 젠킨스가 빌드한 결과물(예: .jar, .war, 빌드된 React 정적 파일)을 운영 서버에 scp나 rsync로 전송하여 덮어씌우는 방식이 주류였습니다. 하지만 바야흐로 컨테이너(Container)의 시대가 도래했습니다. 이제는 빌드 산출물을 압축해서 보내는 것이 아니라, OS와 런타임 환경까지 통째로 얼려버린 도커 이미지(Docker Image)를 만들어 굽고(Build), 원격 이미지 저장소(Registry)로 밀어 넣는(Push) 작업이 현대 CI/CD의 알파이자 오메가가 되었습니다.

본 글에서는 인프라 불변성(Immutable Infrastructure)을 실현하는 Jenkins Pipeline 내에서의 우아한 Docker 제어 기법과 플러그인 100% 활용 노하우에 대하여 다뤄보겠습니다.

1. 사전 준비: Docker in Docker (DinD) 권한 해결

젠킨스 파이프라인 안에서 docker build 명령어를 치려면 가장 먼저 해결해야 할 거대한 장벽이 있습니다. 만약 여러분의 젠킨스 자체가 도커 컨테이너로 떠 있다면, 컨테이너 내부에서 또 도커 명령어를 실행해야 하는 'Docker in Docker' 상황에 놓이게 됩니다.

가장 대중적이고 안정적인 실무 해결책은 Docker Socket Binding (도커 소켓 바인딩)입니다. 젠킨스 컨테이너를 최초로 띄울 때 호스트 머신의 도커 데몬 소켓(/var/run/docker.sock)을 젠킨스 컨테이너 안으로 볼륨 마운트 시켜줍니다. 이렇게 하면 젠킨스 내부에서 치는 docker 명령어가 사실은 호스트 머신의 도커 엔진에게 전달되어 실행되므로, 권한 문제와 성능 저하를 동시에 해결할 수 있습니다.


2. Docker Pipeline Plugin: 객체 지향적 접근

소켓이 연결되었다면 파이프라인의 steps 안에 무식하게 sh 'docker build -t my-app .' 쉘 스크립트를 도배할 수도 있습니다. 하지만 도커 허브(Docker Hub)나 AWS ECR에 로그인(Login)하기 위해 비밀번호를 치고, 태그를 여러 개 달아서 푸시(Push)하는 과정을 생 쉘 스크립트로 짜면 코드가 매우 지저분해지고 보안 토큰이 노출될 위험이 큽니다.

따라서 실무에서는 젠킨스가 공식 지원하는 Docker Pipeline Plugin을 활용합니다. 이 플러그인을 사용하면 도커 명령어를 그루비(Groovy)의 객체 지향 문법처럼 캡슐화하여 아주 우아하게 다룰 수 있습니다. (참고: Declarative Pipeline의 script { ... } 블록 안에서 사용해야 제맛이 납니다.)


3. 실무 적용 코드 파헤치기: Build & Push

단순히 latest 태그만 덮어씌우면 롤백(Rollback)이 불가능합니다. 실무에서는 이번 빌드의 고유 번호(env.BUILD_NUMBER)를 태그로 달아 Push하고, 동시에 latest 태그도 업데이트해 주는 '이중 태깅(Double Tagging)' 전략을 씁니다.

pipeline {
    agent any
    environment {
        // 도커 허브 계정명과 앱 이름 정의
        IMAGE_NAME = "my-dockerhub-id/my-awesome-api"
        // 실무 핵심: 고유한 버저닝을 위해 젠킨스 빌드 번호를 태그로 사용
        IMAGE_TAG = "${env.BUILD_NUMBER}" 
    }
    stages {
        stage('Docker Build & Push') {
            steps {
                script {
                    // 1. withRegistry 블록: 로그인 자격 증명을 젠킨스 Credentials 금고에서 안전하게 꺼내옵니다.
                    // 주요 인자: 레지스트리 URL (Docker Hub는 기본값 생략 가능하나 명시 권장)
                    // 다음 인자: Credentials에 등록해둔 ID ('dockerhub-login-creds')
                    docker.withRegistry('https://registry.hub.docker.com', 'dockerhub-login-creds') {
                        
                        // 2. 이미지 빌드 (Dockerfile이 있는 현재 경로 '.' 에서 빌드)
                        // 반환된 customImage 객체를 변수에 담아둡니다.
                        echo "도커 이미지를 굽고 있습니다..."
                        def customImage = docker.build("${IMAGE_NAME}:${IMAGE_TAG}")
                        
                        // 3. 고유 태그(예: 42번 빌드)로 Push
                        echo "버전 ${IMAGE_TAG} 이미지를 푸시합니다..."
                        customImage.push()
                        
                        // 4. latest 태그로도 한 번 더 Push (서버에서 편하게 당겨 쓰기 위함)
                        echo "latest 태그를 업데이트합니다..."
                        customImage.push('latest')
                    }
                }
            }
        }
    }
}

이 플러그인의 withRegistry 블록을 통과하면, 블록이 끝나는 순간 젠킨스가 알아서 docker logout 명령어를 수행해 주어 호스트 서버에 로그인 정보 찌꺼기가 남는 보안 취약점을 완벽하게 방어해 줍니다.


4. 마무리 및 주요 발전 방향

지금까지 현대 배포의 표준인 도커 이미지를 젠킨스 내부에서 굽고, 플러그인을 활용하여 가장 우아하고 안전하게 레지스트리(Docker Hub)에 밀어 넣는(Push) 전 과정에 대해 다루어 보았습니다. 이제 여러분의 결과물은 가상화된 이미지 형태로 둥둥 떠다니며, 세상 어떤 서버에서든 docker pull 한 방에 동일한 환경으로 실행될 준비를 마쳤습니다.

 

이미지 저장소에 무기(Image)를 완벽하게 장전해 두었으니, 이제 이 무기를 발사하여 실제 전쟁터(Production)에 투입할 차례입니다. 서버가 한두 대라면 docker-compose로 충분하겠지만, 서버가 수십 대라면 오케스트레이션의 제왕인 Kubernetes(쿠버네티스)의 조종석에 앉아야 합니다.

 

다음 포스트에서는 방금 푸시한 따끈따끈한 도커 이미지를 K8s 클러스터로 쏘아 보내 무중단 배포(Rolling Update)를 수행하는 궁극의 'Jenkins & Kubernetes 연동 자동화 전략'에 대해 다뤄보겠습니다.