Khi xây dựng CI/CD trên jenkins, chúng ta thường sử dụng các plugins cài sẵn hoặc bash/bat script để config build, test, report, deploy vv..

Với Bash/Bat script tiếp cận nó đã khó, sử dụng nó còn khó khăn hơn. Chưa kể với cách truyền thống còn hạn chế về mặt môi trường build / run project , mặc dù Slave Node có thể giải quyết vấn đề môi trường chạy ,nhưng nó cũng chưa thực sự là giải pháp tối ưu. Hoặc không thể sử dụng đa luồng chạy song song, cũng không thể sử dụng nhiều môi trường chạy trong 1 build...vvv

Vì vậy trong bài viết này, mình sẽ giới thiệu Jenkins Pipeline - một workflow CI trên Jenkins 2.0, cái sẽ trợ giúp chúng ta khắc phục những khó khăn đó.

Jenkins Pipeline là gì

Như trang https://jenkins.io/doc/book/pipeline/#overview có trình bày :

Jenkins Pipeline (or simply "Pipeline" with a capital "P") is a suite of plugins which supports implementing and integrating continuous delivery pipelines into Jenkins

Nó sẽ hỗ trợ chúng ta trong việc tạo và cấu hình một hệ thống CI/CD trên Jenkins với concept Pipeline

Jenkins Pipeline dựa trên syntax của DSL (Domain Specific Language), mang đậm phong cách coder. Vì vậy được gọi là Pipeline as Code, ngôn ngữ chính là : Groovy sử dụng Groovy Script

Dưới đây là một ví dụ :

pipeline {
    agent any 

    stages {
        stage('Build') { 
            steps { 
                sh 'make' 
            }
        }
        stage('Test'){
            steps {
                sh 'make check'
                junit 'reports/**/*.xml' 
            }
        }
        stage('Deploy') {
            steps {
                sh 'make publish'
            }
        }
    }
}

stage : Được hiểu như từng chức năng con, trong đó nó sẽ phân rẽ các chức năng cụ thể tùy thuộc vào bài toán của bạn. Chẳng hạn như stage Init, Build , Test, Deploy ..vv

Việc trình bày Pipeline code cũng có 2 cách từ đơn giản đến phức tạp như sau :

1. Declarative Pipeline

(Là ví dụ bên trên) - nó được gọi là Presents a more Simplified. Dựa trên các methods / functions dựng sẵn, việc của chúng ta sử dụng và tuân thủ theo các rule và syntax được định nghĩa sẵn theo các steps và funtions như vậy để implement theo các stages (từng đoạn trong pipeline)

2. Scripted Pipeline

Sử dụng Groovy script là kỹ thuật nâng cao hơn khi cần sử dụng code để implement vài tasks nào đó hoặc logic phức tạp tùy thuộc bài toán.

Một ví dụ đơn giản Scripted Pipeline như sau

def mavenBuild = {
    profile, output ->
        sh('git reset --hard ')
        sh("mvn clean compile exec:java package -P ${profile} -Dskiptests=true")
        sh("mv target/bookstore.war ${output}")
}

if (TOMCAT_USER == 'true') mavenBuild(mavenProfileUser, WAR_USER_BUILD_PATH)
if (TOMCAT_ADMIN == 'true') mavenBuild(mavenProfileAdmin, WAR_ADMIN_BUILD_PATH)
if (TOMCAT_STUDENT == 'true') mavenBuild(mavenProfileStudent, WAR_STUDENT_BUILD_PATH)

Lợi ích Jenkins Pipeline mang lại

So với cách thông thường, sử dụng các plugin build có sẵn phải lệ thuộc theo cách làm việc của plugin đó. Hoặc là viết Bash / Bat script để customize theo project của mình. Thì cá nhân mình thấy nó quá là bó hẹp phạm vi mở rộng, cũng như những gì mà mình đã nói ở phần đầu.

Do đó cá nhân mình đề cao những lợi ích sau mà Jenkins Pipeline mang lại:

  • Sử dụng ngôn ngữ Groovy : mang đến sự thân thiện, dễ dàng implement hơn là sử dụng Bash / Bat Script
  • Hỗ trợ Docker : Tùy biến môi trường phát triển cho từng project riêng biệt.
  • Cấu trúc DSL chặt chẽ : sử dụng cú pháp DSL (Domain Specific Language) thuận tiện , cùng với đó là các methods/functions general của jenkins pipeline mang đến sự implement tối giản hơn.
  • Dễ dàng mở rộng với Shared Libraries : Tự do xây dựng library common để sử dụng trong khi runtime.
  • Hỗ trợ build đa luồng, đa môi trường phân tán trong cùng 1 build

