0

Hướng dẫn Setup Jenkins cho hệ thống Monolith - Bài 14: Tự động chặn Pipeline khi mã nguồn vi phạm lỗi bảo mật hoặc code smell nghiêm trọng

Ở Bài 13, chúng ta đã thiết lập thành công lệnh sonar-scanner trong Jenkins để đẩy mã nguồn của hệ thống Monolith lên máy chủ SonarQube phân tích. Tuy nhiên, có một lỗ hổng lớn trong quy trình đó: Ngay khi đẩy dữ liệu lên SonarQube thành công, lệnh sonar-scanner lập tức trả về mã thoát 0 (thành công) và Jenkins cứ thế chạy thẳng sang bước đóng gói Artifact và Deploy mà không hề quan tâm kết quả chấm điểm trên SonarQube là Đạt (Passed) hay Trượt (Failed).

Bài học hôm nay sẽ hướng dẫn bạn cách khép kín vòng lặp kiểm soát chất lượng: Bắt buộc Jenkins phải tạm dừng chờ SonarQube chấm điểm xong thông qua cơ chế Webhook bất đồng bộ, và lập tức bẻ gãy (Abort/Fail) toàn bộ Pipeline nếu phát hiện lỗ hổng bảo mật hoặc code smell vượt ngưỡng cho phép.

1. Bản chất cơ chế Bất đồng bộ (Asynchronous) giữa Jenkins và SonarQube

Tại sao sonar-scanner không chờ lấy kết quả luôn mà lại kết thúc ngay?

  • Đối với một dự án Monolith có hàng trăm nghìn dòng code, khi đẩy báo cáo lên SonarQube Server, báo cáo đó sẽ được đưa vào một hàng đợi xử lý ngầm (Compute Engine Queue). Quá trình phân tích cây cú pháp, đối chiếu lịch sử Git và tính toán nợ kỹ thuật có thể mất từ vài chục giây đến vài phút.

  • Nếu bắt Jenkins Agent giữ kết nối đồng bộ (Synchronous) hoặc liên tục hỏi thăm (Polling) từng giây xem SonarQube chạy xong chưa, tài nguyên CPU/Network sẽ bị lãng phí rất lớn.

Giải pháp chuẩn kiến trúc:

  1. Jenkins chạy xong sonar-scanner sẽ chuyển sang trạng thái Tạm nghỉ (Sleep/Wait) tại Stage Quality Gate.

  2. Khi SonarQube Server xử lý xong tác vụ ngầm, nó sẽ chủ động bắn một HTTP POST Webhook chứa kết quả (OK hoặc ERROR) ngược về cho Jenkins.

  3. Jenkins nhận tín hiệu từ Webhook, thức dậy và quyết định cho phép đi tiếp hay đánh sập Pipeline.

[Jenkins Agent] ──(1. Đẩy báo cáo quét code)──► [SonarQube Server (Compute Engine)]
       │                                                       │
(2. Tạm dừng chờ: waitForQualityGate)                 (3. Phân tích ngầm...)
       │                                                       │
       ▼                                                       ▼
[Jenkins Controller] ◄──(4. Bắn Webhook trả kết quả JSON)──────┘
       │
(5. Nếu Status != OK -> Hủy Pipeline ngay lập tức!)

2. Cấu hình Webhook trên SonarQube Server để gọi ngược về Jenkins

