Jenkins là gì?
Xem thửTình huống: Jenkins vừa dựng xong, ngày đầu đã có ba chuyện lạ
GDev chọn Jenkins cho pipeline gdev-ci. Ngày đầu tiên có ba chuyện xảy ra. Một: Linh tạo job bằng cách bấm chuột trong giao diện, tuần sau không ai nhớ job đó build thế nào. Hai: ba người bấm Build cùng lúc, hai build đứng chờ dù máy còn rảnh. Ba: Hùng dán mật khẩu registry thẳng vào cấu hình job để "chạy cho nhanh", rồi mật khẩu xuất hiện trong ảnh chụp màn hình gửi lên nhóm chat.
Cả ba chuyện đều có cùng gốc: chưa hiểu Jenkins gồm những phần nào, việc nào do phần nào làm, và cấu hình sống ở đâu. Bài này xây mô hình đó, rồi bạn tự tái hiện chuyện thứ hai và thứ ba trên một Jenkins chạy bằng Docker.
Sau bài này bạn làm được các việc sau:
- Phân biệt controller, agent, executor và label, rồi đoán được vì sao một build phải chờ.
- Chọn đúng loại job (Freestyle, Pipeline, Multibranch) và đọc được một Jenkinsfile Declarative.
- Giải thích vì sao secret nằm trong kho credential chứ không nằm trong Jenkinsfile, và giới hạn của việc che secret trong log.
Bạn cần có sẵn: Docker, và đã đọc bài CI/CD là gì.
Ba mô hình cần nhớ
- Controller điều phối, agent làm việc: controller giữ giao diện, cấu hình, credential và hàng đợi. Việc build thật chạy trên agent, mỗi agent có một số executor, mỗi executor chạy được một việc tại một thời điểm.
- Pipeline là code nằm trong Git: file
Jenkinsfilemô tả các stage, được review và có lịch sử như code ứng dụng. Job cấu hình bằng chuột thì không có những thứ đó. - Credential tách khỏi code: secret sống trong kho credential của Jenkins, Jenkinsfile chỉ nhắc tới nó bằng ID. Ai đọc được repo cũng không đọc được secret.
Chuẩn bị môi trường lab
Cách nhanh nhất để có Jenkins là chạy image chính thức jenkins/jenkins:lts-jdk21. Bài sau sẽ cài Jenkins lên Ubuntu và giải thích kỹ từng bước, ở đây chỉ cần một bản chạy được.
docker run -d --name gdev-jenkins \
-p 8080:8080 -p 50000:50000 \
-v gdev-jenkins-home:/var/jenkins_home \
jenkins/jenkins:lts-jdk21
docker exec gdev-jenkins cat /var/jenkins_home/secrets/initialAdminPassword
Lệnh thứ hai in ra mật khẩu khởi tạo, một chuỗi 32 ký tự. Mở http://localhost:8080, dán mật khẩu, chọn Install suggested plugins, rồi tạo tài khoản quản trị tên gdev-admin. Chờ vài phút cho tới khi vào được trang chủ.
Cổng 8080 là giao diện web. Cổng 50000 để agent kiểu inbound kết nối vào controller, lab này chưa dùng tới. Volume gdev-jenkins-home giữ toàn bộ cấu hình, nên xóa container đi tạo lại thì dữ liệu vẫn còn.
Một bước chuẩn bị nữa: vào Manage Jenkins, chọn Nodes, bấm Built-In Node, rồi Configure. Đặt Number of executors thành 1 và lưu. Ta cố tình để rất ít executor để quan sát hàng đợi.
Mô hình 1: controller, agent, executor, label
| Thành phần | Nó là gì | Việc của nó |
|---|---|---|
| Controller | Tiến trình Jenkins chính, nơi bạn cài Jenkins | Giao diện, đọc Jenkinsfile, giữ credential, xếp hàng đợi, giao việc |
| Built-In Node | Một node nằm ngay trong tiến trình controller | Chạy việc trực tiếp trên controller, tài liệu khuyên tắt bằng cách đặt executor về 0 |
| Agent | Một máy hoặc container chạy một tiến trình Java nhỏ kết nối về controller | Chạy sh, docker build, test... |
| Executor | Một slot trên node, về bản chất là một luồng | Chạy một việc tại một thời điểm |
| Label | Nhãn chữ gắn vào node, ví dụ linux, docker | Để Jenkinsfile chọn đúng loại máy |
Vì sao tách controller và agent? Build chạy code của người khác (test, script). Nếu chạy ngay trên controller thì một build nặng làm treo giao diện, và một đoạn script độc hại đọc được credential và cấu hình của cả hệ thống. Theo tài liệu Jenkins, để controller chỉ điều phối và đưa việc build sang agent là cách làm khuyến nghị. Lab này dùng Built-In Node cho gọn, đừng làm vậy ở môi trường thật.
Một chi tiết hay bị bỏ sót: bản thân việc điều phối một Pipeline chạy nhẹ trên controller và không chiếm executor. Executor chỉ bị chiếm khi pipeline đi vào khối node hoặc agent, tức là khi có việc thật cần chạy.
Hãy dự đoán trước khi làm tiếp: Built-In Node có 1 executor, bạn bấm Build hai lần liên tiếp cho một job chạy 60 giây. Build thứ hai bắt đầu ngay, chờ, hay bị hủy?
Lab 1: tái hiện hàng đợi vì thiếu executor
Tạo job: New Item, tên gdev-hello, chọn Pipeline, rồi dán đoạn script sau vào ô Pipeline (Definition là Pipeline script):
pipeline {
agent any
stages {
stage('Wait') {
steps {
echo "running on ${env.NODE_NAME}"
sh 'sleep 60'
}
}
}
}
Bấm Build Now hai lần cách nhau vài giây, rồi mở console của build #2. Kết quả minh họa (định dạng và thời gian có thể khác chút):
Started by user gdev-admin
[Pipeline] Start of Pipeline
[Pipeline] node
Still waiting to schedule task
Waiting for next available executor
Đọc kết quả:
[Pipeline] nodelà dòng mà Pipeline xin một executor, vìagent anynghĩa là "chạy trên node nào cũng được".Waiting for next available executorcho biết controller đã nhận build, nhưng không có executor trống. Build không bị hủy, nó nằm trong hàng đợi.- Khi build #1 xong sau 60 giây, executor rảnh và build #2 mới chạy, dòng tiếp theo sẽ có dạng sau:
Running on Built-In Node in /var/jenkins_home/workspace/gdev-hello
Câu trả lời cho dự đoán ở trên: build thứ hai được nhận nhưng phải chờ. Cách chữa không phải là bấm lại, mà là thêm executor hoặc thêm agent.
Trong thực tế bạn có nhiều agent với nhãn khác nhau. Dự đoán trước khi bấm: nếu agent có nhãn docker mất kết nối, build cần docker có được giao sang agent linux đang rảnh không?
Đang tải Animation…
Mô hình 2: job là gì, pipeline là gì, Declarative và Scripted khác nhau ra sao
Jenkins có nhiều loại job. Ba loại bạn cần phân biệt:
| Loại job | Cấu hình ở đâu | Hợp khi |
|---|---|---|
| Freestyle | Nhập bằng form trong giao diện | Việc một lần, thử nghiệm nhanh, không cần review |
| Pipeline | Một Jenkinsfile, dán vào job hoặc đọc từ Git | Một nhánh, quy trình nhiều stage cần review |
| Multibranch Pipeline | Jenkinsfile trong từng nhánh hoặc pull request, Jenkins tự quét và tạo job con | Nhiều nhánh, mỗi pull request cần build riêng |
Trước khi xem sơ đồ, hãy tự chọn: với gdev-ci, bạn cần review pipeline trong Git và có nhiều nhánh. Loại nào hợp? Sơ đồ quyết định dưới đây giúp kiểm tra lại câu trả lời.
Đang tải sơ đồ…
Cách đọc sơ đồ: câu hỏi đầu tiên tách việc "chạy rồi bỏ" khỏi việc cần sống lâu. Chuyện đầu tiên của Linh (job bấm chuột, không ai nhớ cấu hình) rơi vào nhánh Freestyle: không có lịch sử thay đổi, không review được. Với gdev-ci, câu trả lời là "có" cho cả hai câu hỏi, nên GDev dùng Multibranch Pipeline ở bài sau.
Pipeline có hai cú pháp. Theo tài liệu chính thức, cả hai đều viết được pipeline hoàn chỉnh, và Jenkinsfile lưu trong Git được coi là cách làm tốt nhất. Cùng một việc "chạy test trên agent có nhãn linux" viết theo hai kiểu:
pipeline {
agent { label 'linux' }
stages {
stage('Test') {
steps {
sh 'node --test'
}
}
}
}
node('linux') {
stage('Test') {
sh 'node --test'
}
}
Khối đầu là Declarative: một cấu trúc cố định (pipeline, agent, stages, steps, và thêm post, when, options, environment). Jenkins kiểm tra cú pháp trước khi chạy bất cứ bước nào, nên lỗi gõ sai hiện ra ngay. Khối sau là Scripted: một node với Groovy đầy đủ, bạn tự viết vòng lặp, try/catch, điều kiện. Linh hoạt hơn nhưng dễ thành chương trình phức tạp khó đọc.
Chương này dùng Declarative. Khi cần một đoạn logic Groovy, Declarative cho phép chèn khối script { } vào trong một step, đó là lối thoát có kiểm soát thay vì chuyển hẳn sang Scripted.
Một ghi chú về giao diện: Blue Ocean từng là giao diện hiện đại cho Pipeline nhưng tài liệu Jenkins hiện ghi nó đã ngừng phát triển tính năng và khuyên dùng plugin Pipeline Graph View. Stage View là một cách xem khác. Bạn không cần Blue Ocean để học chương này.
Mô hình 3: credential nằm trong Jenkins, Jenkinsfile chỉ biết ID
Credential là bí mật mà pipeline cần: mật khẩu registry, token, khóa SSH, file kubeconfig. Jenkins lưu chúng trong kho credential, mã hóa trên đĩa của controller. Mỗi credential có ID do bạn đặt, loại (Kind) và phạm vi (scope):
| Kind | Chứa gì | Ví dụ ở GDev |
|---|---|---|
| Secret text | Một chuỗi bí mật | Token API |
| Username with password | Hai trường tên và mật khẩu | Tài khoản đẩy image lên registry |
| SSH Username with private key | Tên người dùng và khóa riêng | Truy cập máy chủ deploy |
| Secret file | Nội dung một file | File kubeconfig của cluster |
Scope Global cho job dùng được, còn scope System chỉ dành cho chính Jenkins (ví dụ kết nối agent), job không nhìn thấy.
Vì sao Jenkinsfile không được chứa secret? Jenkinsfile nằm trong Git. Mọi người đọc được repo sẽ đọc được secret, và nó còn nằm mãi trong lịch sử commit kể cả khi bạn xóa dòng đó. Đó là chuyện thứ ba của Hùng ở đầu bài.
Cách đúng là Jenkinsfile chỉ nói "lấy credential có ID này":
pipeline {
agent any
environment {
DEMO = credentials('gdev-demo-secret')
}
stages {
stage('Show') {
steps {
sh 'echo "value is $DEMO"'
}
}
}
}
Hàm credentials() đặt biến môi trường DEMO cho pipeline. Với kiểu Username with password, Jenkins còn tạo thêm DEMO_USR và DEMO_PSW. Chú ý lệnh sh dùng dấu nháy đơn: để shell đọc biến, không để Groovy chèn giá trị vào chuỗi. Bài Pipeline sẽ giải thích rủi ro của nháy kép.
Failure lab: Jenkins che secret trong log, nhưng đó không phải bảo mật
Tạo credential: Manage Jenkins, Credentials, domain Global credentials (unrestricted), Add Credentials. Kind chọn Secret text, Secret nhập s3cret-value, ID nhập gdev-demo-secret. Tạo job Pipeline gdev-secret với Jenkinsfile ở trên rồi bấm Build. Kết quả minh họa:
[Pipeline] withEnv
[Pipeline] {
[Pipeline] sh
+ echo value is ****
value is ****
Jenkins thay giá trị bằng **** trong console. Dễ khiến ta nghĩ secret đã an toàn. Giờ sửa step sh thành mã hóa giá trị trước khi in:
sh 'echo "$DEMO" | base64'
czNjcmV0LXZhbHVl
Chuỗi này là s3cret-value mã hóa base64, ai cũng giải ngược được. Triệu chứng: secret hiện rõ trong log dù đã "được che". Nguyên nhân: cơ chế che chỉ tìm và thay đúng chuỗi gốc xuất hiện nguyên văn, còn bất kỳ dạng biến đổi nào cũng lọt. Tài liệu Jenkins cũng nói rõ che log không ngăn được người cố ý lấy credential bằng cách khác.
Cách kiểm chứng: đọc kỹ console sau mỗi thay đổi Jenkinsfile. Cách phòng gồm ba việc. Chỉ cho job tin cậy dùng credential. Review Jenkinsfile của người khác trước khi nó chạy với credential quan trọng, vì ai sửa được Jenkinsfile là có thể in secret ra. Dùng credential có quyền tối thiểu, ví dụ token chỉ đẩy được image chứ không xóa được repo.
Xóa các job và credential lab sau khi xong. Nếu dùng xong Jenkins Docker này, dọn bằng:
docker rm -f gdev-jenkins
docker volume rm gdev-jenkins-home
Lỗi thường gặp
| Triệu chứng | Nguyên nhân hay gặp | Cách kiểm chứng |
|---|---|---|
Build đứng ở Waiting for next available executor | Hết executor, hoặc không có node khớp label | Manage Jenkins, Nodes: xem executor và label từng node |
| Giao diện Jenkins chậm khi có build nặng | Build chạy trên Built-In Node | Đặt executor của Built-In Node về 0, build trên agent |
| Job tạo bằng chuột, không ai biết cấu hình | Dùng Freestyle cho quy trình lâu dài | Chuyển sang Jenkinsfile trong Git |
| Secret xuất hiện trong repo hoặc log | Ghi thẳng vào Jenkinsfile, hoặc in ra dạng biến đổi | Tìm trong lịch sử Git, đọc console |
| Pipeline không tìm thấy credential | Sai ID, sai scope, hoặc credential ở folder khác | So ID trong Jenkinsfile với ID trong kho credential |
| Không phân biệt được Declarative với Scripted | Thấy node nhưng bài viết dùng pipeline | Declarative bắt đầu bằng pipeline {, Scripted bằng node { |
Một quy tắc thực tế: coi controller là tài sản nhạy cảm nhất của hệ thống CI, vì nó giữ mọi credential. Càng ít thứ chạy trực tiếp trên nó càng tốt.
Tự kiểm tra
Hãy trả lời trước khi đọc đáp án ngay bên dưới mỗi câu.
Câu 1. Jenkins có 1 agent nhãn linux với 4 executor, đang rảnh hoàn toàn. Một build ghi agent { label 'windows' } được gửi tới. Nó chạy ở đâu?
Đáp án: Nó không chạy và nằm trong hàng đợi. Controller chỉ giao cho node có label windows, agent linux rảnh cũng không được dùng.
Câu 2. Vì sao đưa mật khẩu vào Jenkinsfile nguy hiểm hơn đưa vào credential store, kể cả khi repo là private?
Đáp án: Mật khẩu ở trong Git sẽ nằm mãi trong lịch sử commit và lan theo mọi bản sao, mọi người đọc được repo đều thấy. Credential store giữ secret trên controller và chỉ nhắc tới nó bằng ID.
Câu 3. Console in **** thay cho secret. Điều đó có chứng minh secret không thể bị lộ qua log không?
Đáp án: Không. Cơ chế che chỉ thay chuỗi nguyên văn, nên một lệnh in dạng đã biến đổi như base64 vẫn làm lộ giá trị.
Nếu chỉ nhớ 3 điều
- Controller điều phối, agent chạy việc; build chờ vì thiếu executor hoặc không có node khớp label.
- Pipeline là Jenkinsfile trong Git, dùng cú pháp Declarative cho hầu hết trường hợp.
- Secret ở kho credential, Jenkinsfile chỉ chứa ID; che log không phải là bảo mật.
Bài tiếp theo là cài Jenkins lên Ubuntu, nơi bạn gặp Java, kho apt và mật khẩu khởi tạo.
Tài liệu chính thức: Pipeline, Using a Jenkinsfile, Managing Nodes và Docker image jenkins/jenkins.