SonarQube 정적분석 및 Jenkins CI/CD 통합

SonarQube 정적분석으로 소스 품질 확보하기

SonarQube는 소스코드의 품질을 검증하기 위한 정적분석 도구 오픈소스로 Jenkins와 CI/CD를 통합하여 동작할 수 있다. 정적분석 도구를 적용하지 않고 애플리케이션의 검증을 진행하기 위해서는 Runtime 환경을 구성하여 UI를 기반으로 테스트를 진행해야하지만, 이는 검증에 소요되는 시간이 많이 소비될 수 있다.

따라서 정적분석도구를 적용하여 Runtime 환경을 구성하지 않고도 검증할 수 있는 환경을 마련해야 한다.

1. SonarQube 설치

a. SonarQube 설치

SonarQube를 Manually하게 설치할 수도 있지만, Docker로 기동하여 SonarQube를 기동해 보도록 하자.

[root@ip-192-168-123-141 ~]# docker run -d --name sonarqube -p 9000:9000 sonarqube:8.6-community
Unable to find image 'sonarqube:8.6-community' locally
8.6-community: Pulling from library/sonarqube
0a6724ff3fcd: Pull complete 
eb833291b55c: Pull complete 
2a91bfbe66cb: Pull complete 
db9c21026d71: Pull complete 
ddac7b0d6339: Pull complete 
Digest: sha256:6716837a86ab991205a7579b1630fb087c64624835c7f78aa233b6e1bf305082
Status: Downloaded newer image for sonarqube:8.6-community
6c7adbbdc0d1429f7e243e9bac6ecdefe7b1fe0808d4dfbe5f15f1e557c9dadf
[root@ip-192-168-123-141 ~]#

b. SonarQube 대시보드

기동이 완료되면 다음과 같이 SonarQube 대시보드에 접속할 수 있다. SonarQube는 9000 포트를 default로 사용하며, 초기 ID/PW는 admin/admin이다. (로그인 시 비밀번호 변경 창이 나타난다.)

접속이 완료되면 다음과 같은 메인화면을 볼 수 있다.

2. SonarQube 구성

a. SonarQube Project 생성 및 Token 생성

먼저 demo project의 정적분석을 진행하기 위해 SonarQube에 프로젝트를 생성하고, Token을 Generate한다.

위와 같이 Create new project > Project key(Project Name) > Set Up 클릭

Provide a token의 경우 임의의 키를 입력하고, Generate를 클릭하면, 키가 생성된다.

생성된 Token을 기반으로 분석을 진행하기 위한 sonar.login 키를 제공한다. 이를 복사하여 Jenkins Pipeline 작성 시 활용한다.

b. Jenkins Webhook 등록

다음으로 Jenkins와의 연동을 위한 Webhooks을 등록한다. Webhook은 Jenkins에서 요청한 정적 분석이 완료된 이후 분석 결과를 Jenkins에 Webhook으로 알려 줌으로써 Jenkins QualifyGate 응답을 받도록 구성하기 위함이다.

Administration > Configuration > Webhooks > Create 클릭

위와 같이 Webhook을 위한 URL을 등록한다. URL은 http://{JENKINS_URL}:{JENKINS_PORT}/sonarqube-webhook/ 으로 등록한다.

위와 같이 등록이 완료되면 SonarQube의 등록이 완료된 상태이다. 이제 Jenkins를 구성해 보도록 하자.

3. Jenkins Pipeline Job Stage 구성

a. SonarQube 플러그인 설치

먼저 SonarQube Server 정보를 Jenkins에 등록한다. 등록을 위해서는 먼저 SonarQube 플러그인이 설치되어야 한다.

위와 같이 SonarQube Scanner, Sonar Quality Gates 플러그인을 설치한다.

b. 시스템 설정 (SonarQube Server 등록)

다음으로 Jenkins 관리 > 시스템 설정 > SonarQube servers에서 SonarQube Server 정보를 등록한다.

위와 같이 SecretKey에 앞서 SonarQube Project에서 생성한 Token을 등록하고 이를 적용한 SonarQube Server 시스템 정보를 설정한다.

