Khi cụm có hàng chục Cloud VPS, số việc vận hành lặp lại hằng ngày rất lớn: tạo site, deploy code, đổi domain, backup, gỡ site, chạy một lệnh artisan trên đúng minion. Nếu mỗi việc là một đoạn script nằm trên máy của một người, hoặc một chuỗi lệnh SSH gõ tay, kết quả phụ thuộc vào trí nhớ của người làm. Tài liệu này hướng dẫn dùng Jenkins như một "control plane": một nơi duy nhất để bấm chạy, có tham số, có lịch, có log, và là nơi duy nhất giữ chìa khoá SSH tới mọi máy. Nội dung gồm khái niệm, cách chọn freestyle hay pipeline, cách tổ chức job theo nhóm, kiểm tra tham số, rủi ro và checklist bảo vệ.

1. Jenkins là gì

Jenkins là một automation server mã nguồn mở. Đơn vị làm việc là job: một tập lệnh kèm cấu hình về tham số, lịch chạy, thông báo. Mỗi lần chạy job là một build có số thứ tự, log riêng, trạng thái thành công hay thất bại, và ghi lại ai đã bấm chạy. Lệnh được thực thi trên agent (với cụm nhỏ, chính máy controller cũng có thể là agent). Job chạy theo ba cách: bấm tay, theo lịch cron, hoặc được gọi qua API.

Sức mạnh của Jenkins nằm ở hệ plugin. Với vai trò control plane cho Cloud VPS, chỉ cần vài plugin: SSH Agent để nạp private key vào build, Credentials để giữ key, Slack Notification để báo kết quả, và Pipeline để viết job nhiều bước. Tổng quan kiến trúc có trong Jenkins User Handbook.

2. Freestyle hay Pipeline

Jenkins có hai kiểu job phổ biến, và một control plane thường dùng cả hai:

FreestylePipeline (Declarative)
Cách viếtÔ "Execute shell" trên giao diện, chạy từ trên xuốngGroovy, chia stages, có khối post
Hợp vớiViệc ngắn, một mạch: backup theo lịch, restart dịch vụ, chạy một lệnhViệc nhiều bước cần kiểm tra giữa chừng: tạo site, đổi domain, gỡ site
Xử lý lỗiset -e: lỗi là dừngMỗi stage dừng riêng, post { failure { ... } } để báo hoặc dọn dẹp
Hiển thịMột khối log dàiTừng stage có thời gian và trạng thái riêng

Quy tắc thực dụng: job nào viết được trong khoảng hai chục dòng bash thì để freestyle. Job nào có từ ba bước trở lên mà bước sau phụ thuộc kết quả bước trước thì viết pipeline.

3. Mô hình control plane: Jenkins SSH tới mọi máy

                 người vận hành (trình duyệt, qua VPN)
                               │  Build with Parameters
                               ▼
   ┌─────────────────────────────────────────────────┐
   │ JENKINS (mạng private)                           │
   │  credential SSH ── ssh-agent plugin              │
   │  lịch cron ── tham số ── log ── Slack            │
   └──────┬───────────────┬───────────────┬──────────┘
          │ ssh deploy@...│               │
          ▼               ▼               ▼
      balancer         minion        máy vận hành ── mysql/mysqldump/rclone
                                            └──▶ máy database

Jenkins không cần cài agent trên từng Cloud VPS. Nó chỉ cần SSH được tới máy bằng tên trong DNS nội bộ (xem bài về DNS nội bộ). Mọi thao tác với MySQL đi qua máy vận hành, đúng nguyên tắc trong bài mô hình ba tầng. Máy mới dựng bằng cloud image và cloud-init chỉ cần thêm public key của Jenkins là thuộc quyền điều khiển của control plane.

Với job freestyle, key được nạp bằng build wrapper của SSH Agent. Với pipeline thì dùng bước sshagent:

pipeline {
    agent any
    options { disableConcurrentBuilds() }
    parameters {
        choice(name: 'minion', choices: ['app-php74-1', 'app-php82-1', 'app-php82-2'])
        string(name: 'domain', defaultValue: '', description: 'vd: example.com')
    }
    stages {
        stage('Check Web Exists') {
            steps {
                sshagent(['ssh-ops-key']) {
                    sh '''
                      ssh -o ConnectTimeout=10 deploy@"$minion" \
                          "test -d /var/www/$domain" \
                        || { echo "Không thấy /var/www/$domain trên $minion"; exit 1; }
                    '''
                }
            }
        }
    }
    post {
        failure { slackSend(color: 'danger', message: "FAIL ${env.JOB_NAME} #${env.BUILD_NUMBER}: ${params.domain}") }
    }
}

Private key chỉ tồn tại trong Credentials store của Jenkins. Nó được nạp vào bộ nhớ của ssh-agent trong thời gian build chạy, không ghi ra workspace, không lộ trong log.

4. Tổ chức job Jenkins theo nhóm chức năng

NhómViệc làmKiểu jobKích hoạt
Tạo site / deployKiểm tra tham số, kiểm tra DNS đã trỏ về balancer, so phiên bản PHP của minion với yêu cầu, sinh mật khẩu và tạo database, clone code, dựng .env, chạy artisan, sinh cấu hình nginx, xin SSL, cuối cùng curl kiểm tra site trả 200PipelineBấm tay
Đổi domainĐọc thông tin database từ .env, backup rồi thay domain trong dữ liệu, sửa cấu hình minion và balancer, kiểm tra HTTPPipelineBấm tay
BackupDuyệt từng site, dump database qua máy vận hành, nén, đẩy lên object storageFreestyleCron hằng đêm
Gỡ siteBackup file và database, đẩy lên storage, xoá code và cấu hình, xoá bản ghi DNSPipelineBấm tay
Chạy lệnhChạy một lệnh (git, artisan, composer) trong thư mục của một site, sau đó trả quyền cho user chạy webFreestyleBấm tay
Hạ tầngTạo swap, cài agent giám sát, thêm SSH public key, restart dịch vụ định kỳFreestyleBấm tay hoặc cron

