Featured image of post Cron Job với Bash: tự động hóa theo lịch cho DevOps

Cron Job với Bash: tự động hóa theo lịch cho DevOps

Hướng dẫn dùng cron job với Bash: crontab, cú pháp lịch 5 trường, @daily, @reboot, log cron, PATH và ví dụ backup database, healthcheck tự động.

Cron job trong Bash cho DevOps

bài trước chúng ta đã quản lý biến, .env, tham số CLI và các biến đặc biệt trong Bash. Khi script đã chạy ổn định bằng tay, bước tiếp theo thường là: làm sao để nó tự chạy đúng thời điểm?

cron là công cụ kinh điển trên Linux/Unix để chạy command hoặc script theo lịch. Với DevOps, cron thường dùng cho backup định kỳ, dọn log, healthcheck, đồng bộ file, tạo report, kiểm tra chứng chỉ SSL hoặc gọi một endpoint bảo trì nhẹ.

Điểm quan trọng: cron chạy trong môi trường tối giản hơn terminal của bạn. Vì vậy script chạy đúng khi gọi tay chưa chắc chạy đúng trong cron nếu thiếu PATH, working directory, biến môi trường hoặc log rõ ràng.


Cron và crontab là gì

cron là daemon chạy nền, đọc các bảng lịch và thực thi command khi tới thời điểm phù hợp. Bảng lịch đó thường được gọi là crontab.

Một số lệnh cơ bản:

1
2
3
crontab -l   # xem crontab của user hiện tại
crontab -e   # chỉnh crontab
crontab -r   # xóa crontab của user hiện tại

Bạn có thể chỉnh crontab của user hiện tại bằng crontab -e. Trên server production, hãy cẩn thận với crontab -r vì lệnh này xóa toàn bộ lịch của user đó.

Ví dụ một dòng cron đơn giản:

1
*/5 * * * * /opt/scripts/check-health.sh >> /var/log/check-health.log 2>&1

Dòng trên chạy script mỗi 5 phút và ghi cả stdout/stderr vào file log.


Cú pháp lịch 5 trường

Một cron job phổ biến có 5 trường thời gian, sau đó là command:

1
2
3
4
5
6
7
* * * * * command-to-run
│ │ │ │ │
│ │ │ │ └── day of week: 0-7 (0 hoặc 7 thường là Chủ nhật)
│ │ │ └──── month: 1-12
│ │ └────── day of month: 1-31
│ └──────── hour: 0-23
└────────── minute: 0-59

Một vài ví dụ hay gặp:

1
2
3
4
5
6
* * * * *      # mỗi phút
*/5 * * * *    # mỗi 5 phút
0 * * * *      # đầu mỗi giờ
30 2 * * *     # 02:30 mỗi ngày
0 3 * * 0      # 03:00 Chủ nhật hằng tuần
0 1 1 * *      # 01:00 ngày đầu mỗi tháng

Cron cũng hỗ trợ danh sách, range và bước nhảy:

1
2
0 9,17 * * 1-5     # 09:00 và 17:00 từ thứ Hai đến thứ Sáu
*/10 8-18 * * *    # mỗi 10 phút trong khung 08:00-18:59

Khi viết lịch cho production, nên thêm comment phía trên để người sau hiểu mục đích của job.


Shortcut @daily, @hourly, @reboot

Nhiều hệ thống cron hỗ trợ các shortcut giúp crontab dễ đọc hơn:

1
2
3
4
5
@hourly   /opt/scripts/hourly-report.sh
@daily    /opt/scripts/backup-db.sh
@weekly   /opt/scripts/cleanup-old-logs.sh
@monthly  /opt/scripts/monthly-report.sh
@reboot   /opt/scripts/start-worker.sh

@daily thường tương đương chạy một lần mỗi ngày vào nửa đêm. @reboot chạy khi cron daemon khởi động, thường dùng cho tác vụ nhẹ sau khi server boot.

Không nên lạm dụng @reboot cho service quan trọng. Với daemon dài hạn, systemd service thường phù hợp hơn vì có restart policy, dependency và log rõ ràng qua journalctl.


Viết script Bash thân thiện với cron

Script chạy từ cron nên tự chuẩn bị môi trường cần thiết thay vì phụ thuộc vào shell tương tác.

Ví dụ khung script tốt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash
set -euo pipefail

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="/var/log/example-app"

mkdir -p "${LOG_DIR}"

log_info() {
  printf '%s [INFO] %s\n' "$(date -Is)" "$*"
}

log_error() {
  printf '%s [ERROR] %s\n' "$(date -Is)" "$*" >&2
}

cd "${SCRIPT_DIR}"
log_info "Script started"

Các điểm cần chú ý:

  • Khai báo shebang rõ ràng.
  • Bật set -euo pipefail để lỗi không bị nuốt im lặng.
  • Đặt PATH trong script hoặc trong crontab.
  • Dùng path tuyệt đối cho file quan trọng.
  • Ghi log có timestamp.
  • Không giả định cron chạy từ thư mục project.

Redirect log trong cron