Để SonarQube biết địa chỉ của Jenkins mà trả kết quả, bạn thực hiện theo các bước sau trên giao diện Web của SonarQube:

  1. Đăng nhập vào SonarQube với quyền Admin -> Chọn Administration (trên thanh menu đỉnh) -> Configuration -> Webhooks.

  2. Bấm nút Create ở góc phải màn hình.

  3. Điền các thông tin cấu hình:

    • Name: Jenkins-Monolith-Webhook

    • URL: Nhập địa chỉ Jenkins của bạn kèm hậu tố bắt buộc /sonarqube-webhook/:

      Plaintext

      http://<IP_HOẶC_DOMAIN_JENKINS>:8080/sonarqube-webhook/
      
      

      (Lưu ý cực kỳ quan trọng: Bắt buộc phải có dấu gạch chéo / ở cuối cùng của /sonarqube-webhook/, nếu thiếu dấu / này Webhook sẽ bị lỗi 302/404 và Jenkins sẽ treo mãi mãi).

    • Secret: Có thể để trống (hoặc nhập chuỗi bí mật nếu bạn cấu hình Webhook Secret nâng cao trong Jenkins).

  4. Bấm Create để lưu lại.

Mẹo xử lý lỗi mạng nội bộ (Docker Network): Nếu cả Jenkins và SonarQube của bạn đều đang chạy bằng Docker trên cùng một máy chủ Linux (như Bài 3 và Bài 13), tuyệt đối không điền http://localhost:8080/sonarqube-webhook/ vì bên trong container SonarQube, localhost là chính nó chứ không phải Jenkins. Hãy điền IP mạng LAN của máy chủ (ví dụ [http://192.168.1.50:8080/sonarqube-webhook/](http://192.168.1.50:8080/sonarqube-webhook/)) hoặc nối 2 container vào chung một Docker Network.

3. Thiết lập các quy tắc "Thép" trong Quality Gate cho hệ thống Monolith

Trước khi viết code chặn Pipeline, hãy đảm bảo bộ quy tắc (Quality Gate) trên SonarQube đã được siết chặt để bắt đúng các lỗi chí mạng của hệ thống Monolith.

  1. Trên SonarQube, vào mục Quality Gates -> Chọn Create và đặt tên là Monolith-Production-Gate.

  2. Trong phần Conditions, hãy thiết lập các ngưỡng chặn cho cả On New Code (Code mới viết) và các chỉ số bảo mật cốt lõi:

    • Vulnerabilities (Lỗ hổng bảo mật như SQL Injection, XSS, Hardcoded Password): is greater than 0 -> Chỉ cần xuất hiện 1 lỗi bảo mật mới, lập tức đánh rớt!

    • Security Hotspots Reviewed (Các điểm nhạy cảm bảo mật cần review): is less than 100%.

    • Reliability Rating (Độ tin cậy / Bugs gây Crash hoặc NullPointerException): is worse than A (Tức là có từ 1 Bug trở lên sẽ bị chặn).

    • Maintainability Rating (Code Smell / Độ phức tạp thuật toán quá cao): is worse than A (Ngăn chặn việc dev viết các hàm if-else lồng nhau quá sâu hoặc class phình to hàng nghìn dòng).

    • Duplicated Lines (%) (Tỷ lệ copy-paste code): is greater than 3.0%.

  3. Sau khi tạo xong, bấm vào tab Projects ở bên dưới và gán dự án Monolith Core System vào Quality Gate này.

4. Tích hợp hàm waitForQualityGate vào Jenkinsfile

Trong bộ plugin SonarQube Scanner for Jenkins, chúng ta được cung cấp sẵn hàm waitForQualityGate. Hàm này có tham số vàng là abortPipeline: true — tự động chuyển trạng thái bản build thành FAILURE và dừng mọi bước tiếp theo nếu Quality Gate trả về trạng thái khác OK.