Pipeline Syntax

Như mình đã nói ở trên, chúng ta có 2 cách cấu hình Pipeline code là Declarative PipelineScripted Pipeline . Do đó syntax cũng có sự sắp xếp khác biệt đôi chút.

Declarative Pipeline

pipeline {
    /* insert Declarative Pipeline here */
}

pipeline{ } Đây là top-level khi bắt đầu pipeline code, tất cả các thành phần sections, stages, khai báo docker image , run sh script ..vv đều phải nằm trong cặp thẻ này.

Tiếp theo bên trong cặp pipeline là sub-level của các sections

agent

agent là môi trường để chạy pipeline code, các đoạn shell script vv...

Agent có thể hiểu chính jenkins master, slave machine nào đó, hoặc docker image.

Options :

  • any : Chỉ định cho bất cứ master/slave nào available sẽ được pick để chạy.
  • none : Không sử dụng, thay vì đó các stages sẽ phải tường minh chỉ định agent cho chính nó ( Xem phần stages ở phía dưới )
  • label : tên của agent (master hoặc slave machine) sẽ dụng để thực thi nhiều stages đã khai báo. ( Khác với any ở chỗ - any sẽ tự động pick master/slave machine available)
  • node : Giống như label nhưng có thể thêm nhiều options hơn. Hay nói cách khác option node này là phụ trợ cho label
  • docker : sử dụng docker image cho việc chạy pipeline - https://hub.docker.com/explore/

Ví dụ :

pipeline {
    
    // agent any
    // agent none
    // agent { label 'master' }
    // agent { label 'manhnv-slave' }
    /* agent {
            node {
                label 'manhnv-slave'
                customWorkspace '/path/to/custom/workspace'
            }
        }
    */
    // Hoặc với docker image
    agent { docker 'maven:3-alpine' } 
    stages {
        stage('Example Build') {
            steps {
                sh 'mvn clean compile exec:java package -P staging'
            }
        }
    }
}

post

Tiếp theo sub section của pipelinepost. Cái sẽ được chạy cuối cùng khi kết thúc Pipeline hoặc stage (giống như finally trong Java đó) Nhằm handle kết quả của pipeline hoặc chạy các tác vụ cần chạy sau cùng.

//Một ví dụ từ  https://jenkins.io/doc/pipeline/tour/post/
pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }

    post {
        always {
            echo 'One way or another, I have finished'
            deleteDir() /* clean up our workspace */
        }
        success {
            echo 'I succeeeded!'
        }
        unstable {
            echo 'I am unstable :/'
        }
        failure {
            echo 'I failed :('
        }
        changed {
            echo 'Things were different before...'
        }
    }
}
  • always : luôn luôn được gọi, bất kể kết quả buidl là gì
  • changed : chỉ chạy khi kết quả của lần build này khác với lần build trước
  • success : khi kết quả build SUCCESS
  • unstable : khi kết quả build là UNSTABLE
  • failure : khi kết quả build là FAILURE
  • aborted : khi kết quả build là ABORTED

Class define kết quả build có thể refer tại Java doc - http://javadoc.jenkins-ci.org/hudson/model/Result.html

stages

Lại là một sub section nữa bên trong pipeline, đó là stages.

Một stages có thể hiểu là chứa nhiều stage con. Mỗi stage sẽ đóng một vai trò đảm nhiệm khác nhau tùy bài toán của bạn. Nó giống như việc bạn viết nhiều method trong 1 class vậy

pipeline {
    agent any
    stages { 
        stage('Clone') {
            //implement pipeline code 
        }
        stage('Build') {
            //implement pipeline code 
        }
        stage('Test') {
            //implement pipeline code 
        }
    }
}

P/s : Nếu agent là none bạn phải chỉ định agent tương ứng cho các stage( refer section agent ở mục trên)

steps

steps là một thành phần nằm bên trong stage con. Nơi mà chúng ta đặt các xử lý logic bên trong nó .

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'

                script {
                    def browsers = ['chrome', 'firefox']
                    for (int i = 0; i < browsers.size(); ++i) {
                        echo "Testing the ${browsers[i]} browser"
                    }
                }
            }
        }
    }
}

