0

Hướng dẫn Setup Jenkins cho hệ thống Monolith - Bài 13: Thiết lập Quality Gate với SonarQube để phân tích tĩnh mã nguồn Monolith

Ở Bài 11 và 12, Unit Test đã giúp chúng ta kiểm tra xem các hàm xử lý logic có trả về đúng kết quả mong đợi hay không. Tuy nhiên, Unit Test hoàn toàn mù mờ trước những đoạn code "chạy được nhưng viết cực kỳ tệ": các vòng lặp lồng nhau 5-6 cấp, những câu truy vấn SQL ghép chuỗi trực tiếp gây lỗ hổng SQL Injection, biến khai báo thừa thãi, hay hàng trăm dòng code copy-paste giữa các module trong dự án Monolith.

Để quét sạch những "rác ngầm" này trước khi chúng tích tụ thành nợ kỹ thuật (Technical Debt) khổng lồ, chúng ta cần tích hợp công cụ phân tích mã nguồn tĩnh (Static Code Analysis) hàng đầu hiện nay: SonarQube.

1. Tại sao hệ thống Monolith đặc biệt cần SonarQube và cơ chế "Clean as You Code"?

Hệ thống Monolith lâu năm thường có một đặc điểm chung: chứa hàng chục nghìn dòng code cũ (Legacy Code) do nhiều thế hệ lập trình viên để lại. Nếu bạn áp dụng một bộ quy tắc kiểm tra toàn bộ dự án ngay lập tức, SonarQube có thể báo hàng nghìn lỗi, khiến đội ngũ nản lòng và muốn tắt luôn công cụ.

Giải pháp của SonarQube cho các hệ thống Monolith chính là triết lý Clean as You Code (Chỉ kiểm soát gắt gao phần code mới):

  • Overall Code (Toàn bộ mã nguồn): Ghi nhận hiện trạng để lên kế hoạch refactor dần dần.

  • New Code (Mã nguồn vừa mới viết/chỉnh sửa trong nhánh hoặc bản build): Áp dụng chốt chặn Quality Gate khắt khe nhất. Code cũ có thể còn nợ kỹ thuật, nhưng bất kỳ dòng code mới nào đưa vào hệ thống Monolith bắt buộc phải đạt chuẩn: 0 Bugs, 0 Vulnerabilities, Code Smells ở mức A, và Duplications dưới 3%.

2. Dựng nhanh SonarQube Server bằng Docker Compose

Nếu hạ tầng của bạn chưa có sẵn SonarQube Server, bạn có thể dựng nhanh một cụm SonarQube chuẩn Production (đi kèm PostgreSQL) ngay trên máy chủ thông qua file docker-compose-sonarqube.yml:

YAML

version: '3.8'

services:
  sonarqube:
    image: sonarqube:lts-community
    container_name: sonarqube-server
    restart: unless-stopped
    ports:
      - "9000:9000"
    environment:
      - SONAR_JDBC_URL=jdbc:postgresql://sonar-db:5432/sonar
      - SONAR_JDBC_USERNAME=sonar
      - SONAR_JDBC_PASSWORD=sonar_strong_password
    volumes:
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_extensions:/opt/sonarqube/extensions
      - sonarqube_logs:/opt/sonarqube/logs
    depends_on:
      - sonar-db

  sonar-db:
    image: postgres:15-alpine
    container_name: sonarqube-postgres
    restart: unless-stopped
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar_strong_password
      - POSTGRES_DB=sonar
    volumes:
      - postgresql_data:/var/lib/postgresql/data

volumes:
  sonarqube_data:
  sonarqube_extensions:
  sonarqube_logs:
  postgresql_data:

Lưu ý kỹ thuật trên Linux Host: SonarQube sử dụng Elasticsearch bên dưới để đánh chỉ mục mã nguồn, do đó bạn cần tăng giới hạn bộ nhớ ảo vm.max_map_count trên máy chủ Linux trước khi chạy container:

Bash

sudo sysctl -w vm.max_map_count=524288
docker compose -f docker-compose-sonarqube.yml up -d

Sau khi khởi động, truy cập http://<IP_SERVER>:9000 với tài khoản mặc định admin / admin và đổi mật khẩu mới.

3. Cấu hình kết nối hai chiều giữa Jenkins và SonarQube

Để Jenkins gửi mã nguồn sang cho SonarQube quét, chúng ta cần thực hiện 3 bước cấu hình sau:

Bước 1: Tạo Token xác thực trên SonarQube

  1. Trên giao diện SonarQube, vào My Account (góc trên bên phải) -> tab Security.

  2. Tại mục Generate Tokens:

    • Name: jenkins-monolith-scanner

    • Type: Chọn Global Analysis Token (hoặc User Token).

  3. Bấm Generate và copy chuỗi Token vừa sinh ra (ví dụ: sqa_8f9d2a...).

Bước 2: Lưu Token vào Jenkins Credentials

  1. Trên giao diện Jenkins, vào Manage Jenkins -> Credentials -> Global credentials -> Add Credentials.

  2. Chọn Kind: Secret text.

  3. Dán chuỗi Token của SonarQube vào ô Secret, đặt ID là sonarqube-auth-token và bấm Create.

Bước 3: Khai báo SonarQube Server và Scanner Tool trong Jenkins

  1. Đảm bảo bạn đã cài đặt plugin SonarQube Scanner (đã nhắc đến ở Bài 4).

  2. Vào Manage Jenkins -> System, cuộn xuống mục SonarQube servers:

    • Bấm Add SonarQube.

    • Name: SonarQube-Main-Server (Nhớ kỹ tên này để gọi trong Jenkinsfile).

    • Server URL: http://<IP_SONARQUBE_SERVER>:9000

    • Server authentication token: Chọn sonarqube-auth-token vừa tạo. Bấm Save.

  3. Vào Manage Jenkins -> Tools, cuộn xuống mục SonarQube Scanner installations:

    • Bấm Add SonarQube Scanner.

    • Name: SonarScanner-CLI

    • Tích chọn Install automatically (Chọn phiên bản mới nhất từ Maven Central) và bấm Save.

4. Tạo tệp cấu hình sonar-project.properties cho dự án Monolith

Với một dự án Monolith đồ sộ, nếu bạn để SonarQube quét toàn bộ thư mục gốc, nó sẽ quét nhầm cả thư mục vendor/, node_modules/, các tệp log, hoặc code sinh tự động (generated code), gây treo bộ nhớ và sai lệch kết quả nghiêm trọng.

Hãy tạo tệp sonar-project.properties đặt ngay tại thư mục gốc của mã nguồn Monolith (cùng cấp với Jenkinsfile):

Properties

# Định danh duy nhất của dự án trên SonarQube
sonar.projectKey=monolith-core-system
sonar.projectName=Monolith Core System

# Mã hóa ký tự chuẩn
sonar.sourceEncoding=UTF-8

# Chỉ định các thư mục chứa code chính của Monolith (Ví dụ cho Laravel / Golang / Node.js)
sonar.sources=app,routes,config,src

# Chỉ định thư mục chứa Unit Test (Để SonarQube không áp luật code chuẩn Production lên code Test)
sonar.tests=tests

# CỰC KỲ QUAN TRỌNG VỚI MONOLITH: Loại trừ các thư mục thư viện, cache, migration, tệp tĩnh
sonar.exclusions=**/vendor/**,**/node_modules/**,**/storage/**,**/public/**,**/database/migrations/**,**/*.min.js

# Đường dẫn tới báo cáo kết quả Unit Test (tạo ra từ Bài 11)
sonar.junit.reportPaths=reports/junit.xml

5. Tích hợp Stage SonarQube Analysis vào Jenkinsfile