c. Global Tool Configuration (SonarQube Scanner 등록)

다음으로 Jenkins 관리 > Global Tool Configuration > SonarQube Scanner에서 SonarQube Scanner 정보를 등록한다.

SonarQube Scanner가 설치되어 있지 않을 경우에는 Install automatically를 체크하여 자동으로 설치하도록 구성한다.

d. Jenkins Stage 추가

마지막으로 SonarQube Analysis & Quality Gate를 수행하는 Stage를 추가한다.

pipeline {
    agent any
    parameters {
        string(name: 'GIT_URL', defaultValue: 'http://192.168.123.141/root/demo.git', description: 'GIT_URL')
        booleanParam(name: 'VERBOSE', defaultValue: false, description: '')
    }
    
    environment {
        GIT_BUSINESS_CD = 'master'
        GITLAB_CREDENTIAL_ID = 'gitlabuser'
        VERBOSE_FLAG = '-q'
    }

    stages{
        stage('Preparation') { // for display purposes
            steps{
                script{
                    env.ymd = sh (returnStdout: true, script: ''' echo `date '+%Y%m%d-%H%M%S'` ''')
                }
                echo("params : ${env.ymd} " + params.tag)
            }
        }

        stage('Checkout') {
            steps{
                git(branch: "${env.GIT_BUSINESS_CD}", 
                credentialsId: "${env.GITLAB_CREDENTIAL_ID}", url: params.GIT_URL, changelog: false, poll: false)
            }
        }
        
        stage('SonarQube analysis') {
            steps{
                withSonarQubeEnv('SonarQube-Server'){
                    sh "mvn clean package"
                    sh "mvn sonar:sonar -Dsonar.projectKey=demo -Dsonar.host.url=http://192.168.123.141:9000 -Dsonar.login=03a3d935387d5a8bb8894ff0a0f282055f39466a"
                }
            }
        }
        
        stage('SonarQube Quality Gate'){
            steps{
                timeout(time: 1, unit: 'MINUTES') {
                    script{
                        echo "Start~~~~"
                        def qg = waitForQualityGate()
                        echo "Status: ${qg.status}"
                        if(qg.status != 'OK') {
                            echo "NOT OK Status: ${qg.status}"
                            updateGitlabCommitStatus(name: "SonarQube Quality Gate", state: "failed")
                            error "Pipeline aborted due to quality gate failure: ${qg.status}"
                        } else{
                            echo "OK Status: ${qg.status}"
                            updateGitlabCommitStatus(name: "SonarQube Quality Gate", state: "success")
                        }
                        echo "End~~~~"
                    }
                }
            }
        }
    }
}

– SonarQube Analysis : 대상 SonarQube Server를 지정하여 분석을 진행하는 과정 (withSonarQubeEnv는 Jenkins 관리 > 시스템 설정에 등록한 SonarQube Server Name과 매핑)

– SonarQube Quality Gate : SonarQube Server에서 분석 결과를 응답하기까지 대기하도록 하는 Stage, 대기 시간을 지정하여 무한정 대기하는 상태를 방지하도록 함 (waitForQualityGate는 Server에서 분석을 완료하고 상태를 반환할때까지 파이프라인을 중단시키는 시간을 지정)

e. Jenkins Job 실행

위와 같은 구성이 완료되면, Jenkins Job을 실행하여 SonarQube 서버와 연동할 수 있다.

Started by user son.nara
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /root/.jenkins/workspace/DemoSonarQubeJob
[Pipeline] {
[Pipeline] withEnv
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Preparation)
[Pipeline] script
[Pipeline] {
[Pipeline] sh
++ date +%Y%m%d-%H%M%S
+ echo 20201219-073000
[Pipeline] }
[Pipeline] // script
[Pipeline] echo
params : 20201219-073000
 null
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Checkout)
[Pipeline] git
The recommended git tool is: NONE
Warning: CredentialId "gitlabuser" could not be found.
Cloning the remote Git repository
Cloning repository http://192.168.123.141/root/demo.git
 > git init /root/.jenkins/workspace/DemoSonarQubeJob # timeout=10