Scripted Pipeline

Nói lại một chút về Declarative Pipeline ở trên:

Nếu bạn muốn chèn một đoạn groovy script, bạn phải dùng thẻ script {} như ví dụ steps ở bên. Thì Scripted Pipeline không phải làm điều đó, bởi vì nó implement theo cách mang nghĩa Scripted

Giống như Declarative Pipeline, Scripted Pipeline cũng sử dụng cú pháp DSL và build với Groovy. Nhưng nó có thể sử dụng các functions cung cấp bởi Groovy, và "coding" theo cách của một coder

Ở đây là một ví dụ tiếp theo về Scripted Pipeline

node{
    stage("Prepare") {
        // Tạo function validate input param
        def checkParam = {
            val -> ('' == val || val == 'false')
        }
        if (checkParam(TOMCAT_USER) && checkParam(TOMCAT_STUDENT)) {
            error('You must select environment(s) to deploy')
        }

        // clean old build
        sh("rm -rf ${WAR_USER_BUILD_PATH}")
        sh("rm -rf ${WAR_STUDENT_BUILD_PATH}")

        // change dir và clone source vào folder src
        dir('source') { 
            git url: 'https://github.com/path/to/project', branch: 'develop', credentialsId: "${env.GITHUB_ID}"
        }
    }
}

node trong scripted

node là gi ?

  • node chính là agent theo cách hiểu của Declarative Pipeline. Bạn có thể thấy nó ở http://<jenkins-url>/computer/
  • Một node có thể là master / slave machine / hoặc kết hợp node đó với docker
  • Dùng để khai báo môi trường sẽ chạy jenkins job, hoặc chạy các stage vv. ( Giống như agent)

Syntax :

node(<node_name_or_label>) {
    <processor>
}

Trong đó :

  • node_name_or_label : Tên hoặc label của node http://<jenkins-url>/computer/. Nếu để trống, xử lý này sẽ take một node bất kỳ đang available. Giống như agent any của Declarative Pipeline
  • processor : Implement các logic xử lý trong các stage, hoặc shell script ..vv

Ví dụ :

node { // sẽ take một node bất kỳ đang available
    stage('Prepare'){
        //TODO
    }
}

node('manhnv1_slave') { // sẽ chạy trên slave machine manhnv1_slave
    stage('Log1'){
        //TODO
    }
}

node('manhnv2_slave') { // sẽ chạy trên slave machine manhnv2_slave
    stage('Log2'){
        //TODO
    }
}

node('master') { // Hoặc chạy trên jenkins master 
    stage('Log'){
        //TODO
    }
}

Một stage có thể trong một node, 1 node cũng có thể sử dụng trong 1 stage tùy thuộc vào bài toán của bạn.

Agent vs Node

Sau khi đi qua những phần cơ bản về Declarative vs Scripted Pipeline.

Mình cùng compare lại cách sử dụng giữa agent (Declarative) vs node (Scripted) bằng hình ảnh dưới để thấy rõ trong cách sử dụng



Sư khác biệt giữa Declarative vs Scripted Pipeline

DeclarativeScripted
Bắt đầu bằng pipeline {}Không quy định
Khai báo stage con nằm trong section stages {} chaKhông cần stages cha, chỉ cần khai báo các stage
Groovy script phải viết trong section scriptViết groovy script ở bất kỳ đâu - không ràng buộc
Bắt buộc phải có stage trong các xử lýStage là không bắt buộc
Các section phải theo đúng level cha conKhông cố định level , linh động phụ thuộc từng bài toán
Không phải xử lý logic - đơn giản hơn với các section định nghĩa sẵn (section post {} là một ví dụ)Phải xử lý logic, nâng cao hơn - sử dụng script code để kiểm tra tính đúng đắn

Tổng kết

Như vậy, mình đã giới thiệu xong những thành phần cơ bản và cũng là thành phần bắt đầu để xây dựng Jenkins Pipeline Build. Và mình nghĩ đó là những thứ cần thiết cho các bạn sẽ/bắt đầu tìm hiểu Jenkins Pipeline code.

Ngoài những thành phần kể trên, còn rất nhiều thành phần nâng cao khác như sử dụng tools tích hợp, schedule/ trigger job, build condition , build paralle..vv mà không thể giới thiệu hết trong một bài.

Hy vọng qua bài viết sẽ giúp ich được phần nào đó!