Bây giờ, chúng ta sẽ bổ sung Stage 4. SonarQube Code Analysis vào Jenkinsfile của hệ thống Monolith:

Groovy

pipeline {
    agent any

    options {
        buildDiscarder(logRotator(numToKeepStr: '15'))
        timeout(time: 30, unit: 'MINUTES')
        timestamps()
    }

    environment {
        APP_NAME = "monolith-core-system"
        BUILD_VERSION = "v1.0.${env.BUILD_NUMBER}"
    }

    stages {
        stage('1. Checkout Source Code') {
            steps {
                checkout scm
            }
        }

        stage('2. Install Dependencies') {
            steps {
                sh 'composer install --no-interaction --prefer-dist'
            }
        }

        stage('3. Run Unit Tests') {
            environment {
                APP_ENV = "testing"
                DB_CONNECTION = "sqlite"
                DB_DATABASE = ":memory:"
            }
            steps {
                sh '''
                    mkdir -p reports
                    ./vendor/bin/phpunit --log-junit reports/junit.xml
                '''
            }
            post {
                always {
                    junit testResults: 'reports/junit.xml', allowEmptyResults: false
                }
            }
        }

        stage('4. SonarQube Static Analysis') {
            steps {
                echo "=== Bắt đầu quét mã nguồn tĩnh với SonarQube ==="
                script {
                    // Gọi công cụ SonarScanner-CLI đã cấu hình trong Global Tools
                    def scannerHome = tool 'SonarScanner-CLI'
                    
                    // Bọc trong môi trường SonarQube-Main-Server đã khai báo ở System
                    withSonarQubeEnv('SonarQube-Main-Server') {
                        sh """
                            ${scannerHome}/bin/sonar-scanner \
                            -Dsonar.projectVersion=${BUILD_VERSION}
                        """
                    }
                }
            }
        }

        stage('5. Clean & Package Artifact') {
            when {
                expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
            }
            steps {
                sh '''
                    composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
                    tar --exclude='.git' --exclude='tests' --exclude='reports' -czf ${APP_NAME}-${BUILD_VERSION}.tar.gz .
                '''
            }
        }
    }

    post {
        success {
            archiveArtifacts artifacts: "${APP_NAME}-${BUILD_VERSION}.tar.gz", fingerprint: true
        }
        always {
            sh 'rm -rf reports/ .env'
        }
    }
}

(Mẹo dành cho dự án Java Maven Monolith: Bạn không cần tạo file sonar-project.properties hay gọi SonarScanner-CLI mà chỉ cần chạy trực tiếp lệnh sh 'mvn sonar:sonar' bên trong khối withSonarQubeEnv('SonarQube-Main-Server')).

6. Thiết lập tiêu chuẩn Quality Gate trên giao diện SonarQube

Sau khi chạy Pipeline lần đầu tiên, dự án Monolith Core System sẽ tự động xuất hiện trên Dashboard của SonarQube kèm biểu tượng SonarQube gắn ngay trong trang Jenkins Job.

Tuy nhiên, bạn cần hiểu rõ cơ chế hoạt động lúc này:

  1. Ở bước 4 (sonar-scanner), Jenkins chỉ đóng gói kết quả phân tích và đẩy lên hàng đợi (Background Task) của SonarQube Server. Ngay khi đẩy xong, lệnh sonar-scanner kết thúc và báo thành công, Jenkins chuyển ngay sang bước 5 mà chưa biết kết quả phân tích trên SonarQube là Đạt (PASSED) hay Rớt (FAILED).

  2. Để tùy chỉnh bộ lọc chất lượng trên SonarQube: Truy cập vào mục Quality Gates trên menu đỉnh của SonarQube -> Bấm Create (ví dụ: đặt tên Monolith-Strict-Gate) -> Thêm các điều kiện kiểm soát trên mã nguồn mới (On New Code) hoặc trên toàn bộ mã nguồn, sau đó gán Quality Gate này vào dự án Monolith Core System.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí