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-integration yazın
  • Type: Global Analysis Token seç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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir