Chuỗi bài về Jenkins

1. Pipeline trong jenkins

2. Hướng dẫn tạo Jenkinsfile

3. Pipeline CI/CD hoàn chỉnh với Laravel Framework

Mục đích cuối cùng của chuỗi bài: xây dựng 1 CI/CD hoàn chỉnh bao gồm:

  • Docker: sử dụng nền tảng container để triển khai
  • Laravel: framework PHP để làm website
  • Unit testing: Unit test cho PHP
  • Feature testing: test chức năng cho service
  • Deploy: CD deploy container bằng Pipeline

Mở đầu

bài trước ta đã làm quen với Pipeline trong Jenkins, tuy nhiên chỉ là cách thiết lập cực kỳ đơn giản, bài viết này sẽ giải thích cách tạo ra file cấu hình Pipeline là Jenkinsfile.

Jenkinsfile

Pipeline as a code: thiết lập pipeline như là lập trình bằng script vậy. Ở trong bài viết trước mình đã giới thiệu có 2 cách để tạo file Jenkinsfile là file để thiết lập Pipeline cho project dạng Pipeline:

  1. WebUI: thiết lập trực tiếp trên WebUI của project Pipeline.
  2. Jenkinsfile trong git repo: thiết lập Pipeline trong file Jenkinsfile của project.
    Về script Pipeline Jenkinsfile thì có 2 loại:
    • Declarative Pipeline(mới có): sử dụng những syntax đơn giản hơn. 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)
    • Scripted Pipeline: top of the underlying Pipeline sub-system. 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.

Ví dụ script Jenkinsfile:

Declarative Pipeline Scripted Pipeline
pipeline {
  agent {
      docker { image 'node:7-alpine' }
  }
  stages {
      stage('Test') {
          steps {
              sh 'node --version'
          }
      }
  }
}
node {
  stage('Example') {
      if (env.BRANCH_NAME == 'master') {
          echo 'I only execute on the master branch'
      } else {
          echo 'I execute elsewhere'
      }
  }
}

Một vài so sánh giữa syntax của Declarative Pipeline và Scripted Pipeline

Declarative Pipeline Scripted Pipeline
Block ngoài cùng là pipeline {...} Block ngoài cùng ko có quy định bắt buộc, nhưng thường là cùng là node {...}
Bắt buộc phải có block stages { ... } cha để khai báo các stage { ... } con Không bắt buộc
Các Groovy khi muốn sử dụng phải add vào trong block scrip { ... } Có thể viết bất kì đâu
Mục đích: sử dụng các methods/functions định nghĩa sẵn để viết script đơn giản nhất Mục đích: có thể sử dụng code Groovy thoải mái, áp dụng cho viết các script có logic phức tạp hơn, mang lại cho ta sự linh hoạt hơn


***

Hướng dẫn qua Syntax của Jenkinsfile

Cấu tạo Jenkinsfile sẽ gồm những thành phần sau

Declarative Pipeline

  • Sections: Các phân đoạn, phạm vi thực thi, bao gồm các block sau
    • agent : chỉ định môi trường sẽ thực thi các thao tác trong steps
    • post : các action sẽ được chạy sau cùng, ví dụ gửi mail thông báo kết quả, xóa các resource sử dụng,...
    • stages : là block cha của block stage, block stage chứa khâu, giai đoạn trong Pipeline (là Pipe trong Pipeline)
    • steps : là block con bên trong block stage là các action thực thi các công việc cần thiết cho mỗi state

Tìm hiểu thêm tại link sau

  • Directives: Nhóm các khai báo chỉ đạo, điều hướng thực thi
    • environment : được khai báo trong block pipeline hoặc stage, là 1 chuỗi các cặp Key-Val định nghĩa các biến môi trường cho toàn bộ (global - nếu để tại scope pipeline) hoặc cho 1 stage riêng ( nếu được khai báo trong block stage đấy ). Có hàm credentials() để lấy thông tin xác thực nhạy cảm (biến định nghĩa ra sẽ ko thể xem được raw text của nó mà chỉ thấy được **** nhưng có thể sử dụng để xác thực được. Ví dụ
pipeline {
    agent any
    environment { // Định nghĩa global
        DB_ENGINE    = 'sqlite'
    }
    stages {
        stage('Build') {
            environment { // Định nghĩa riêng cho stage Build thôi
                AN_ACCESS_KEY = credentials('longta-hoho') 
            }
            steps {
                //sh 'printenv'
                echo "DB Engige : ${DB_ENGINE}"
                echo "DB Engige : ${AN_ACCESS_KEY_USR}"
            }
        }
    }
}

Giải thích: Jenkinsfile trên định nghĩa biến global DB_ENGINE và biến local AN_ACCESS_KEY cho riêng stage Build, trong stage Build sẽ in ra nội dung của 2 biến đó.
Lúc chạy Pipeline trên ta sẽ được kết quả như sau nếu xem ở Console Log của lượt build đó (xem ở dưới sẽ thấy nội dung biến AN_ACCESS_KEY_USR đã bị che mờ đi)

[Pipeline] echo
DB Engige : sqlite
[Pipeline] echo
DB Engige : ****

Lưu ý: : Đối với Credentials là dạng "Standard username and password" thì sẽ có 2 biến được định nghĩa là MYVARNAME_USRMYVARNAME_PSW

* `options` : Chỉ được đặt trong block `pipeline`, định nghĩa các option config cho Pipeline, bao gồm các option được hỗ trợ sẵn và các options của các Plugin được cài đặt thêm. Ví dụ như `buildDiscarder` là được Pipeline hỗ trợ sẵn còn `timestamps` là option của Plugin.
* `parameters` :  Chỉ được đặt trong block `pipeline`, định nghĩa các parameters mà user phải cung cấp để chạy Pipeline. Ví dụ
pipeline {
    agent any
    parameters {
        string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
    }
    stages {
        stage('Example') {
            steps {
                echo "Hello ${params.PERSON}" // chỗ này sẽ in ra console là "Hello anh Long" nếu ta truyền biến PERSON=anh Long vào
            }
        }
    }
}

Giải thích: định nghĩa param tên PERSON với default value là Mr Jenkins, lúc ở WebUI của Jenkins ta chọn build Pipeline này thì sẽ hiện ra form input nhập các param đã được định nghĩa, xem hình dưới đây:

* triggers : Chỉ được đặt trong block pipeline, định nghĩa Pipeline được trigger để chạy như thế nào, hỗ trợ 3 kiểu là
* cron: chạy Pipeline định kỳ, cú pháp là cú pháp của cron linux ( 0 * * * * : là chạy mỗi giờ đúng chẳng hạn)
* pollSCM: định lỳ check source trên repo xem có j thay đổi ko, ví dụ triggers { pollSCM('H */4 * * 1-5') } là kiểm tra vào phút 0,15,30,45 mỗi giờ từ thứ 2->6
* upstream: được trigger chạy Pipeline khi 1 project khác được build xong. Ví dụ triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS) } là khi 1 trong 2 job1,job2 chạy xong mà thành công thì sẽ chạy Pipeline.
* stage : block con của block stages chứa block steps là các action, thực thi sẽ thực hiện trong stage đó.
* tools : được đặt trong block pipeline hoặc block stage, định nghĩa các tool (version) được chạy trong Pipeline hay stage và khai báo đường dẫn chạy binary vào biến môi trường PATH, tools sẽ ko sử dụng được khi ta khai báo agent none. Các tool được hỗ trợ sẵn là maven, jdkgradle. Các tool này sẽ được tự động cài đặt bởi Jenkins. Ví dụ