Cron có thể gửi output qua mail nội bộ nếu hệ thống cấu hình mail. Trong thực tế DevOps, ghi log vào file thường dễ kiểm soát hơn.

Ví dụ redirect stdout và stderr:

1
*/5 * * * * /opt/scripts/check-health.sh >> /var/log/check-health.log 2>&1

Ý nghĩa:

  • >> /var/log/check-health.log: append stdout vào file.
  • 2>&1: chuyển stderr vào cùng nơi với stdout.

Nếu muốn tách log lỗi riêng:

1
*/5 * * * * /opt/scripts/check-health.sh >> /var/log/check-health.out 2>> /var/log/check-health.err

Với job chạy thường xuyên, cần có chiến lược rotate log bằng logrotate hoặc tự giới hạn dung lượng, nếu không file log có thể đầy disk.


Kiểm tra log cron

Vị trí log cron phụ thuộc distro và cấu hình logging.

Trên nhiều hệ thống Ubuntu/Debian dùng syslog:

1
grep CRON /var/log/syslog

Trên hệ thống dùng systemd journal:

1
2
journalctl -u cron
journalctl -u crond

Một số distro dùng service tên crond thay vì cron. Nếu không chắc, kiểm tra service:

1
2
systemctl status cron
systemctl status crond

Lưu ý: log cron thường chỉ cho biết cron đã gọi command hay chưa, không đảm bảo command thành công. Muốn biết script lỗi gì, bạn vẫn cần redirect output hoặc tự ghi log trong script.


Vấn đề PATH và environment trong cron

Cron không load đầy đủ .bashrc, .profile hoặc environment giống terminal của bạn. Đây là nguyên nhân phổ biến khiến script chạy tay thì đúng, nhưng vào cron lại lỗi command not found.

Ví dụ nên đặt PATH ở đầu crontab:

1
2
3
4
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

*/5 * * * * /opt/scripts/check-health.sh >> /var/log/check-health.log 2>&1

Hoặc dùng path tuyệt đối cho command trong script:

1
/usr/bin/curl --fail --silent --show-error https://example.com/health

Với biến môi trường riêng của app, ưu tiên load từ file config do bạn kiểm soát:

1
2
3
4
5
set -a
source /etc/example-app/backup.env
set +a

: "${DATABASE_URL:?DATABASE_URL is required}"

Không đặt secret trực tiếp trong crontab nếu nhiều người có quyền đọc crontab hoặc backup hệ thống. Với secret quan trọng, dùng secret manager, file quyền hạn chặt chẽ hoặc cơ chế secret của nền tảng đang dùng.


Thực hành DevOps: backup database hằng đêm

Ví dụ dưới đây minh họa script backup PostgreSQL hằng đêm. Script dùng biến môi trường từ file cấu hình, tạo thư mục backup, nén output và xóa bản backup cũ.

File /opt/scripts/backup-db.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env bash
set -euo pipefail

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ENV_FILE="/etc/example-app/backup.env"
BACKUP_DIR="/var/backups/example-app"
RETENTION_DAYS="${RETENTION_DAYS:-7}"

log() {
  printf '%s [%s] %s\n' "$(date -Is)" "$1" "$2"
}

if [[ ! -r "${ENV_FILE}" ]]; then
  log "ERROR" "Cannot read env file: ${ENV_FILE}" >&2
  exit 1
fi

set -a
source "${ENV_FILE}"
set +a

: "${DATABASE_NAME:?DATABASE_NAME is required}"
: "${DATABASE_USER:?DATABASE_USER is required}"
: "${DATABASE_HOST:?DATABASE_HOST is required}"

mkdir -p "${BACKUP_DIR}"

backup_file="${BACKUP_DIR}/${DATABASE_NAME}-$(date +%Y%m%d%H%M%S).sql.gz"

log "INFO" "Starting backup to ${backup_file}"

pg_dump \
  --host "${DATABASE_HOST}" \
  --username "${DATABASE_USER}" \
  --dbname "${DATABASE_NAME}" \
  --format plain \
  --no-owner \
  | gzip > "${backup_file}"

find "${BACKUP_DIR}" -type f -name "${DATABASE_NAME}-*.sql.gz" -mtime +"${RETENTION_DAYS}" -delete

log "INFO" "Backup completed"

File /etc/example-app/backup.env:

1
2
3
4
5
DATABASE_NAME=blog_app
DATABASE_USER=backup_user
DATABASE_HOST=db-01.example.com
PGPASSWORD=<database-password>
RETENTION_DAYS=14

Đặt quyền file config chặt chẽ:

1
2
3
sudo chown root:root /etc/example-app/backup.env
sudo chmod 600 /etc/example-app/backup.env
sudo chmod 700 /opt/scripts/backup-db.sh

Thêm cron job chạy lúc 02:30 mỗi ngày:

1
30 2 * * * /opt/scripts/backup-db.sh >> /var/log/backup-db.log 2>&1

Trong môi trường thật, hãy test restore định kỳ. Backup chỉ có giá trị khi bạn biết chắc có thể khôi phục được.


Thực hành DevOps: healthcheck mỗi 5 phút

