Chuỗi bài về 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:
WebUI
: thiết lập trực tiếp trên WebUI của project Pipeline.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 |
---|---|
|
|
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 trongsteps
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 blockstage
, blockstage
chứa khâu, giai đoạn trong Pipeline (là Pipe trong Pipeline)steps
: là block con bên trong blockstage
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 blockpipeline
hoặcstage
, 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 scopepipeline
) hoặc cho 1 stage riêng ( nếu được khai báo trong blockstage
đấy ). Có hàmcredentials()
để 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_USR và MYVARNAME_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, jdk và gradle. 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ậpfailFast 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 01
và Stage-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ớiagent
trong Declarativestage
: giống vsstage
trong Declarativelabel
: giống vslabel
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à Declarative và Scripted, 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.