pipeline {
    agent any
    tools {
        maven 'apache-maven-3.0.1' // khai báo sử dụng maven 3.0.1
    }
    stages {
        stage('Example') {
            steps {
                sh 'mvn --version' // kiểm tra version của maven
            }
        }
    }
}
* `when` : chỉ được đặt trong block `stage`, định nghĩa điều kiện (condition) để thực hiện các thực thi stage đó, có thể có 1 hoặc nhiều điều kiện, với nhiều điều kiện thì phải thỏa mãn tất cả . Hỗ trợ các điều liện lồng nhau (nested) bằng các cú pháp `not`- ko thỏa mãn điều kiện , `allOf` - thỏa mãn tất cả điều kiện, và `anyOf` - thỏa mãn 1 trong các điều kiện. Ví dụ:
pipeline {
    agent any
    environment { 
        DEPLOY_CON_1 = 'condition1'
        DEPLOY_CON_2 = 'condition2'
    }
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when { // phải thỏa mãn cả 2 điều kiện mới chạy stage này
                allOf {
                    environment name: 'DEPLOY_CON_1', value: 'condition1'
                    environment name: 'DEPLOY_CON_2', value: 'condition2'
                }
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

Giải thích: Jenkinsfile trên định nghĩa 2 biến môi trường là DEPLOY_CON_1 và DEPLOY_CON_2, trong stage Example Deploy ta khai báo 1 condition allOf(phải thỏa mãn tất cả điều kiện) của 2 biến môi trường đó thì mới thực hiện chạy stage này.
* Các điều kiện hỗ trợ là branch, environment, expression

  • Parallel: được đặt trong block stages, thiết lập các stage được chạy đồng thời với nhau, ta có thể thiết lập failFast true sẽ cho Jenkins biết là nếu có 1 stage fail thì nó sẽ hủy việc chạy các stage khác nếu các stage khác chưa kết thúc. Ví dụ:
pipeline {
    agent any
    stages {
        stage('Non-Parallel Stage') {
            steps {
                echo 'This stage will be executed first.'
            }
        }
        stage('Parallel Stage') {
            failFast true
            parallel {
                stage('Stage-Parallel 01') {
                    steps {
                        echo "Stage-Parallel 01"
                    }
                }
                stage('Stage-Parallel 02') {
                    steps {
                        echo "Stage-Parallel 02"
                    }
                }
            }
        }
    }
}

Nếu sử dụng Plugin Blue Ocean ta có thể xem trực quan các stage được chạy như thế nào trong hình dưới đây ( stage Stage-Parallel 01Stage-Parallel 02 đã được chạy song song)

  • Steps: các lệnh thực thi được liệt kê tại link

Sections : giải thích thêm một chút về Sections
các block trong Sections là các block lồng nhau dạng như sau

pipeline{
    agent ...
    // quan trọng nhất, là khai báo các công đoạn trong quá trính CI/CD
    stages {
        stage ('stage_name') {
        agent {...} // chỉ có khi agent ngoài chùng thiết lập là none 
            steps {
            ...
            }
        {
    }
    // những action, xử lý chạy sau cùng
    post {
    ...
    }
}

Chú ý: Về Syntax của Jenkinsfile các bạn có thể xem thêm chi tiết hơn tại link

Scripted Pipeline

Scripted sử dụng những khối block sau:

  • node : giống với agent trong Declarative
  • stage : giống vs stage trong Declarative
  • label : giống vs label trong Declarative

Trong Scripted Pipeline ta có thể sử dụng thoải mái Groovy cũng như thiết lập logic 1 cách linh hoạt hơn. Trong Scripted Pipeline ta chủ yếu sử dụng các module Groovy được cung cắp sẵn hoặc thông qua các Plugin để khai báo các nội dung thực thi của Pipeline. Ví dụ:

node {
    stage('Example') {
        if (env.BRANCH_NAME == 'master') {
            echo 'I only execute on the master branch'
        } else {
            echo 'I execute elsewhere'
        }
    }
}

Ví dụ trên sử dụng if-else condition trong Groovy để điều khiển logic thực thi


Jenkinsfile với docker

Theo sát sự phát triển bùng nổ của docker (container thay cho VM), Jenkins cũng hỗ trợ rất nhiều cho việc sử dụng docker trên Pipeline, Pipeline trên Jenkins hỗ trợ build inside (thiết lập agent trong Declarative Pipeline, hoặc sử dụng phương thức docker.image('xxx').inside { ... } trong Scripted Pipeline) docker container, hỗ trợ build image bằng Dockerfile, hỗ trợ tương tác với remote docker daemon,...

Thực hiện build inside docker container

Bằng việc thiết lập agent trong Declarative Pipeline, hoặc sử dụng phương thức docker.image('xxx').inside { ... } trong Scripted Pipeline ta có thể thực thi trên các docker container thay vì server thực tế. Ví dụ

  • Với Declarative Pipeline:
pipeline {
    agent {
        docker { image 'node:7-alpine' }
    }
    stages {
        stage('Test') {
            steps {
                sh 'node --version'
            }
        }
    }
}
  • Với Scripted Pipeline:
node {
    /* Requires the Docker Pipeline plugin to be installed */
    docker.image('node:7-alpine').inside {
        stage('Test') {
            sh 'node --version'
        }
    }
}

Dockerfile

Ta có thể sử dụng luôn Dockerfile để build image rồi thực thi trên container được tạo bởi image đó. Lúc sử dụng Dockerfile ta thiết lập agent { dockerfile true }. Ví dụ:

pipeline {
    agent { dockerfile true }
    stages {
        stage('Test') {
            steps {
                sh 'node --version'
                sh 'svn --version'
            }
        }
    }
}

Build docker image và push lên registry

Ta có thể khai báo trong Jenkinsfile để build docker image và push lên registry như sau:

node {
    checkout scm
    def customImage = docker.build("my-image:${env.BUILD_ID}")
    customImage.push()

    customImage.push('latest')
}

Remote docker daemon

Mặc định thì Jenkins sẽ tương tác với docker daemon trên local ( máy cài Jenkins ), nếu ta muốn tương tác với remote docker daemon ( docker daemon chạy trên máy khác với máy Jenkins ), ví dụ như tương tác vs swarm cluster chẳng hạn. Ta có thể sử dụng method withServer trong Scripted Pipeline để khai báo tương tác vs remote docker daemon. Lưu ý là có thể phải khai báo Credentials ID của Docker Server Certificate Authentication được config trước trong Jenkins. Ví dụ

node {
    checkout scm

    docker.withServer('tcp://swarm.example.com:2376', 'swarm-certs') {
        docker.image('mysql:5').withRun('-p 3306:3306') {
            ...
        }
    }
}

Sử dụng docker custom registry

Custom registry là ko phải docker hub -> ta phải khai báo registry URL và có thể là Credentials ID của registry đó nếu cần. Ví dụ

node {
    checkout scm

    docker.withRegistry('https://registry.example.com') {

        docker.image('my-custom-image').inside {
            sh 'make test'
        }
    }
}

Với registry yêu cầu authen thì ta khai báo như sau docker.image('my-custom-image', 'credentials-id').inside {...}


Chú ý: Xem thêm về sử dụng docker trong Pipeline tại đây

Kết luận

  • Bài viết hy vọng sẽ giúp cho các bạn hiểu được các thành phần trong quá trình tạo file Jenkinsfile, hiểu được sẽ giúp ta hình dung được giải pháp trong quá trình triển khai thực tế.
  • Có 2 kiểu Jenkinsfile là DeclarativeScripted, ta tùy vào tình trạng của project để lựa chọn cho phù hợp.
  • Bài viết cũng cung cấp cách thao tác Pipeline với docker container, hy vọng bạn đọc thấy hấp dẫn.