Systemd Timer Cho Sysadmin: Tự Động Hóa Backup Và Job Định Kỳ An Toàn Trên Linux

Systemd timer backup Linux là một cách hiện đại để chạy job định kỳ trên server Linux thay cho các cron job khó quan sát, khó giới hạn quyền và dễ bị “chạy âm thầm rồi hỏng âm thầm”. Cron vẫn hữu ích, nhưng trong môi trường production, sysadmin thường cần nhiều hơn một lịch chạy: cần log tập trung trong journal, retry rõ ràng, dependency với network/mount, timeout, sandbox, trạng thái lần chạy gần nhất và khả năng kiểm tra bằng lệnh chuẩn.

Bài viết này hướng dẫn triển khai systemd timer cho một bài toán thực tế: backup thư mục cấu hình và dữ liệu ứng dụng bằng rsync. Nội dung đi từ khái niệm, kiến trúc production/lab, file unit cụ thể, lệnh kiểm tra, output mẫu, lỗi thường gặp, troubleshooting, checklist nghiệm thu và bài lab cuối bài.

1. Vì sao nên dùng systemd timer thay cron cho job quan trọng?

Cron có ưu điểm là đơn giản, phổ biến và gần như có mặt ở mọi hệ thống Unix/Linux. Nhưng khi job trở nên quan trọng, cron bộc lộ nhiều điểm yếu: log phân tán qua mail/syslog tùy distro, khó biết lần chạy gần nhất thành công hay thất bại, khó áp timeout, khó định nghĩa dependency với network hoặc mount, và dễ bị chồng lấn nếu job cũ chưa chạy xong.

Systemd timer tách lịch chạy ra khỏi logic chạy. Timer quyết định khi nào chạy, service quyết định chạy cái gì. Nhờ vậy job định kỳ có đầy đủ năng lực của systemd service: journalctl, systemctl status, restart policy, timeout, user riêng, working directory, environment file, hardening bằng sandbox và audit dễ hơn.

  • Quan sát tốt hơn: xem trạng thái bằng systemctl list-timers, log bằng journalctl -u.
  • An toàn hơn: chạy bằng user ít quyền, giới hạn file system, giới hạn network nếu cần.
  • Ít lỗi vận hành hơn:Persistent=true để bù lần chạy bị miss khi server tắt.
  • Dễ chuẩn hóa: unit file có thể quản lý bằng Ansible/Git.

2. Kiến trúc lab và production mẫu

Giả sử server ứng dụng có dữ liệu cần backup ở /srv/app và cấu hình ở /etc/nginx. Backup sẽ được đẩy sang thư mục mount sẵn /mnt/backup/app01 hoặc một máy backup qua SSH. Trong lab, bạn có thể dùng thư mục cục bộ để tránh rủi ro.

2.1. Thành phần chính

  • /usr/local/sbin/app-backup.sh: script backup có kiểm tra lock, disk, rsync và retention.
  • /etc/systemd/system/app-backup.service: service one-shot chạy script.
  • /etc/systemd/system/app-backup.timer: lịch chạy hằng ngày.
  • journalctl -u app-backup.service: nơi xem log.
  • /var/lock/app-backup.lock: lock chống chạy chồng.

3. Chuẩn bị user, thư mục và quyền

Trên server lab Ubuntu/Debian/RHEL tương đương, tạo user riêng cho job nếu backup không cần root toàn quyền. Với dữ liệu cần đọc nhiều thư mục hệ thống, có thể dùng root nhưng phải hardening kỹ service unit.

sudo useradd --system --home /var/lib/app-backup --shell /usr/sbin/nologin appbackup
sudo mkdir -p /var/lib/app-backup /mnt/backup/app01 /var/log/app-backup
sudo chown -R appbackup:appbackup /var/lib/app-backup /mnt/backup/app01 /var/log/app-backup
sudo chmod 750 /mnt/backup/app01

Giải thích nhanh:

  • --system: tạo account hệ thống, không dùng để đăng nhập tương tác.
  • /usr/sbin/nologin: giảm nguy cơ account bị dùng làm shell login.
  • chmod 750: chỉ owner/group có quyền đọc nội dung backup.

4. Viết script backup có kiểm tra lỗi

Không nên nhét toàn bộ logic vào dòng ExecStart. Hãy viết script rõ ràng, có set -euo pipefail, log có timestamp, kiểm tra dung lượng và dùng lock để tránh chạy chồng.

sudo tee /usr/local/sbin/app-backup.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

SRC_DIRS=("/srv/app" "/etc/nginx")
DEST="/mnt/backup/app01"
LOCK="/var/lock/app-backup.lock"
DATE="$(date +%F_%H%M%S)"
TARGET="$DEST/$DATE"