Nên đặt tên job theo quy ước <nhóm>_<phạm-vi>, ví dụ backup_app-php82-1 hay site_create. Ô tìm kiếm của Jenkins khi đó trở thành "menu" vận hành: gõ backup_ là thấy toàn bộ job backup. Có thể gom thêm bằng folder hoặc view theo nhóm.

Lịch cron trong Jenkins nên dùng ký hiệu H, ví dụ H 19 * * *. Jenkins băm tên job ra một phút cố định trong giờ đó, nên hàng chục job backup không cùng lao vào máy vận hành đúng một thời điểm.

5. Tham số hoá: chặn lỗi gõ tay từ đầu vào

Giá trị lớn nhất của một job Jenkins so với một đoạn script là form nhập tham số:

  • Choice cho tên balancer và minion: chọn từ danh sách, không gõ, nên không chạy nhầm sang máy không tồn tại.
  • String cho domain, repo, branch. Giá trị luôn được kiểm tra lại ngay ở bước đầu tiên.
  • Boolean cho các tuỳ chọn như cần cron, cần supervisor, cần xin SSL.
  • File để tải lên file SQL khi tạo site từ dữ liệu có sẵn. Job phải kiểm tra file đúng định dạng trước khi dùng.

Domain đi thẳng vào đường dẫn và lệnh shell, nên phải được kiểm tra trước mọi thao tác:

if echo "$domain" | grep -qE '\.\.|[;&|<>`$]'; then
    echo "Error: domain chứa ký tự không hợp lệ: $domain"; exit 1
fi
if ! echo "$domain" | grep -qE '^[a-zA-Z0-9.-]+$'; then
    echo "Error: chỉ cho phép chữ, số, dấu chấm, gạch ngang"; exit 1
fi
if ! ssh deploy@"$minion" "[ -d /var/www/$domain ]"; then
    echo "Error: không có /var/www/$domain trên $minion"; exit 1
fi

Ba bước kiểm tra này chặn được cả lỗi vô ý (gõ thừa dấu cách, chọn nhầm minion) lẫn chuỗi nguy hiểm như ../ hay ; rm -rf trong ô domain.

6. Rủi ro khi dùng Jenkins làm control plane

Gom quyền về một chỗ thì tiện, nhưng cũng gom luôn rủi ro về chỗ đó. Những rủi ro hay gặp và cách giảm:

  • Job không nằm trong Git: script viết thẳng trên giao diện, lưu trong config.xml, sửa job là sửa trực tiếp trên production, không review, không diff. Nên để Jenkinsfile trong repo và cho job đọc từ SCM, hoặc tối thiểu định kỳ xuất config.xml vào Git.
  • Một key quyền cao cho mọi máy: ai có quyền cấu hình job là có quyền trên toàn cụm. Dùng user deploy có sudo giới hạn thay vì root, phân quyền Jenkins chặt (ai được tạo, sửa, chạy job nào) và giới hạn mạng để chỉ Jenkins SSH được vào máy.
  • Job "chạy lệnh tuỳ ý": rất tiện cho lập trình viên, nhưng bản chất là một shell có giao diện web. Giới hạn người được chạy, giới hạn tập lệnh cho phép, và giữ nhật ký build (ai chạy, lệnh gì).
  • Danh sách máy nằm rải rác trong từng job: thêm hay gỡ minion phải sửa choice list ở nhiều chỗ, sót một chỗ là job chạy vào máy không còn tồn tại. Đưa danh sách máy vào biến môi trường dùng chung hoặc file cấu hình trong repo.
  • Jenkins là điểm lỗi đơn: Jenkins chết thì không backup, không deploy. JENKINS_HOME (job, credential, lịch sử build) phải được backup như mọi dữ liệu quan trọng.
  • StrictHostKeyChecking=no: tiện khi dựng lại máy liên tục, nhưng bỏ qua kiểm tra danh tính máy đích. Quản lý known_hosts cho Jenkins thay vì tắt kiểm tra.
  • Hai build cùng đụng một site: disableConcurrentBuilds() chỉ chặn trong cùng một job. Muốn chặn giữa các job khác nhau (đổi domain trong lúc backup) phải dùng khoá theo tên site, ví dụ plugin Lockable Resources.

7. Checklist cho Jenkins control plane

  • Jenkins chỉ truy cập được qua VPN hoặc mạng private, bật xác thực và phân quyền theo vai trò.
  • Private key nằm trong Credentials, nạp bằng SSH Agent; máy đích dùng user deploy riêng.
  • Mỗi thao tác lặp lại là một job có tham số, kiểm tra đầu vào, log và thông báo.
  • Job nhiều bước viết bằng pipeline, có post { failure } báo lỗi.
  • Script job nằm trong Git và được review như code ứng dụng.
  • Cron dùng ký hiệu H; có khoá theo site cho các job ghi dữ liệu.
  • JENKINS_HOME được backup và đã thử khôi phục.

Control plane không cần phức tạp: Jenkins, một credential SSH, DNS nội bộ và quy ước thư mục thống nhất là đủ để điều khiển hàng chục Cloud VPS. Người mới không cần nhớ lệnh, chỉ cần biết chọn job nào.

Bài tiếp theo

Backup database hằng đêm: mysqldump, rclone và object storage. Job backup trên Jenkins chạy thế nào, giữ bao nhiêu ngày, và những lỗi làm bản backup thất bại mà không ai để ý.