Ví dụ script kiểm tra endpoint /health, retry nhẹ rồi ghi log. Nếu endpoint vẫn lỗi, script trả exit code khác 0 để cron log lại trạng thái lỗi.

File /opt/scripts/check-health.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env bash
set -euo pipefail

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
HEALTH_URL="${HEALTH_URL:-https://example.com/health}"
MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
DELAY_SECONDS="${DELAY_SECONDS:-5}"

log() {
  printf '%s [%s] %s\n' "$(date -Is)" "$1" "$2"
}

attempt=1

while (( attempt <= MAX_ATTEMPTS )); do
  if curl --fail --silent --show-error --max-time 10 "${HEALTH_URL}" > /dev/null; then
    log "INFO" "Healthcheck OK: ${HEALTH_URL}"
    exit 0
  fi

  log "WARN" "Healthcheck attempt ${attempt}/${MAX_ATTEMPTS} failed"

  if (( attempt == MAX_ATTEMPTS )); then
    log "ERROR" "Healthcheck failed after ${MAX_ATTEMPTS} attempts" >&2
    exit 1
  fi

  sleep "${DELAY_SECONDS}"
  ((attempt++))
done

Crontab:

1
*/5 * * * * HEALTH_URL=https://example.com/health /opt/scripts/check-health.sh >> /var/log/check-health.log 2>&1

Inline environment variable như trên phù hợp với giá trị không nhạy cảm. Nếu cần token hoặc header bí mật, hãy load từ file quyền hạn chặt chẽ hoặc secret manager thay vì đặt thẳng trong crontab.


Tránh job chạy chồng lên nhau

Nếu một job chạy lâu hơn chu kỳ cron, lần chạy mới có thể bắt đầu khi lần cũ chưa xong. Điều này nguy hiểm với backup, cleanup hoặc deploy.

Một cách phổ biến là dùng flock:

1
*/5 * * * * flock -n /tmp/check-health.lock /opt/scripts/check-health.sh >> /var/log/check-health.log 2>&1

flock -n sẽ không chờ lock. Nếu job cũ còn chạy, job mới thoát ngay. Với backup:

1
30 2 * * * flock -n /tmp/backup-db.lock /opt/scripts/backup-db.sh >> /var/log/backup-db.log 2>&1

Không phải hệ thống nào cũng cài sẵn flock, nhưng trên nhiều distro Linux nó nằm trong gói util-linux. Nếu môi trường của bạn không có flock, cần dùng cơ chế lock khác và xử lý stale lock cẩn thận.


Sai sót thường gặp

  • Quên redirect log: Job lỗi nhưng không có output để debug.
  • Phụ thuộc vào working directory: Cron không đảm bảo chạy trong thư mục project.
  • Thiếu PATH: Command như docker, kubectl, node, python có thể không tìm thấy.
  • Dùng path tương đối: File config, log, backup nên dùng path tuyệt đối.
  • Không kiểm soát job chạy chồng: Backup/cleanup có thể chạy đồng thời và gây hỏng dữ liệu.
  • Đặt secret trực tiếp trong crontab: Khó quản lý quyền và audit.
  • Không test command trước khi lưu crontab: Nên chạy script bằng đúng user sẽ chạy cron.

Ghi chú triển khai

  • Khi áp dụng vào dự án của bạn, hãy chuẩn hóa mỗi cron job theo checklist nhỏ:
    • Script có shebang, set -euo pipefail và path tuyệt đối.
    • Crontab có comment mô tả mục đích job.
    • Output được redirect vào log hoặc gửi về hệ thống logging.
    • Job dài có lock để tránh chạy chồng.
    • Secret nằm trong nơi phù hợp, không hardcode trong script public.
  • Best practices:
    • Test script bằng tay với đúng user: sudo -u <app-user> /opt/scripts/job.sh.
    • Ghi timestamp trong log để dễ đối chiếu sự cố.
    • Đặt PATH rõ ràng trong script hoặc crontab.
    • Dùng flock cho job có thể chạy lâu.
    • Theo dõi dung lượng log/backup bằng logrotate hoặc retention policy.
  • Troubleshooting:
    • Job không chạy? → Kiểm tra crontab -l, cú pháp lịch, timezone và log cron.
    • Job chạy nhưng command lỗi? → Kiểm tra file log riêng của script.
    • Chạy tay đúng nhưng cron sai? → Kiểm tra PATH, working directory, permission và biến môi trường.

🎯 Lời kết

Cron giúp Bash script trở thành một phần của lịch vận hành tự động: backup hằng đêm, healthcheck định kỳ, cleanup log hoặc tạo report. Nhưng để cron job đáng tin cậy, bạn cần viết script tự chủ về môi trường, dùng path tuyệt đối, ghi log rõ ràng và tránh chạy chồng khi job có thể kéo dài.

Ở bài tiếp theo, chúng ta sẽ đi vào debug script Bash: bash -x, bash -n, set -x, set -e, set -u, pipefail, trap ERR, PS4shellcheck để tìm lỗi nhanh hơn. 🚀


Tài liệu tham khảo