log() { echo "$(date -Is) $*"; }

exec 9>"$LOCK"
if ! flock -n 9; then
  log "ERROR: previous backup is still running"
  exit 20
fi

if ! mountpoint -q "$DEST"; then
  log "ERROR: destination $DEST is not a mountpoint"
  exit 21
fi

FREE_KB=$(df -Pk "$DEST" | awk 'NR==2 {print $4}')
if [ "$FREE_KB" -lt 1048576 ]; then
  log "ERROR: backup destination has less than 1GB free"
  exit 22
fi

mkdir -p "$TARGET"
for src in "${SRC_DIRS[@]}"; do
  if [ -e "$src" ]; then
    log "INFO: syncing $src to $TARGET"
    rsync -aHAX --numeric-ids --delete "$src" "$TARGET/"
  else
    log "WARN: source $src does not exist, skipped"
  fi
done

find "$DEST" -maxdepth 1 -type d -name '20*' -mtime +14 -print -exec rm -rf {} +
log "INFO: backup completed: $TARGET"
EOF
sudo chmod 750 /usr/local/sbin/app-backup.sh

Output thành công thường giống như:

2026-06-21T01:30:01+07:00 INFO: syncing /srv/app to /mnt/backup/app01/2026-06-21_013001
2026-06-21T01:30:18+07:00 INFO: syncing /etc/nginx to /mnt/backup/app01/2026-06-21_013001
2026-06-21T01:30:19+07:00 INFO: backup completed: /mnt/backup/app01/2026-06-21_013001

5. Tạo systemd service one-shot

Service nên là Type=oneshot vì backup chạy xong rồi thoát. Thêm TimeoutStartSec để job treo không chiếm tài nguyên vô hạn. Với server production, cân nhắc hardening như NoNewPrivileges, PrivateTmp, ProtectSystemReadWritePaths.

sudo tee /etc/systemd/system/app-backup.service >/dev/null <<'EOF'
[Unit]
Description=Application backup job
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/app-backup.sh
TimeoutStartSec=2h
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=read-only
ProtectSystem=full
ReadWritePaths=/mnt/backup/app01 /var/lock /var/log/app-backup

[Install]
WantedBy=multi-user.target
EOF

Lưu ý: nếu script cần đọc /srv/app, /etc/nginx và ghi /mnt/backup/app01, bạn phải đảm bảo các directive hardening không chặn nhầm đường dẫn. Với ProtectSystem=full, hệ thống chỉ đọc ở nhiều vùng, còn ReadWritePaths mở lại những nơi cần ghi.

6. Tạo systemd timer

Timer dưới đây chạy lúc 01:30 hằng ngày. Persistent=true giúp systemd chạy bù nếu máy tắt đúng thời điểm lịch. RandomizedDelaySec tránh nhiều server cùng bắn backup vào storage đúng một phút.

sudo tee /etc/systemd/system/app-backup.timer >/dev/null <<'EOF'
[Unit]
Description=Run application backup daily

[Timer]
OnCalendar=*-*-* 01:30:00
Persistent=true
RandomizedDelaySec=10m
Unit=app-backup.service

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now app-backup.timer

Kiểm tra timer:

systemctl list-timers app-backup.timer
systemctl status app-backup.timer

Output mẫu:

NEXT                        LEFT     LAST PASSED UNIT             ACTIVATES
Mon 2026-06-22 01:35:12 +07 6h left  n/a  n/a    app-backup.timer app-backup.service

7. Chạy thử thủ công và đọc log

sudo systemctl start app-backup.service
systemctl status app-backup.service --no-pager
journalctl -u app-backup.service -n 100 --no-pager

Nếu service thất bại, systemctl status cho biết exit code, còn journalctl cho biết dòng log cụ thể từ script. Đây là lợi thế lớn so với cron job bị mất log hoặc chỉ gửi mail tới local mailbox không ai đọc.

8. Gửi cảnh báo khi job thất bại

Một job backup không có alert thì gần như chưa hoàn thiện. Cách đơn giản là thêm OnFailure để gọi service cảnh báo qua mail, Slack hoặc webhook nội bộ. Ví dụ tối giản ghi log cảnh báo:

sudo tee /etc/systemd/system/notify-backup-failure@.service >/dev/null <<'EOF'
[Unit]
Description=Notify backup failure for %i

[Service]
Type=oneshot
ExecStart=/usr/bin/logger -p authpriv.err "Backup unit failed: %i"
EOF

Sau đó thêm vào app-backup.service:

[Unit]
OnFailure=notify-backup-failure@%n.service

Trong production, thay logger bằng script gửi alert tới hệ thống bạn đang dùng. Quan trọng là alert phải có owner và kênh nhận rõ ràng, không phải gửi vào nơi không ai trực.