Fetching upstream changes from http://192.168.123.141/root/demo.git
 > git --version # timeout=10
 > git --version # 'git version 2.23.3'
 > git fetch --tags --force --progress -- http://192.168.123.141/root/demo.git +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git config remote.origin.url http://192.168.123.141/root/demo.git # timeout=10
 > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10
Avoid second fetch
 > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
Checking out Revision 6524006a91bd94386c0615dabc74aa8246afc2e4 (refs/remotes/origin/master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 6524006a91bd94386c0615dabc74aa8246afc2e4 # timeout=10
 > git branch -a -v --no-abbrev # timeout=10
 > git checkout -b master 6524006a91bd94386c0615dabc74aa8246afc2e4 # timeout=10
Commit message: "init"
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (SonarQube analysis)
[Pipeline] withSonarQubeEnv
Injecting SonarQube environment variables using the configuration: SonarQube-Server
[Pipeline] {
[Pipeline] sh
+ mvn clean package
[INFO] Scanning for projects...
[WARNING] 
[WARNING] Some problems were encountered while building the effective model for com.spring:demo:jar:0.0.1-SNAPSHOT
[WARNING] 'dependencyManagement.dependencies.dependency.exclusions.exclusion.artifactId' for org.quartz-scheduler:quartz:jar with value '*' does not match a valid id pattern. @ org.springframework.boot:spring-boot-dependencies:2.3.7.RELEASE, /root/.m2/repository/org/springframework/boot/spring-boot-dependencies/2.3.7.RELEASE/spring-boot-dependencies-2.3.7.RELEASE.pom, line 2014, column 25
[WARNING] 
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING] 
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING] 
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ demo ---
[INFO] 
[INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /root/.jenkins/workspace/DemoSonarQubeJob/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /root/.jenkins/workspace/DemoSonarQubeJob/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /root/.jenkins/workspace/DemoSonarQubeJob/target/test-classes
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ demo ---
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.spring.demo.DemoApplicationTests
07:30:04.132 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate]
07:30:04.144 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)]
07:30:04.176 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [com.spring.demo.DemoApplicationTests] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper]
07:30:04.192 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.spring.demo.DemoApplicationTests], using SpringBootContextLoader
07:30:04.196 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.spring.demo.DemoApplicationTests]: class path resource [com/spring/demo/DemoApplicationTests-context.xml] does not exist
07:30:04.197 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.spring.demo.DemoApplicationTests]: class path resource [com/spring/demo/DemoApplicationTestsContext.groovy] does not exist
07:30:04.197 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [com.spring.demo.DemoApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}.
07:30:04.198 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [com.spring.demo.DemoApplicationTests]: DemoApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
07:30:04.236 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [com.spring.demo.DemoApplicationTests]
07:30:04.300 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [/root/.jenkins/workspace/DemoSonarQubeJob/target/classes/com/spring/demo/DemoApplication.class]
07:30:04.301 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration com.spring.demo.DemoApplication for test class com.spring.demo.DemoApplicationTests
07:30:04.409 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [com.spring.demo.DemoApplicationTests]: using defaults.
07:30:04.418 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener]
07:30:04.429 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/TransactionDefinition]
07:30:04.429 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute]
07:30:04.430 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@4efc180e, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@bd4dc25, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@25084a1e, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@156b88f5, org.springframework.test.context.support.DirtiesContextTestExecutionListener@3bf9ce3e, org.springframework.test.context.event.EventPublishingTestExecutionListener@16610890, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@71def8f8, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@383bfa16, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@4d465b11, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@53fdffa1, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@5562c41e, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener@32ee6fee]
07:30:04.433 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@47987356 testClass = DemoApplicationTests, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@22ef9844 testClass = DemoApplicationTests, locations = '{}', classes = '{class com.spring.demo.DemoApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@4c1d9d4b, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@45018215, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@54c562f7, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@7f010382, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@35f983a6], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true]], class annotated with @DirtiesContext [false] with mode [null].
07:30:04.460 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.7.RELEASE)

