소나큐브 도입기
소나큐브란
소나큐브는 코드 품질을 높이기 위해 코드가 가지고 있는 이슈와 결함, 복잡성 등을 분석해서 문제가 있는 코드를 분석해주는 정적 분석 도구다.
소나큐브(정적 분석)는 왜 필요할까?
개발이 끝난 코드를 빌드하면, 빌드 후에 테스트가 실행이되고 패키징이 진행이 되서 운영 서버에 배포작업을 하게 될텐데, 코드 자체에서 컴파일 상 문제가 없고 테스트 상 문제가 없더라도 무조건 배포할 수 없다.
정적인 도구와 동적인 도구를 이용해서 정적, 동적 테스트를 거친 후 코드의 이상 여부에 대해 파악해볼 필요가 있다.
우리가 작성하는 테스트에서 기능의 실행 여부, 기능 버그를 테스트했다면 정적 테스트(소나 큐브)에서는 불필요한 리소스의 낭비나 코드의 품질을 높여주기 위해서 다양한 체크를 해주고 리포트를 해준다.
소나큐브의 동작 구조
소나큐브는 크게 스캐너, 서버, DB로 구성되어 있다.
- Sonarqube Scanner가 소스 코드를 scan 한다.
- gradle, maven, jenkins에 대해선 구현되어있는 플러그인을 제공하고, 직접 스캐너를 커스텀할 수도 있다.
- scan한 소스 코드를 Sonarqube 서버로 전달한다.
- Sonarqube Scanner로부터 API 요청을 받으면, 해당 코드에 대한 정적분석을 실행하고 리포트를 생성한다.
- Database 서버를 연결해주면 해당 DB에 분석 결과들을 저장하게 되고 DB 서버를 따로 연결하지 않으면 소나큐브 서버 내부 h2 db를 사용한다.
그렇다면 우리가 해줄 일은 소나큐브 서버 인스턴스를 하나 생성하고, CI 작업이 일어나는 곳에서 소나큐브 스캐너가 발동되게 하면 된다.
소스코드 Scan을 위한 Gradle Scanner 설정하기
- build.gradle
plugins {
id "org.sonarqube" version "3.4.0.2513" // 소나뷰브 플러그인을 추가한다.
}
sonarqube {
properties {
property "sonar.sources", "src" // 소스 경로
property "java", "java" // 언어 지정
property "sonar.sourceEncoding", "UTF-8" // 인코딩 설정
property "sonar.profile", "Sonar way" // 소나큐브에서 분석할 때 적용할 프로필
property "sonar.java.binaries", "${buildDir}/classes" // 바이너리 파일 위치
property "sonar.test.inclusions", "**/*Test.java" // 코드 분석에 사용될 테스트들
property "sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/reports/jacoco/test/jacocoTestReport.xml" // jacoco 플러그인 결과 파일
}
}
위와 같이 소나큐브 설정을 위해 플러그인을 추가해주었고 property들을 설정해주었다. build.gradle 설정에서 property들은 생략이 가능하고 스캔하는 Task를 실행시킬 때 환경변수로 설정할 수 있다.
그래서 아래와 같이 외부에 노출되면 안되는 property 들은 Github Action의 secret key에 추가해주었고 스캔하는 Task를 실행할 때 환경변수로 추가해주었다.
sonar.host.url
: Sonarqube 서버 urlsonar.login
Sonarqube 서버가 지급해준 tokensonar.projectName
: Sonarqube 프로젝트 이름
소나큐브 Scnner 동작을 위한 Github Actions 워크플로우
# Repository의 Actions 탭에 표시될 workflow 이름
name: checkmate-sonarqube
# Workflow를 실행시키기 위한 이벤트 목록
# main, dev 브랜치에 pr을 열고, pr에 커밋이 추가될 때마다 (branches, types)
# backend 하위 패키지에 변경이 일어날 때만 작동 (paths)
on:
pull_request:
branches:
- main
- dev
paths:
- backend/**
types: [opened, synchronize]
# 작업을 수행할 기본 디렉토리 위치 지정
defaults:
run:
working-directory: backend
jobs:
# job의 이름
analysis:
runs-on: ubuntu-latest
# Sonarqube property들 환경변수로 지정
env:
SONARQUBE_PROJECT_KEY: $
SONARQUBE_URL: $
SONARQUBE_TOKEN : $
PR_NUMBER: $
SUBMODULE_TOKEN : $
steps:
# 소스코드 체크아웃
- name: Checkout source code
uses: actions/checkout@v2
with:
token: $
submodules: true
# gradlew 파일 권한 변경
- name: gradlew permission change
run: sudo chmod 755 gradlew
# Gradle의 소나큐브 scanner 발동, 위에 선언한 환경 변수와 함께 지정
- name: Sonaqube Analysis
run: ./gradlew test sonarqube
-Dsonar.host.url=$
-Dsonar.projectKey=$
-Dsonar.projectName=$-$
-Dsonar.login=$
# action을 활용하여 PR에 소나큐브 분석 결과 정보 Comment 발생
- name: Comment Sonarqube URL
uses: actions/github-script@v4
with:
script: |
const { SONARQUBE_PROJECT_KEY, SONARQUBE_URL, PR_NUMBER } = process.env
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `📊 ${ SONARQUBE_PROJECT_KEY }-${ PR_NUMBER } 분석 결과 확인하기 [링크](${SONARQUBE_URL})`
})
이 워크 플로우가 성공적으로 수행이 되면 Github Action에서 잘 수행되었다는 결과를 알려준다.
그리고 소나큐브 서버가 정적 분석을 통해 도출한 결과가 해당 PR 에 Comment 로 달리게 된다.
왜 Action의 시크릿키가 안 불러와질까?
소나큐브를 적용할 때 정말 애먹은 부분이 있었다. 소나큐브 적용 테스트를 할 때는 아래와 같이 성공적으로 수행되어서 merge를 시켰다.
하지만 다음 PR 부터는 소나큐브가 계속 실패하는 것이다.
왜 문제가 발생했는지 파악을 해보니 추가해놓은 시크릿키가 워크플로우에 주입이 안되고 있었다.
시크릿 키를 잘못 추가했나 싶어서 다시 추가도 해보고 몇번을 확인해보았는데 문제는 계속해서 발생했다.
알고보니 pull request로 트리거 된 워크플로우에는 제약이 있었다.
포크한 저장소 (origin) 에서 업스트림 저장소로 풀 리퀘스트를 보내면, 업스트림 저장소의 시크릿 키를 읽을 수 있는 권한이 없어 읽어오지 못하는 것이었다.
처음 성공을 했을 때는 업스트림 브랜치에서 풀 리퀘스트를 생성했기 때문에 시크릿키가 주입이 되어서 잘 동작했던 것이었고 , 실패했을 때는 origin 저장소에서 업스트림으로 풀 리퀘스트를 생성했기 때문에 시크릿 키가 주입이 되지 않았던 것이다.
우리 팀의 브랜치 전략이 새로운 feature에 대해 포크한 저장소(origin)에 push를 하고, 업스트림으로 풀리퀘스트를 하는 방식이였음
이 사실을 알게 된 후, 팀에게 문제 상황을 공유했고 논의한 결과 업스트림 저장소만 이용하자는 결론이 나와 문제를 해결할 수 있었다.