Khi các pipeline vận hành cho website trên Kubernetes đã chạy ổn trong GitLab CI, câu hỏi tiếp theo thường không phải là "viết lại bằng công cụ gì", mà là "làm sao để người vận hành chạy chúng an toàn, không gõ tay vài chục biến". Bài này mô tả mô hình ghép Jenkins và GitLab CI mà chúng tôi dùng cho Website AI: Jenkins làm form, GitLab CI chạy việc. Jenkins không cần cài kubectl hay helm, không giữ kubeconfig, chỉ nhận tham số, gọi API để chạy pipeline rồi chờ kết quả.
1. Khi nào nên ghép Jenkins và GitLab CI
Mô hình này hợp với đội đang có sẵn hai thứ:
- Logic vận hành đã nằm trong GitLab CI. Ví dụ mô hình mỗi thao tác một pipeline như bài Mỗi branch một pipeline vận hành: tự động hoá bằng GitLab CI: tạo namespace, chạy
helm upgrade --install, tạo database, commit values vào repo chart. Runner đã có image chứa kubectl và Helm. - Người vận hành đã quen làm việc qua Jenkins. Như bài Jenkins làm "control plane" cho hàng trăm website, mỗi thao tác là một job có form, có phân quyền theo vai trò và báo kết quả qua chat.
Nếu thiếu lớp form, người làm phải mở trang chạy pipeline của GitLab hoặc dùng một bộ request API soạn sẵn, điền tay domain, máy database, đường dẫn lưu trữ, node pool. Chỉ cần gõ sai một biến là pipeline chạy sai. Ghép Jenkins và GitLab CI giữ nguyên toàn bộ logic ở GitLab, chỉ thay lớp nhập liệu bằng form có dropdown, giá trị mặc định và kiểm tra đầu vào. Đây cũng là mẫu "trigger qua API, rồi poll trạng thái" đã dùng ở bài Cầu nối Bitbucket và GitLab CI bằng API và polling.
2. Mô hình Jenkins và GitLab CI: Jenkins làm form, GitLab chạy việc
Người vận hành
│ Build with Parameters (domain, loại site, máy DB...)
▼
Jenkins (không có kubectl/helm, không giữ kubeconfig)
│ 1. kiểm tra và chuẩn hoá tham số
│ 2. POST /api/v4/projects/:id/trigger/pipeline (ref + variables)
│ 3. GET /api/v4/projects/:id/pipelines/:pipeline_id (poll định kỳ)
▼
GitLab CI (pipeline tương ứng với thao tác)
│ đọc kubeconfig từ biến CI protected/masked
│ tạo database, tạo thư mục lưu trữ, helm upgrade --install
│ commit values vào repo chart
▼
Cluster KubernetesPhân vai giữa Jenkins và GitLab CI như sau:
- Jenkins: form, kiểm tra đầu vào, phân quyền ai được chạy thao tác nào, gửi thông báo, lưu lịch sử ai chạy gì.
- GitLab CI: mọi thứ chạm vào cluster và database. Bí mật hạ tầng chỉ nằm ở đây.
- Hợp đồng giữa Jenkins và GitLab CI: danh sách biến mỗi pipeline nhận, kiểu dữ liệu và giá trị hợp lệ. Hợp đồng này nên được ghi lại như tài liệu API.
Các thao tác có thể chia thành vài nhóm chức năng: vòng đời site (tạo, đổi domain, clone, gỡ), dữ liệu (sao lưu, khôi phục, xuất và nhập database, đổi máy database) và tác vụ ứng dụng (chạy lệnh artisan theo preset). Mỗi nhóm có thời gian chờ khác nhau: tạo site thường xong trong vài phút, còn sao lưu một site nhiều file có thể mất cả giờ.
3. Trigger và poll giữa Jenkins và GitLab CI
Hai hàm dưới đây là toàn bộ phần cầu nối giữa Jenkins và GitLab CI. Nên đặt chúng trong Jenkins Shared Library để mọi job gọi chung, thay vì chép vào từng job.
// vars/gitlabPipeline.groovy (Shared Library)
def trigger(String ref, Map vars) {
withCredentials([string(credentialsId: 'gitlab-trigger', variable: 'TRIGGER_TOKEN')]) {
def form = vars.collect { k, v -> "--form-string 'variables[${k}]=${v}'" }.join(' ')
def code = sh(returnStdout: true, script: """
curl -s -o resp.json -w '%{http_code}' -X POST \\
--form-string "token=\$TRIGGER_TOKEN" --form-string "ref=${ref}" ${form} \\
"\$GITLAB_API/projects/\$PROJECT_ID/trigger/pipeline"
""").trim()
if (code != '201') { error("Trigger lỗi HTTP ${code} cho ref ${ref}") }
return readJSON(file: 'resp.json').id
}
}
def waitFor(def id, int timeoutMinutes, int intervalSec = 15) {
def deadline = System.currentTimeMillis() + timeoutMinutes * 60000L
def status = 'created'
while (System.currentTimeMillis() < deadline) {
status = fetchStatus(id) // GET /pipelines/:id, header PRIVATE-TOKEN (token chỉ đọc)
if (status in ['success', 'failed', 'canceled', 'skipped']) break
sleep(time: intervalSec, unit: 'SECONDS')
}
return status
}Một số điểm cần lưu ý khi triển khai cầu nối Jenkins và GitLab CI:
- Dùng trigger token để kích hoạt. Pipeline trigger token chỉ có quyền chạy pipeline. Việc đọc trạng thái dùng một token khác, quyền đọc, gửi qua header
PRIVATE-TOKENnhư tài liệu xác thực REST API. Không đặt token trong query string vì nó dễ lọt vào access log của proxy. - Viết payload ra file hoặc dùng
--form-string. Ghép JSON bằng chuỗi trong Groovy rất dễ vỡ khi giá trị có dấu nháy. - Khoảng poll 10–30 giây là đủ cho thao tác vận hành.
- Mỗi job một timeout riêng. Đặt theo thời gian chạy thực tế của pipeline, cộng thêm khoảng dư.
4. Kiểm tra đầu vào ở phía Jenkins
Lợi ích lớn nhất của lớp form là chặn lỗi trước khi nó chạm vào cluster. Mọi kiểm tra nên chạy xong trước khi Jenkins và GitLab CI trao đổi bất cứ thứ gì. Một khung kiểm tra tối thiểu:
def required = ['DOMAIN', 'SITE_TYPE', 'DB_HOST']
required.each { k ->
if (!params[k]?.trim()) { error("Thiếu tham số ${k}") }
}
if (!(params.DOMAIN ==~ /^[a-z0-9.-]+\.[a-z]{2,}$/)) {
error("Domain không hợp lệ: ${params.DOMAIN}")
}
def cmd = PRESETS[params.ACTION]
if (!cmd) { error("Preset không tồn tại: ${params.ACTION}") }Dùng danh sách chọn cố định cho máy database, loại site để người dùng không phải gõ tay. Mật khẩu ngẫu nhiên nên để pipeline GitLab sinh và lưu thẳng vào kho bí mật, không sinh ở Jenkins.
5. Lỗi thường gặp khi ghép Jenkins và GitLab CI
Jenkins hết thời gian chờ trong khi pipeline vẫn chạy
Khi ghép Jenkins và GitLab CI theo kiểu poll, với thao tác dài như sao lưu, Jenkins có thể báo thất bại trong khi GitLab vẫn đang nén và đẩy dữ liệu. "Jenkins báo fail" không có nghĩa là "việc đã fail". Hàm chờ nên trả về trạng thái thay vì tự error(), để job phân biệt được hết thời gian chờ với pipeline thất bại, và in rõ "pipeline vẫn đang chạy, theo dõi tại link".
Biến null thành chữ "null"
bash: line 1: null: command not found
command terminated with exit code 127Khi lựa chọn trên form không khớp preset nào, biến trong Groovy mang giá trị null. Ghép chuỗi xong nó thành chữ null, được gửi sang GitLab và chạy thật trong pod. Kiểm tra rỗng phải làm ở Jenkins trước khi trigger.
Tên tham số lệch hoa thường
Form đặt tên DOMAIN, script đọc params.domain. Groovy phân biệt hoa thường nên script thấy rỗng. Lỗi kiểu này chỉ lộ ra khi chạy nếu script được dán thẳng vào ô cấu hình job. Giữ pipeline trong Jenkinsfile có review, và thống nhất một quy ước đặt tên cho mọi tham số.
Lỗi thật nằm ở GitLab, Jenkins chỉ thấy "failed"
Đây là mặt trái lớn nhất của việc tách Jenkins và GitLab CI thành hai hệ thống. Cách giảm đau: khi pipeline thất bại, Jenkins gọi API lấy danh sách job của pipeline, tìm job lỗi và kéo vài chục dòng cuối của log về console. Thông báo lỗi cũng phải nói đúng thao tác: chép job từ một job khác mà quên sửa thông báo sẽ làm người đọc hiểu sai.
6. Bí mật ở ranh giới giữa hai hệ thống
Ranh giới giữa Jenkins và GitLab CI là nơi bí mật dễ rò nhất. Nguyên tắc chung: bí mật hạ tầng chỉ sống ở GitLab, Jenkins chỉ giữ token để trigger. Cụ thể:
- Không gửi kubeconfig như biến pipeline. Ai xem được biến của pipeline là có quyền vào cluster. Lưu kubeconfig một lần thành biến CI protected, masked trong GitLab, gắn với ServiceAccount chỉ đủ quyền cho việc pipeline cần làm.
- Không in mật khẩu ra console. Jenkins chỉ che giá trị lấy từ Credentials, không che biến tự sinh trong script. Ghi mật khẩu mới vào kho bí mật, console chỉ in đường dẫn tới đó.
- Tách token theo quyền. Một token để trigger, một token chỉ đọc để poll, và rotate định kỳ.
- Hạn chế ai được chạy job nguy hiểm. Thao tác gỡ site hay đổi máy database nên giới hạn theo vai trò, có bước xác nhận.
7. Được và mất khi ghép Jenkins và GitLab CI
| Được | Mất |
|---|---|
| Không cài kubectl, Helm hay kubeconfig lên máy Jenkins | Hai hệ thống, hai nơi xem log |
| Logic vận hành giữ nguyên trong GitLab CI | Poll giữ một executor Jenkins suốt thời gian chờ |
| Form, phân quyền, thông báo của Jenkins dùng lại được | Bí mật phải đi qua ranh giới giữa hai hệ thống |
Với đội vận hành nhỏ đã quen Jenkins, ghép Jenkins và GitLab CI là bước trung gian hợp lý: thay việc gõ tay bằng form mà không viết lại pipeline. Khi số thao tác tăng, có thể cân nhắc gộp về một hệ thống, nhưng chỉ khi pipeline đã ổn định.
8. Checklist cho mô hình Jenkins và GitLab CI
- Hai hàm trigger/poll nằm trong Shared Library, job chỉ gọi lại.
- Trigger bằng trigger token, poll bằng token chỉ đọc qua header, không có token trong URL.
- Mọi tham số bắt buộc được kiểm tra rỗng và định dạng trước khi trigger.
- Timeout đặt riêng cho từng thao tác, phân biệt timeout với pipeline thất bại.
- Khi lỗi, Jenkins kéo log job GitLab bị lỗi về console.
- Kubeconfig chỉ nằm trong biến CI protected của GitLab, gắn ServiceAccount quyền hẹp.
- Console không in mật khẩu hay token sinh ra trong quá trình chạy.
- Pipeline Jenkins nằm trong
Jenkinsfilecó review.
Các bài khác của hai series nằm trong chuyên mục blog kỹ thuật của Sudo.
Bài tiếp theo
ingress-nginx nghỉ hưu: pilot Traefik, Gateway API và rollback. Khi ingress-nginx không còn được vá, chúng tôi chạy thử Traefik và Gateway API song song trên vài site, rồi rút lại để chọn hướng khác.