2020-12-19 07:30:04.673  INFO 11250 --- [           main] com.spring.demo.DemoApplicationTests     : Starting DemoApplicationTests on ip-192-168-123-141.ap-northeast-2.compute.internal with PID 11250 (started by root in /root/.jenkins/workspace/DemoSonarQubeJob)
2020-12-19 07:30:04.675  INFO 11250 --- [           main] com.spring.demo.DemoApplicationTests     : No active profile set, falling back to default profiles: default
2020-12-19 07:30:05.637  INFO 11250 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-12-19 07:30:05.889  INFO 11250 --- [           main] com.spring.demo.DemoApplicationTests     : Started DemoApplicationTests in 1.42 seconds (JVM running for 2.302)
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.993 s - in com.spring.demo.DemoApplicationTests
2020-12-19 07:30:06.116  INFO 11250 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ demo ---
[INFO] Building jar: /root/.jenkins/workspace/DemoSonarQubeJob/target/demo-0.0.1-SNAPSHOT.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.3.7.RELEASE:repackage (repackage) @ demo ---
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.043s
[INFO] Finished at: Sat Dec 19 07:30:06 UTC 2020
[INFO] Final Memory: 24M/302M
[INFO] ------------------------------------------------------------------------
[Pipeline] sh
+ mvn sonar:sonar -Dsonar.projectKey=demo -Dsonar.host.url=http://192.168.123.141:9000 -Dsonar.login=******
[INFO] Scanning for projects...
[WARNING] 
[WARNING] Some problems were encountered while building the effective model for com.spring:demo:jar:0.0.1-SNAPSHOT
[WARNING] 'dependencyManagement.dependencies.dependency.exclusions.exclusion.artifactId' for org.quartz-scheduler:quartz:jar with value '*' does not match a valid id pattern. @ org.springframework.boot:spring-boot-dependencies:2.3.7.RELEASE, /root/.m2/repository/org/springframework/boot/spring-boot-dependencies/2.3.7.RELEASE/spring-boot-dependencies-2.3.7.RELEASE.pom, line 2014, column 25
[WARNING] 
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING] 
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING] 
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- sonar-maven-plugin:3.7.0.1746:sonar (default-cli) @ demo ---
[INFO] User cache: /root/.sonar/cache
[INFO] SonarQube version: 8.6.0
[INFO] Default locale: "en_US", source code encoding: "UTF-8"
[WARNING] SonarScanner will require Java 11 to run starting in SonarQube 8.x
[INFO] Load global settings
[INFO] Load global settings (done) | time=101ms
[INFO] Server id: BF41A1F2-AXZ41q2bqGRDxVJRfrLo
[INFO] User cache: /root/.sonar/cache
[INFO] Load/download plugins
[INFO] Load plugins index
[INFO] Load plugins index (done) | time=61ms
[INFO] Load/download plugins (done) | time=88ms
[INFO] Process project properties
[INFO] Process project properties (done) | time=11ms
[INFO] Execute project builders
[INFO] Execute project builders (done) | time=2ms
[INFO] Project key: demo
[INFO] Base dir: /root/.jenkins/workspace/DemoSonarQubeJob
[INFO] Working dir: /root/.jenkins/workspace/DemoSonarQubeJob/target/sonar
[INFO] Load project settings for component key: 'demo'
[INFO] Load project settings for component key: 'demo' (done) | time=14ms
[INFO] Load quality profiles
[INFO] Load quality profiles (done) | time=146ms
[INFO] Auto-configuring with CI 'Jenkins'
[INFO] Load active rules
[INFO] Load active rules (done) | time=1289ms
[INFO] Indexing files...
[INFO] Project configuration:
[INFO] 3 files indexed
[INFO] 0 files ignored because of scm ignore settings
[INFO] Quality profile for java: Sonar way
[INFO] Quality profile for xml: Sonar way
[INFO] ------------- Run sensors on module demo
[INFO] Load metrics repository
[INFO] Load metrics repository (done) | time=19ms
[INFO] Sensor JavaSquidSensor [java]
[INFO] Configured Java source version (sonar.java.source): 8
[INFO] JavaClasspath initialization
[INFO] JavaClasspath initialization (done) | time=12ms
[INFO] JavaTestClasspath initialization
[INFO] JavaTestClasspath initialization (done) | time=2ms
[INFO] Java Main Files AST scan
[INFO] 1 source files to be analyzed
[INFO] Load project repositories
[INFO] Load project repositories (done) | time=17ms
[INFO] 1/1 source files have been analyzed
[INFO] Java Main Files AST scan (done) | time=1022ms
[INFO] Java Test Files AST scan
[INFO] 1 source files to be analyzed
[INFO] [INFO] 1/1 source files have been analyzed
Java Test Files AST scan (done) | time=69ms
[INFO] Java Generated Files AST scan
[INFO] 0 source files to be analyzed
[INFO] Java Generated Files AST scan (done) | time=1ms
[INFO] Sensor JavaSquidSensor [java] (done) | time=1261ms
[INFO] 0/0 source files have been analyzed
[INFO] Sensor CSS Rules [cssfamily]
[INFO] No CSS, PHP, HTML or VueJS files are found in the project. CSS analysis is skipped.
[INFO] Sensor CSS Rules [cssfamily] (done) | time=1ms
[INFO] Sensor JaCoCo XML Report Importer [jacoco]
[INFO] 'sonar.coverage.jacoco.xmlReportPaths' is not defined. Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml
[INFO] No report imported, no coverage information will be imported by JaCoCo XML Report Importer
[INFO] Sensor JaCoCo XML Report Importer [jacoco] (done) | time=3ms
[INFO] Sensor C# Properties [csharp]
[INFO] Sensor C# Properties [csharp] (done) | time=1ms
[INFO] Sensor SurefireSensor [java]
[INFO] parsing [/root/.jenkins/workspace/DemoSonarQubeJob/target/surefire-reports]
[INFO] Sensor SurefireSensor [java] (done) | time=72ms
[INFO] Sensor JavaXmlSensor [java]
[INFO] 1 source files to be analyzed
[INFO] Sensor JavaXmlSensor [java] (done) | time=128ms
[INFO] 1/1 source files have been analyzed
[INFO] Sensor HTML [web]
[INFO] Sensor HTML [web] (done) | time=3ms
[INFO] Sensor XML Sensor [xml]
[INFO] 1 source files to be analyzed
[INFO] Sensor XML Sensor [xml] (done) | time=105ms
[INFO] Sensor VB.NET Properties [vbnet]
[INFO] 1/1 source files have been analyzed
[INFO] Sensor VB.NET Properties [vbnet] (done) | time=1ms
[INFO] ------------- Run sensors on project
[INFO] Sensor Zero Coverage Sensor
[INFO] Sensor Zero Coverage Sensor (done) | time=7ms
[INFO] Sensor Java CPD Block Indexer
[INFO] Sensor Java CPD Block Indexer (done) | time=11ms
[INFO] CPD Executor 1 file had no CPD blocks
[INFO] CPD Executor Calculating CPD for 0 files
[INFO] CPD Executor CPD calculation finished (done) | time=0ms
[INFO] Analysis report generated in 65ms, dir size=93 KB
[INFO] Analysis report compressed in 17ms, zip size=15 KB
[INFO] Analysis report uploaded in 24ms
[INFO] ANALYSIS SUCCESSFUL, you can browse http://192.168.123.141:9000/dashboard?id=demo
[INFO] Note that you will be able to access the updated dashboard once the server has processed the submitted analysis report
[INFO] More about the report processing at http://192.168.123.141:9000/api/ce/task?id=AXZ55_q4qGRDxVJRfwCx
[INFO] Analysis total time: 4.981 s
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.778s
[INFO] Finished at: Sat Dec 19 07:30:14 UTC 2020
[INFO] Final Memory: 41M/557M
[INFO] ------------------------------------------------------------------------
[Pipeline] }
[Pipeline] // withSonarQubeEnv
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (SonarQube Quality Gate)
[Pipeline] timeout
Timeout set to expire in 1 min 0 sec
[Pipeline] {
[Pipeline] script
[Pipeline] {
[Pipeline] echo
Start~~~~
[Pipeline] waitForQualityGate
Checking status of SonarQube task 'AXZ55_q4qGRDxVJRfwCx' on server 'SonarQube-Server'
SonarQube task 'AXZ55_q4qGRDxVJRfwCx' status is 'PENDING'
SonarQube task 'AXZ55_q4qGRDxVJRfwCx' status is 'SUCCESS'
SonarQube task 'AXZ55_q4qGRDxVJRfwCx' completed. Quality gate is 'OK'
[Pipeline] echo
Status: OK
[Pipeline] echo
OK Status: OK
[Pipeline] updateGitlabCommitStatus
[Pipeline] echo
End~~~~
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // timeout
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

위와 같은 과정으로 JenkinsPipeline을 실행할 수 있으며, updateGitlabCommitStatus에 따라 분기처리 할 수 있다.

이와 같이 Jenkins의 SonarQube를 활용하여 소스코드의 정적분석을 진행하여 품질을 확보할 수 있다.

4. SonarQube 상세 구성

a. False Positive (오탐)

SonarQube는 Rule에 등록된 처리 방식에 따라 의도하지 않게 오류로 판단되는 경우를 방지하기 하기 위해 오탐처리를 지원한다.

오탐 처리 (Issues > Issue 선택 > Comment 입력)

Comment 입력 후 Open Combo Box를 확장하면, 다음과 같이 상태를 변경할 수 있는 창이 오픈된다. 오탐의 경우 False Positive를 선택한다. (Resolve as false positive)

위와 같이 오탐에 대한 처리가 완료되고 다시 Issue 상태로 돌아가보면 기존의 Code Smell이 사라진것을 확인할 수 있다. 본 예시에서는 원하는 처리 방향임을 명시적으로 지정하는 용도로써 False Positive를 적용했지만, 실제 오탐이 발생한 경우 위와 동일한 프로세스로 오탐을 허용할 수 있다.

오탐 삭제 (Issue > Resolution > False Positive 선택)

Resolved 상태를 클릭하여 Reopen을 선택한다. Reopen 후 상태가 False Positive에서 Unresolved로 변경되었는지 확인한다.

b. Rule 관리

Rule을 등록하는 방법은 두가지가 있다. 직접 등록하는 방법과 사전에 정의된 Rule Set을 등록하는 방법이다.

직접 Rule 등록 (Quality Profiles > Create 클릭)

위와 같이 My Java Profile을 생성하고, 룰을 하나씩 원하는데로 조합하여 생성할 수 있다. 또한 기 작성되어 있는 다양한 Rule Set을 한번에 적용하기 위해 Create 대신 Restore를 선택할 수 있다.

위와 같이 My Java Profile을 등록하고, 오른쪽 톱니바퀴에서 Set as Default를 선택한다.

이 상태에서 다시 Jenkins Pipeline을 빌드해 보도록 하자.

빌드 완료 후 Project Information 정보를 확인해 보면, Quality Profile used (java)가 My Java Profile로 변경된 것을 확인할 수 있다.

이는 Default로 설정할 수도 있지만, 각 프로젝트 별로 서로 다른 Profile을 등록하기 위해 Project Settings를 활용할 수 있다.

Project 별로 다양한 설정을 부여할 수 있으며, Project Settings > Quality Profiles로 선택하여 적용할 수 있다.

오른쪽 Change Profile 버튼을 클릭하여 원하는 Profile을 선택하여 적용할 수 있다.

 

5. SonarQube 분석 결과

마지막으로 분석 결과를 확인해 보자. Jenkins의 SonarQube Analysis가 완료되면, SonarQube에 접속하여 아래와 같은 분석 결과를 확인할 수 있다.

상세 내역을 확인해 보면 아래와 같이 신규 추가된 New Code와 전체 분석 결과인 Overall Code로 구분하여 확인할 수 있다.

아래와 같이 Quality gate Stage에서 Quality gate is ‘ERROR’ Return을 받을 경우 이후 Maven Build 스탭이 진행되지 못하도록 파이프라인을 종료 시키도록 구성할 수 있다.

아래와 같이 SonarQube의 Status Failed 상태는 waitForQualityGate의 ERROR로 리턴된다.

Started by user son.nara
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /root/.jenkins/workspace/DemoSonarQubeJob
...
...
...
...
...
[Pipeline] }
[Pipeline] // withSonarQubeEnv
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (SonarQube Quality Gate)
[Pipeline] timeout
Timeout set to expire in 1 min 0 sec
[Pipeline] {
[Pipeline] script
[Pipeline] {
[Pipeline] echo
Start~~~~
[Pipeline] waitForQualityGate
Checking status of SonarQube task 'AXZ6LxK4qGRDxVJRfwCz' on server 'SonarQube-Server'
SonarQube task 'AXZ6LxK4qGRDxVJRfwCz' status is 'IN_PROGRESS'
SonarQube task 'AXZ6LxK4qGRDxVJRfwCz' status is 'SUCCESS'
SonarQube task 'AXZ6LxK4qGRDxVJRfwCz' completed. Quality gate is 'ERROR'
[Pipeline] echo
Status: ERROR
[Pipeline] echo
NOT OK Status: ERROR
[Pipeline] updateGitlabCommitStatus
[Pipeline] error
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // timeout
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Build)
Stage "Build" skipped due to earlier failure(s)
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
ERROR: Pipeline aborted due to quality gate failure: ERROR
Finished: FAILURE

이와 같이 SonarQube의 정적분석을 통해 크리티컬한 소스 결함 및 분석을 통해 배포를 사전에 차단할 수 있도록 구성할 수 있다.

Bugs, Vulnerabilities, Code Smells 등의 여부에 따라 상태가 변경될 수 있다.

위의 경우 신규로 New Code Smell이 발생되었으며, 1을 클릭해보면 아래와 같이 Detail 정보를 확인할 수 있다.

진단 내용을 확인해 보니, Contoller의 어노테이션을 다음과 같이 수정을 권고하고 있다.

[AS-IS Code]
package com.spring.demo;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoHelloController {

	@RequestMapping(value = "/hello", method = RequestMethod.GET)
	public String hello() {
		return "Hello MSA World!";

	}
}

[TO-BE Code]
package com.spring.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoHelloController {
	@GetMapping(value = "/hello")
	public String hello() {
		return "Hello MSA World!";

	}
}

소스코드를 수정함에도 불구하고, Failed 상태가 유지될 경우에는 Quality Gates의 임계치를 검토해야 한다.

상단 메뉴 Quality Gates를 클릭하면 아래와 같이 DEFAULT로 추가되어 있는 Sonar way 라는 Conditions를 확인할 수 있다.

위와 같은 Condition은 모든 Project에 일괄 적용되는 Default 설정으로 새로 추가되는 코드가 Coverage 80% 이상의 테스트를 지원해야 하며, 각 등급이 A를 받고 Security Hotspots Reviewed는 100%를 유지해야 Passed 상태가 되는 Condition이다. 초기 개발 시작 단계에서 위와 같은 Condition을 기준으로 작성할 경우 Passed 상태를 만들기까지 많은 시간이 소요될 수 있다.

이로 인해 아래와 같이 Sonar Test라는 다른 Quality Gates를 생성하고, 이를 Default로 지정하여 테스트를 커버하되 Sonar way에서 지정하는 Condition을 목표로 향상 시켜가는 것을 권고한다. 

이와 같이 수정 후 다시 배포해 보면, 

Passed 상태로 변경된 것을 확인할 수 있다.

Quality Gates에 대한 상태를 진단하기 위해서는

1) Bugs, Vulnerabilities, Code Smell, Security Hotspots, Dept 등의 이슈를 직접 처리하는 과정

2) Quality Gates의 Condition 조절

의 순으로 확인해 볼 수 있을 것이다.

Rule에 의해 Detect 된 Bugs, Vulnerabilities, Code Smell, Security Hotspots, Dept 등을 모두 일거하여 해결 방안을 적을 수는 없겠지만, 이와 동일한 프로세스로 조치가 취해 짐을 기억하고 처리를 진행하게 된다.