Hãy cập nhật Jenkinsfile của hệ thống Monolith với Stage số 5 hoàn toàn mới:

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 "=== Quét mã nguồn và đẩy dữ liệu lên SonarQube ==="
                script {
                    def scannerHome = tool 'SonarScanner-CLI'
                    withSonarQubeEnv('SonarQube-Main-Server') {
                        sh """
                            ${scannerHome}/bin/sonar-scanner \
                            -Dsonar.projectVersion=${BUILD_VERSION}
                        """
                    }
                }
            }
        }

        // STAGE CHỐT CHẶN MỚI: Chờ kết quả từ Webhook và quyết định số phận Pipeline
        stage('5. Quality Gate Check') {
            steps {
                echo "=== Đang chờ kết quả chấm điểm từ SonarQube Webhook... ==="
                // Giới hạn thời gian chờ tối đa 5 phút để tránh treo Pipeline nếu mạng đứt hoặc Webhook cấu hình sai
                timeout(time: 5, unit: 'MINUTES') {
                    // abortPipeline: true sẽ tự động đánh FAIL bản build nếu Quality Gate bị đỏ (ERROR)
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        stage('6. Clean & Package Artifact') {
            steps {
                echo "=== Mã nguồn đạt chuẩn Quality Gate! Tiến hành đóng gói Artifact ==="
                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
        }
        failure {
            echo "=== CHẶN TRIỂN KHAI: Mã nguồn không vượt qua bài kiểm tra Unit Test hoặc vi phạm tiêu chuẩn Quality Gate của SonarQube! ==="
        }
        always {
            sh 'rm -rf reports/ .env'
        }
    }
}

5. Kỹ thuật nâng cao: Xử lý linh hoạt kết quả Quality Gate bằng Groovy Script

Đôi khi trong các dự án Monolith thực tế, Tech Lead muốn tùy biến hành vi: Nếu nhánh develop bị vi phạm nhẹ thì chỉ chuyển trạng thái sang UNSTABLE (vàng) và gửi cảnh báo nhưng vẫn cho build tiếp lên môi trường Dev; còn nếu nhánh main (Production) bị vi phạm thì mới ABORT (đỏ) hoàn toàn.

Thay vì dùng waitForQualityGate abortPipeline: true cứng nhắc, bạn có thể viết khối script bắt trạng thái trả về như sau:

Groovy

stage('5. Quality Gate Check') {
    steps {
        timeout(time: 5, unit: 'MINUTES') {
            script {
                def qg = waitForQualityGate()
                echo "Trạng thái Quality Gate trả về: ${qg.status}"
                
                if (qg.status != 'OK') {
                    if (env.BRANCH_NAME == 'main' || env.BRANCH_NAME == 'master') {
                        error "❌ CHẶN PRODUCTION: Mã nguồn vi phạm tiêu chuẩn SonarQube (${qg.status})!"
                    } else {
                        unstable "⚠️ CẢNH BÁO: Nhánh ${env.BRANCH_NAME} chưa đạt chuẩn Quality Gate (${qg.status}), cần khắc phục trước khi Merge!"
                    }
                }
            }
        }
    }
}

6. Kiểm thử thực tế (Simulating a Security Breach)

Để kiểm chứng "người gác cổng" này hoạt động hiệu quả ra sao, bạn hãy thử cố tình viết một đoạn code vi phạm bảo mật kinh điển vào dự án Monolith rồi git push:

PHP

// Cố tình ghép chuỗi trực tiếp gây lỗi SQL Injection và hardcode mật khẩu
$awsSecret = "AKIAIOSFODNN7EXAMPLEKEY123456"; 
$users = DB::select("SELECT * FROM users WHERE email = '" . $request->input('email') . "'");

Khi Pipeline chạy đến Stage 5 (Quality Gate Check):

  1. Jenkins sẽ dừng lại khoảng 5–10 giây (Checking status of SonarQube task '...' on server 'SonarQube-Main-Server').

  2. Ngay khi SonarQube phát hiện lỗi SQL Injection và Hardcoded Credential, Webhook bắn về trạng thái ERROR.

  3. Jenkins lập tức báo dòng chữ đỏ rực: Pipeline aborted due to quality gate failure: ERROR.

  4. Stage 6 (Clean & Package Artifact) bị gạch chéo (Skipped), ngăn chặn hoàn toàn việc đoạn code nguy hiểm này có cơ hội đóng gói thành bản build.


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í