SonarQube Kod Kalite Analizi: Jenkins ile CI/CD Entegrasyonu
Kod kalitesi meselesi çoğu zaman “sonra hallederiz” kategorisine giriyor ve sonra hiç halledilmiyor. Teknik borç birikmeye devam ediyor, yeni geliştirici geldiğinde “bu kod ne anlama geliyor” soruları başlıyor, production’da gizemli bug’lar ortaya çıkıyor. SonarQube tam bu noktada devreye giriyor: her commit’te kodunuzu otomatik analiz ediyor, güvenlik açıklarını buluyor, tekrarlı kod bloklarını işaret ediyor ve genel kalite skorunuzu takip ediyor. Jenkins ile entegre ettiğinizde ise bu analiz CI/CD pipeline’ınızın doğal bir parçası haline geliyor. Kalitesiz kod artık merge bile olamıyor.
Ortam Gereksinimleri ve Hazırlık
Kuruluma geçmeden önce neye ihtiyaç duyduğunuzu netleştirelim.
Sunucu tarafı:
- SonarQube için en az 2 CPU, 4 GB RAM (community edition için)
- Jenkins için en az 2 CPU, 4 GB RAM
- Java 17 veya üzeri (SonarQube 10.x için zorunlu)
- PostgreSQL 12 veya üzeri (production ortamı için H2 kullanmayın)
Ağ tarafı:
- SonarQube varsayılan olarak 9000 portunu kullanıyor
- Jenkins ve SonarQube’un birbirine erişebildiğinden emin olun
- Eğer ayrı sunuculardaysa firewall kurallarını ayarlayın
Bu yazıda SonarQube ve Jenkins’in kurulu olduğunu varsayıyorum. Odağımız entegrasyon ve pipeline konfigürasyonu olacak. Docker Compose ile hızlı bir test ortamı kurmak isterseniz aşağıdaki dosyayı kullanabilirsiniz:
# docker-compose.yml
version: '3.8'
services:
sonarqube:
image: sonarqube:10-community
container_name: sonarqube
depends_on:
- sonardb
environment:
SONAR_JDBC_URL: jdbc:postgresql://sonardb:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: sonar_secret_pass
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
ports:
- "9000:9000"
sonardb:
image: postgres:15
container_name: sonardb
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar_secret_pass
POSTGRES_DB: sonar
volumes:
- postgresql_data:/var/lib/postgresql/data
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
postgresql_data:
# Compose'u başlatın
docker-compose up -d
# Container loglarını takip edin
docker-compose logs -f sonarqube
# SonarQube hazır olduğunda şu mesajı görürsünüz:
# SonarQube is up
SonarQube ilk açılışta admin/admin credentials’ı ile giriş yapıyorsunuz. İlk iş şifreyi değiştirmek. Administration > Security > Users yolunu izleyin.
SonarQube’da Token ve Proje Oluşturma
Jenkins’in SonarQube’a bağlanabilmesi için bir authentication token’a ihtiyacı var. Kullanıcı adı/şifre yerine token kullanmak hem güvenli hem de best practice.
Token oluşturma:
- SonarQube arayüzüne girin
- Sağ üstte hesabınıza tıklayın
- “My Account” > “Security” sekmesine gidin
- Token Name:
jenkins-integrationyazın - Type:
Global Analysis Tokenseçin - Generate butonuna basın
- Token’ı hemen kopyalayın, bir daha göremezsiniz!
Proje oluşturma:
Manuel proje oluşturmak yerine ilk analiz sırasında otomatik oluşturulmasına izin verebilirsiniz. Ama önceden oluşturmak daha kontrollü bir yaklaşım:
- “Projects” > “Create Project” > “Manually”
- Project display name:
MyApp Backend - Project key:
myapp-backend(bu key pipeline’da kullanılacak) - Main branch name:
main
Quality Gate ayarını da buradan yapabilirsiniz. Default “Sonar way” gate’i başlangıç için yeterli.
Jenkins’te SonarQube Scanner Kurulumu
Jenkins’e SonarQube Scanner plugin’ini ve konfigürasyonunu eklememiz gerekiyor.
Plugin kurulumu:
- Jenkins > Manage Jenkins > Plugins > Available plugins
- “SonarQube Scanner” aratın ve kurun
- Jenkins’i yeniden başlatın
SonarQube server konfigürasyonu:
Manage Jenkins > Configure System > SonarQube servers bölümüne gidin:
- “Add SonarQube” butonuna tıklayın
- Name:
SonarQube(pipeline’da bu ismi kullanacağız) - Server URL:
http://sonarqube:9000(ya da sunucu IP’niz) - Server authentication token: Dropdown’dan “Add” > “Jenkins” seçin
Credential eklerken:
- Kind: Secret text
- Secret: Az önce oluşturduğunuz token
- ID:
sonarqube-token - Description:
SonarQube Analysis Token
SonarQube Scanner tool konfigürasyonu:
Manage Jenkins > Tools > SonarQube Scanner bölümüne gidin:
- “Add SonarQube Scanner” tıklayın
- Name:
SonarScanner - Install automatically işaretleyin
- En güncel sürümü seçin
Bu ayarlar Jenkins’in SonarQube ile konuşabilmesi için yeterli. Şimdi asıl işe, pipeline yazmaya geçebiliriz.
Temel Jenkins Pipeline Entegrasyonu
En basit haliyle bir Jenkinsfile nasıl görünür:
// Jenkinsfile - Basit SonarQube entegrasyonu
pipeline {
agent any
environment {
SONAR_PROJECT_KEY = 'myapp-backend'
SONAR_PROJECT_NAME = 'MyApp Backend'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn clean compile -DskipTests'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
jacoco(
execPattern: '**/target/jacoco.exec',
classPattern: '**/target/classes',
sourcePattern: '**/src/main/java'
)
}
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh """
mvn sonar:sonar
-Dsonar.projectKey=${SONAR_PROJECT_KEY}
-Dsonar.projectName='${SONAR_PROJECT_NAME}'
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
"""
}
}
}
stage('Quality Gate') {
steps {
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
}
post {
failure {
echo 'Pipeline failed! Quality gate veya test hatasi olabilir.'
}
success {
echo 'Pipeline basariyla tamamlandi. Kod kalitesi onaylandi!'
}
}
}
Burada dikkat etmeniz gereken kritik nokta: waitForQualityGate adımı. Bu adım SonarQube analizinin tamamlanmasını bekliyor ve Quality Gate sonucuna göre pipeline’ı başarılı ya da başarısız olarak işaretliyor. abortPipeline: true ile kalitesiz kod kesinlikle ilerleyemiyor.
Gerçek Dünya Senaryosu: Java Maven Projesi
Büyük bir e-ticaret firmasında çalışıyorsunuz diyelim. Java backend servisleriniz var, her sprint sonunda yayına çıkıyorsunuz ve teknik borç kontrol dışına çıkmaya başladı. Takım lideriniz “artık her PR’da SonarQube geçmeli” diyor. İşte bu senaryoya uygun production-ready bir pipeline:
// Jenkinsfile - Production Java Maven projesi
pipeline {
agent {
docker {
image 'maven:3.9-eclipse-temurin-17'
args '-v /root/.m2:/root/.m2'
}
}
options {
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
environment {
SONAR_PROJECT_KEY = 'ecommerce-backend'
MAVEN_OPTS = '-Xmx1024m'
BRANCH_NAME = "${env.GIT_BRANCH?.replaceAll('origin/', '') ?: 'unknown'}"
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT_MSG = sh(
script: 'git log -1 --pretty=%B',
returnStdout: true
).trim()
env.GIT_AUTHOR = sh(
script: 'git log -1 --pretty=%an',
returnStdout: true
).trim()
}
}
}
stage('Unit Tests') {
steps {
sh 'mvn test -Punit-tests jacoco:report'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
}
}
}
stage('Integration Tests') {
when {
anyOf {
branch 'main'
branch 'develop'
changeRequest()
}
}
steps {
sh 'mvn verify -Pintegration-tests -DskipUnitTests'
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh """
mvn sonar:sonar
-Dsonar.projectKey=${SONAR_PROJECT_KEY}
-Dsonar.branch.name=${BRANCH_NAME}
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
-Dsonar.exclusions='**/generated/**,**/dto/**,**/*Config.java'
-Dsonar.cpd.exclusions='**/test/**'
-Dsonar.qualitygate.wait=true
"""
}
}
}
stage('Quality Gate Check') {
steps {
script {
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "Quality Gate FAILED: ${qg.status}. SonarQube dashboard'unu kontrol edin: ${env.SONAR_HOST_URL}/dashboard?id=${SONAR_PROJECT_KEY}"
}
}
}
}
stage('Package') {
when {
branch 'main'
}
steps {
sh 'mvn package -DskipTests'
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
}
post {
always {
cleanWs()
}
failure {
slackSend(
channel: '#engineering-alerts',
color: 'danger',
message: """
*Build FAILED* - ${env.JOB_NAME}
Branch: ${BRANCH_NAME}
Author: ${env.GIT_AUTHOR}
Commit: ${env.GIT_COMMIT_MSG}
SonarQube: ${env.SONAR_HOST_URL}/dashboard?id=${SONAR_PROJECT_KEY}
Jenkins: ${env.BUILD_URL}
"""
)
}
}
}
Bu pipeline’da birkaç önemli detay var. sonar.exclusions parametresiyle generated dosyaları ve DTO’ları analizden çıkarıyoruz, bunlar genellikle gürültü yaratır. branch.name ile SonarQube’da her branch için ayrı analiz görüyorsunuz, bu özellik developer edition ve üzeri gerektirir ancak community edition’da da main branch analizi çalışır.
Node.js Projesi için SonarQube Konfigürasyonu
Backend Java değil de Node.js ise yapı biraz farklı. Özellikle coverage raporu için Jest kullanıyorsanız:
# sonar-project.properties - proje kökünde oluşturun
sonar.projectKey=nodejs-api
sonar.projectName=NodeJS API Service
sonar.sources=src
sonar.tests=tests
sonar.test.inclusions=**/*.test.js,**/*.spec.js
sonar.exclusions=node_modules/**,coverage/**,dist/**
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.testExecutionReportPaths=test-results/sonar-report.xml
// Jenkinsfile - Node.js projesi
pipeline {
agent {
docker {
image 'node:20-alpine'
}
}
stages {
stage('Install') {
steps {
sh 'npm ci --prefer-offline'
}
}
stage('Test & Coverage') {
steps {
sh '''
npm test --
--coverage
--coverageReporters=lcov
--coverageReporters=text
--testResultsProcessor=jest-sonar-reporter
'''
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
script {
def scannerHome = tool 'SonarScanner'
sh "${scannerHome}/bin/sonar-scanner"
}
}
}
}
stage('Quality Gate') {
steps {
timeout(time: 3, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
}
}
Node.js projesinde sonar-scanner binary’sini Maven plugin yerine kullanıyoruz. jest-sonar-reporter paketi test sonuçlarını SonarQube’un anlayacağı formata dönüştürüyor.
Custom Quality Gate Tanımlama
Default “Sonar way” gate’i çoğu proje için iyidir ama bazı durumlarda kendi kriterlerinizi belirlemeniz gerekebilir. Örneğin legacy bir projeyi SonarQube’a yeni ekliyorsanız sıfırdan %80 coverage beklemek gerçekçi değil.
SonarQube arayüzünden Quality Gate oluşturma:
- Quality Gates > Create
- Name:
Legacy Project Gate - Koşulları ekleyin:
Yeni kod için önerilen minimum koşullar:
- Coverage on New Code: %70’in altında hata ver
- Duplicated Lines (%) on New Code: %5’in üzerinde hata ver
- Maintainability Rating on New Code: A’dan kötü ise hata ver
- Reliability Rating on New Code: A’dan kötü ise hata ver
- Security Rating on New Code: A’dan kötü ise hata ver
- Security Hotspots Reviewed on New Code: %100 altında uyarı ver
Bu yaklaşım “new code” odaklı çalışıyor. Eski kodun sorunlarından değil, yeni yazdığınız kodun kalitesinden sorumlu tutuyorsunuz ekibi. Bu psikolojik olarak da çok daha kabul edilebilir ve pratik.
Projeyi bu gate ile ilişkilendirmek için:
- Project Settings > Quality Gate > ilgili gate’i seçin
Webhook Konfigürasyonu
waitForQualityGate adımının çalışabilmesi için SonarQube’un Jenkins’e analiz sonucunu bildirmesi gerekiyor. Bu webhook üzerinden oluyor.
SonarQube’da webhook ayarı:
- Administration > Configuration > Webhooks > Create
- Name:
Jenkins - URL:
http://jenkins:8080/sonarqube-webhook/ - Secret: Opsiyonel ama production’da mutlaka kullanın
# Webhook URL formatları
# Jenkins cloud agent kullanıyorsanız:
http://jenkins.internal:8080/sonarqube-webhook/
# Reverse proxy arkasındaysanız:
https://jenkins.company.com/sonarqube-webhook/
# Docker network içindeyse container adı ile:
http://jenkins:8080/sonarqube-webhook/
Webhook çalışıp çalışmadığını test etmek için SonarQube’da “Test” butonunu kullanabilirsiniz. Jenkins loglarında şunu görmeniz gerekiyor:
# Jenkins logunda beklenen mesaj
[SonarQube] SonarQube task 'xxx' status is 'SUCCESS'
[SonarQube] Quality gate status: OK
Sorun Giderme: Sık Karşılaşılan Hatalar
Birkaç yaygın sorunu ve çözümlerini paylaşayım.
Problem: “ANALYSIS FAILED – Unable to parse properties”
# Hata mesajı
ERROR: Error during SonarQube Scanner execution
ERROR: ANALYSIS FAILED
# Çözüm: sonar-project.properties encoding'ini kontrol edin
file -i sonar-project.properties
# UTF-8 olmalı, BOM olmadan
# Ya da Jenkinsfile'da explicit olarak belirtin
-Dproject.settings=./sonar-project.properties
Problem: Coverage raporu bulunamıyor
# Jacoco XML raporunun nerede olduğunu bulun
find . -name "jacoco.xml" -type f
# Maven'da jacoco plugin konfigürasyonu (pom.xml)
# verify phase'de report hedefinin çalıştığından emin olun
mvn verify sonar:sonar
# Değil sadece:
mvn sonar:sonar # Bu coverage raporunu üretmez!
Problem: Quality Gate timeout
# Webhook çalışmıyorsa polling ile bekleyebilirsiniz
stage('Quality Gate') {
steps {
timeout(time: 10, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true,
credentialsId: 'sonarqube-token'
}
}
}
Problem: “Project not found” hatası
SonarQube’da proje key’i büyük/küçük harfe duyarlı. Pipeline’daki sonar.projectKey ile SonarQube’daki proje key’inin birebir eşleşmesi gerekiyor.
Problem: Branch analizi community edition’da çalışmıyor
Community edition’da branch plugin ücretsiz kullanılamıyor. Alternatif olarak:
# PR analizi için farklı proje key kullanabilirsiniz
-Dsonar.projectKey=myapp-backend-pr-${env.CHANGE_ID}
Bu yöntem pek temiz değil ama community edition kısıtlamalarını aşmanın pratik bir yolu.
Metrik Takibi ve Raporlama
SonarQube’un en güçlü yanlarından biri zaman içindeki trend takibi. Ekibinize periyodik raporlar göndermek istiyorsanız:
#!/bin/bash
# sonar-report.sh - Haftalık kalite raporu scripti
SONAR_URL="http://sonarqube:9000"
SONAR_TOKEN="your-token-here"
PROJECT_KEY="myapp-backend"
# Ana metrikler
METRICS="bugs,vulnerabilities,code_smells,coverage,duplicated_lines_density,ncloc,sqale_index"
RESULT=$(curl -s -u "${SONAR_TOKEN}:"
"${SONAR_URL}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}")
echo "=== SonarQube Haftalık Kalite Raporu ==="
echo "Proje: ${PROJECT_KEY}"
echo "Tarih: $(date)"
echo ""
echo $RESULT | python3 -c "
import sys, json
data = json.load(sys.stdin)
measures = data['component']['measures']
for m in measures:
print(f'{m["metric"]}: {m["value"]}')
"
Jenkins’te bu scripti haftalık çalıştırıp Slack’e ya da email’e gönderebilirsiniz.
Güvenlik Açığı Tespiti: SAST Perspektifi
SonarQube aynı zamanda basit bir SAST (Static Application Security Testing) aracı olarak da çalışıyor. SQL injection, XSS, hardcoded credentials gibi yaygın güvenlik açıklarını tespit ediyor.
Pipeline’a güvenlik odaklı bir stage eklemek istiyorsanız:
stage('Security Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh """
mvn sonar:sonar
-Dsonar.projectKey=${SONAR_PROJECT_KEY}
-Dsonar.security.hotspots.review.enabled=true
-Dsonar.issue.severity.filter=BLOCKER,CRITICAL
"""
}
}
post {
always {
script {
def qg = waitForQualityGate()
// Sadece güvenlik konularında blocker varsa fail et
if (qg.status == 'ERROR') {
unstable('Guvenlik uyarilari mevcut, kontrol edin!')
}
}
}
}
}
Burada abortPipeline: true yerine unstable() kullanmak bazen daha uygun olabiliyor. Pipeline fail etmek yerine “unstable” işaretliyor, build devam ediyor ama ekip uyarılıyor.
Sonuç
SonarQube ve Jenkins entegrasyonu ilk bakışta fazla adım gerektiriyor gibi görünüyor: token oluştur, credential ekle, webhook konfigüre et, Jenkinsfile yaz. Ama bir kez kurduğunuzda neredeyse sıfır bakım istiyor ve ekibe muazzam bir değer katıyor.
Uyguladıktan sonra gördüğünüz somut faydalar şunlar olacak: code review sürecinde “bu kod kötü yazılmış” tartışmaları azalıyor çünkü SonarQube zaten söylüyor. Yeni geliştiriciler projeye katıldığında kalite standartlarını pipeline üzerinden öğreniyor. Güvenlik açıkları production’a çıkmadan yakalanıyor. Ve belki en önemlisi, takım olarak teknik borcu görsel hale getirip önceliklendirmeye başlıyorsunuz.
Başlangıç için birkaç pratik öneri: Eğer legacy projeniz varsa önce Quality Gate’i gevşek tutun, “new code” odaklı çalışın. Sıfırdan başlıyorsanız default “Sonar way” gate’i kullanın. Coverage’ı coverage uğruna değil, gerçekten test edilmesi gereken kritik kodlar için zorunlu tutun. Ve son olarak SonarQube dashboard URL’ini her ekip üyesinin bookmark’ında olduğundan emin olun, görünür olmayan şeyler iyileştirilmiyor.
