sonar-scanner를 처음 돌렸을 때 돌아온 건 분석 결과가 아니라 이 에러였다.
08:45:50.100 ERROR Error during SonarScanner CLI execution
org.sonar.java.AnalysisException: Your project contains .java files, please provide compiled classes with sonar.java.binaries property, or exclude them from the analysis with sonar.exclusions property.
demoapp 프로젝트에 SonarQube로 SAST(정적 애플리케이션 보안 테스트) 환경을 붙이면서 겪은 일을 정리한다. 처음에는 Mac에서 brew로 Scanner를 설치해 끝냈고, 그다음 brew가 없는 환경에서도 같은 구성이 돌도록 서버와 Scanner를 모두 Docker로 옮겼다.
서버는 Docker로, Scanner는 brew로 시작했다
SonarQube는 두 조각으로 나뉜다. SonarQube는 코드 품질과 보안을 정적으로 분석하고 결과를 웹 UI로 보여주는 서버다. SonarScanner는 소스를 그 서버로 보내 분석을 시키는 커맨드라인 도구다.
첫 구성에서 서버는 docker run으로 띄우고, Scanner만 brew로 설치했다.
# SonarQube 서버를 Docker로 실행
docker run -d --name sonarqube \
-p 9000:9000 \
-e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
sonarqube:lts-community
# Mac에서 SonarScanner 설치
brew install sonar-scanner
# 설치 확인
sonar-scanner --version
서버가 뜨면 웹 UI에서 프로젝트를 만들고 토큰을 받는다.
SonarQube 웹 UI(
http://localhost:9000)에 접속한다.로그인 후 상단 “Projects"에서 “Create Project"를 누른다.
“Manually"를 선택한다.
이름(예: demoapp)과 display name을 입력하고 “Set Up"을 누른다.
“Locally"를 선택한다.
“Generate” 버튼으로 토큰을 만들고 복사해 둔다.
프로젝트 Key와 토큰을 기억해 둔다. (예: 프로젝트 key
demoapp, 토큰sqp_6f...)
프로젝트 루트에 sonar-project.properties를 만들고 Scanner를 실행한다.
sonar.projectKey=demoapp
sonar.projectName=DemoApp
sonar.host.url=http://localhost:9000
sonar.login=<여기에_아까_복사한_토큰_붙여넣기>
# Gradle 프로젝트일 경우
sonar.java.binaries=build/classes
# Maven 프로젝트일 경우
# sonar.java.binaries=target/classes
# 프로젝트 루트 폴더로 이동
cd /Users/jin/workspace/DevSecOps/demoapp
# 정적 분석 실행
sonar-scanner
Java 분석은 소스가 아니라 .class를 요구한다
맨 위의 에러가 여기서 나왔다. 메시지를 풀면 “자바 파일은 있는데 컴파일된 클래스 파일(.class)을 못 찾겠다"는 뜻이다. SonarQube의 Java 정적 분석은 소스만으로 끝나지 않고 컴파일된 클래스 파일을 반드시 필요로 한다. sonar.java.binaries에 Gradle이면 build/classes, Maven이면 target/classes처럼 .class 경로를 적어 줘야 하고, 그 경로에 실제로 파일이 있어야 한다.
해결은 빌드를 먼저 하는 것이었다.
# 프로젝트 루트 경로에서 빌드해 build/classes를 만든다
./gradlew build
# 그다음 다시 분석
sonar-scanner
brew가 없는 곳에서도 같은 구성이 돌게 하고 싶었다
여기까지로 Mac에서는 분석이 돌았지만 뭔가 아쉬웠다. 이 구성은 brew가 있다는 전제 위에 서 있다. brew를 쓸 수 없는 서버나 다른 개발자의 머신, 좀 더 보편적인 환경에서도 그대로 쓸 수 있는 방법이 필요했다.
그래서 서버와 Scanner를 모두 컨테이너로 옮겼다. 서버는 docker compose로 띄우고, Scanner는 sonarsource/sonar-scanner-cli 이미지를 일회성 컨테이너로 돌린다.
docker compose로 서버 띄우기
docker run 한 줄과 달리 compose 파일에는 volume을 붙여 분석 데이터와 플러그인이 컨테이너 재생성 후에도 남도록 했다.
version: "3.8"
services:
sonarqube:
image: sonarqube:lts-community
container_name: sonarqube
ports:
- "9000:9000"
environment:
# Elasticsearch 부트스트랩 체크를 끈다 (로컬 실습용)
- SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true
volumes:
# 분석 데이터 보존
- sonarqube_data:/opt/sonarqube/data
# 플러그인 보존
- sonarqube_extensions:/opt/sonarqube/extensions
volumes:
sonarqube_data:
sonarqube_extensions:
# docker-compose 파일이 있는 디렉토리에서 실행
docker-compose up -d sonarqube
SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true는 SonarQube 안에 들어 있는 Elasticsearch의 부트스트랩 체크를 건너뛰게 한다. 로컬에서 커널 설정을 따로 만지지 않고 띄우기 위한 옵션이라 운영 환경에서는 다시 볼 필요가 있다.
Scanner를 컨테이너로 돌리기
설정값은 devsecops.env로 빼고, 스크립트가 그 값을 읽어 Scanner 컨테이너에 넘긴다.
SONAR_PROJECT_KEY=demoapp
SONAR_LOGIN={토큰}
SONAR_BINARIES=build/classes
SONAR_HOST_URL=http://host.docker.internal:9000
SONAR_SOURCES=.
PROJ_DIR=/Users/jin/IdeaProjects/workspace/DevSecOps/demoapp
Scanner가 컨테이너 안에서 돌기 때문에 서버 주소를 호스트를 가리키는 host.docker.internal로 바꿔야 한다. 컨테이너 안의 localhost는 컨테이너 자신이고, 호스트의 9000번 포트에 떠 있는 SonarQube에 닿으려면 호스트를 가리키는 이름이 필요하다.
#!/bin/bash
# 같은 경로에 있는 devsecops.env 파일을 읽어온다
source ./devsecops.env
# 프로젝트 디렉토리를 /usr/src에 마운트하고 Scanner를 일회성으로 실행
docker run --rm \
-v "$PROJ_DIR":/usr/src \
-w /usr/src \
sonarsource/sonar-scanner-cli \
-Dsonar.projectKey="$SONAR_PROJECT_KEY" \
-Dsonar.sources="$SONAR_SOURCES" \
-Dsonar.host.url="$SONAR_HOST_URL" \
-Dsonar.login="$SONAR_LOGIN" \
-Dsonar.java.binaries="$SONAR_BINARIES"
# 실행 권한 부여
chmod +x sonar-docker.sh
# 실행
./sonar-docker.sh
sonar.java.binaries는 여기서도 그대로 필요하다. Scanner를 컨테이너로 옮겨도 .class를 요구하는 건 달라지지 않으니, 스크립트를 돌리기 전에 호스트에서 빌드가 끝나 있어야 한다.
SonarQube SAST 분석은 이렇게 성공했고, 이제 이 구성에 필요한 건 Docker 하나뿐이다.
정리
brew 버전은 금방 끝났지만 Mac 밖으로 가져갈 수 없었다. Docker 버전은 파일 세 개(docker-compose.yml, devsecops.env, sonar-docker.sh)로 같은 분석을 어디서든 재현한다. 준비할 파일은 늘었지만, 옮겨 갈 수 있다는 점이 그 값을 한다.