9. Troubleshooting lỗi thường gặp

9.1. Timer không chạy

systemctl is-enabled app-backup.timer
systemctl list-timers --all | grep app-backup
systemctl cat app-backup.timer

Kiểm tra timer đã enable chưa, tên Unit= có đúng service không và đã chạy systemctl daemon-reload sau khi sửa file chưa.

9.2. Service chạy thủ công được nhưng timer không chạy đúng giờ

Kiểm tra timezone server bằng timedatectl. Nếu dùng RandomizedDelaySec, giờ chạy thực tế có thể trễ trong khoảng cho phép. Đây là chủ đích để giảm tải đồng thời.

timedatectl
systemctl list-timers app-backup.timer --all

9.3. Lỗi permission denied

Nếu chạy bằng user riêng, user đó cần quyền đọc source và ghi destination. Nếu dùng hardening directive, hãy kiểm tra ReadWritePaths, ProtectHome, ProtectSystem. Dùng lệnh:

journalctl -u app-backup.service -b --no-pager | tail -50
systemctl show app-backup.service -p User -p Group -p ReadWritePaths

9.4. Backup bị chạy chồng

Dùng flock như script mẫu. Nếu job cũ còn chạy, job mới thoát với mã lỗi riêng. Với backup lớn, cũng nên xem lại lịch chạy và băng thông storage.

9.5. Destination chưa mount

Nếu backup đẩy vào mount NFS/iSCSI, hãy thêm dependency mount cụ thể hoặc kiểm tra mountpoint -q. Không kiểm tra mount có thể khiến dữ liệu backup ghi vào thư mục local trống và làm đầy root filesystem.

10. Hardening production nên có

  • Chạy với user ít quyền nếu có thể.
  • Dùng SSH key riêng cho backup, giới hạn command hoặc path ở server đích.
  • Bật encryption at rest cho nơi lưu backup.
  • Không lưu secret trực tiếp trong unit file; dùng environment file có quyền chặt hoặc secret manager.
  • Giới hạn CPU/IO nếu backup ảnh hưởng service production.
  • Có retention rõ ràng: daily/weekly/monthly.
  • Có restore drill định kỳ, vì backup chưa restore được thì chưa phải backup đáng tin.

11. Checklist nghiệm thu

  • systemctl list-timers hiển thị timer và thời điểm chạy kế tiếp.
  • Chạy thủ công systemctl start app-backup.service thành công.
  • journalctl -u app-backup.service có log rõ ràng, đủ timestamp.
  • Script có lock chống chạy chồng.
  • Script kiểm tra destination mount và dung lượng trống.
  • Backup tạo ra thư mục/snapshot đúng cấu trúc.
  • Có retention, không để backup tăng vô hạn.
  • Có alert khi service failed.
  • Đã restore thử ít nhất một file và một thư mục.
  • Unit file được lưu trong Git/Ansible hoặc tài liệu vận hành.

12. Bài lab cuối bài

  • Tạo VM Ubuntu hoặc Debian.
  • Tạo thư mục /srv/app với vài file test.
  • Tạo destination /mnt/backup/app01. Nếu không có mount thật, tạm thời bỏ check mountpoint trong lab hoặc bind mount một thư mục khác.
  • Triển khai script, service và timer như bài viết.
  • Chạy thủ công service, xem log bằng journalctl.
  • Giả lập lỗi bằng cách đổi quyền destination thành read-only, kiểm tra service failed và alert.
  • Khôi phục quyền, chạy lại service, sau đó restore một file từ bản backup.
  • Viết lại runbook ngắn cho team: cách kiểm tra trạng thái, cách chạy backup thủ công, cách restore và cách xử lý lỗi.

Kết luận

Systemd timer không phải “cron mới cho vui”, mà là cách đưa job định kỳ vào cùng hệ sinh thái quản trị service của Linux. Với backup, log, retry, timeout, dependency, hardening và alert là những thứ quan trọng ngang với lệnh rsync. Bắt đầu bằng một service one-shot rõ ràng, một timer có Persistent=true, script có kiểm tra lỗi và một restore drill định kỳ. Khi đó job backup của bạn không còn là dòng cron mong manh, mà trở thành một runbook vận hành có thể kiểm chứng.

Tác giả: Mạnh Hoàng

Tôi là Hoàng Mạnh, người sáng lập blog SysadminSkills.com. Tôi viết về quản trị hệ thống, bảo mật máy chủ, DevOps và cách ứng dụng AI để tự động hóa công việc IT. Blog này là nơi tôi chia sẻ những gì đã học được từ thực tế – đơn giản, ngắn gọn và áp dụng được ngay.