Contents
see List왜 cron 대신 systemd timer를 검토해야 하나
서버 운영에서 정기 작업은 작지만 중요한 축입니다. 백업, 임시 파일 정리, 리포트 생성, 외부 API 동기화, 인증서 갱신, 로그 압축 같은 작업이 정해진 시간에 실행되지 않으면 장애가 조용히 쌓입니다. 전통적인 cron은 단순하고 익숙하지만, 실패 원인 추적, 실행 환경 고정, 로그 수집, 중복 실행 방지, 권한 격리 측면에서는 운영자가 직접 보완해야 할 부분이 많습니다. 반면 systemd timer는 작업 단위인 service와 스케줄 단위인 timer를 분리해 관리하므로, 실행 결과를 journald에서 확인하고, 실패 시 재시도 정책을 붙이고, 특정 사용자와 디렉터리, 네트워크 조건을 명확히 지정할 수 있습니다.
systemd timer가 모든 cron을 대체해야 한다는 뜻은 아닙니다. 개인 계정의 작은 작업이나 단순 알림은 cron이 충분할 수 있습니다. 그러나 운영 서버에서 실패 여부를 추적해야 하거나, 작업이 서비스 의존성과 연결되어 있거나, 배포 자동화에 포함되는 작업이라면 timer가 더 예측 가능한 선택이 됩니다. 특히 여러 서버에 같은 작업을 배포할 때 unit 파일은 코드 저장소에서 리뷰하고 변경 이력을 남기기 쉽습니다.
service와 timer를 나누어 설계하기
systemd timer는 실제 명령을 직접 길게 담기보다, 별도의 service를 호출하는 방식으로 구성하는 것이 좋습니다. service에는 실행할 프로그램, 작업 디렉터리, 환경 변수 파일, 사용자, 실패 정책을 담고, timer에는 언제 실행할지만 담습니다. 이렇게 나누면 수동 점검도 쉬워집니다. 예를 들어 타이머가 새벽 3시에 실행되도록 설정되어 있어도, 운영자는 같은 service를 즉시 실행해 결과를 확인할 수 있습니다.
[Unit]
Description=Daily application backup job
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=app
Group=app
WorkingDirectory=/srv/app
EnvironmentFile=-/etc/app/backup.env
ExecStart=/usr/local/bin/app-backup.sh
TimeoutStartSec=20min
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
# 실패를 너무 빨리 반복하지 않도록 제한
StartLimitIntervalSec=1h
StartLimitBurst=3
위 예제에서 Type=oneshot은 장시간 상주 서비스가 아니라 한 번 실행되고 종료되는 작업임을 뜻합니다. EnvironmentFile 앞의 하이픈은 파일이 없어도 unit 로드를 실패시키지 않겠다는 의미입니다. 운영 환경에서는 민감한 값이 스크립트 본문이나 unit 파일에 박히지 않도록 별도 파일로 분리하고, 해당 파일 권한을 제한해야 합니다. TimeoutStartSec도 중요합니다. 백업 스크립트가 외부 스토리지 장애로 무한 대기하면 다음 작업과 겹칠 수 있으므로, 허용 가능한 최대 시간을 명확히 정해야 합니다.
타이머 스케줄과 지연 실행 설정
timer 파일은 실행 시점과 부팅 후 보정 정책을 담당합니다. cron의 0 3 * * * 표현에 해당하는 설정은 OnCalendar=*-*-* 03:00:00처럼 읽기 쉬운 형태로 쓸 수 있습니다. Persistent=true를 사용하면 서버가 꺼져 있던 동안 놓친 실행을 부팅 후 한 번 보정할 수 있습니다. 백업이나 정산처럼 반드시 하루 한 번 실행되어야 하는 작업에 유용합니다.
[Unit]
Description=Run daily application backup
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10min
AccuracySec=1min
Unit=app-backup.service
[Install]
WantedBy=timers.target
RandomizedDelaySec는 여러 서버가 같은 시각에 동시에 외부 저장소나 데이터베이스로 몰리는 상황을 줄여줍니다. 예를 들어 20대의 서버가 모두 03:00:00에 백업을 시작하면 스토리지 부하가 급격히 증가할 수 있습니다. 10분 정도의 무작위 지연을 주면 전체 작업은 여전히 새벽 시간대에 끝나면서도 순간 부하를 낮출 수 있습니다. AccuracySec는 systemd가 전력과 부하를 고려해 실행 시간을 묶을 수 있는 허용 오차입니다. 운영상 초 단위 정확도가 필요하지 않은 작업은 너무 좁게 잡지 않는 편이 좋습니다.
배포, 활성화, 즉시 점검 명령
unit 파일을 작성한 뒤에는 systemd가 새 파일을 인식하도록 daemon-reload를 실행하고, timer를 enable 및 start합니다. enable은 부팅 후 자동 활성화를 의미하고, start는 현재 세션에서 즉시 타이머를 켜는 동작입니다. 작업 자체를 바로 검증하려면 timer가 아니라 service를 start해야 합니다.
sudo cp app-backup.service /etc/systemd/system/app-backup.service
sudo cp app-backup.timer /etc/systemd/system/app-backup.timer
sudo systemctl daemon-reload
sudo systemctl enable --now app-backup.timer
# 다음 실행 예정 시간 확인
systemctl list-timers app-backup.timer
# 스케줄을 기다리지 않고 작업을 수동 실행
sudo systemctl start app-backup.service
# 최근 실행 로그 확인
journalctl -u app-backup.service -n 100 --no-pager
운영 배포에서는 위 명령을 그대로 수동 실행하기보다 Ansible, 배포 스크립트, CI/CD 작업으로 묶어 반복 가능하게 만드는 편이 좋습니다. unit 파일이 바뀌었는데 daemon-reload를 빼먹는 실수도 흔하므로, 파일 복사 후 reload, timer restart, 상태 확인 순서를 고정해두면 장애를 줄일 수 있습니다.
중복 실행과 실패 알림을 다루는 방법
정기 작업에서 자주 발생하는 문제는 이전 실행이 끝나기 전에 다음 실행이 시작되는 것입니다. systemd의 같은 service는 기본적으로 이미 실행 중이면 다시 동시에 시작되지 않지만, 스크립트 내부에서 백그라운드 프로세스를 띄우거나 여러 이름의 unit이 같은 자원을 건드리면 중복 문제가 생길 수 있습니다. 중요한 작업은 flock을 함께 사용해 파일 잠금을 거는 방식이 실용적입니다.
#!/usr/bin/env bash
set -euo pipefail
LOCK=/run/lock/app-backup.lock
exec 9>"$LOCK"
if ! flock -n 9; then
echo "backup already running"
exit 0
fi
/usr/bin/pg_dump "$DATABASE_URL" | gzip > "/backup/app-$(date +%F).sql.gz"
find /backup -type f -name 'app-*.sql.gz' -mtime +14 -delete
실패 알림은 별도 OnFailure service로 연결할 수 있습니다. 예를 들어 app-backup.service에 OnFailure=notify-failure@%n.service를 추가하면 실패한 unit 이름을 알림 스크립트로 넘길 수 있습니다. 이때 알림 스크립트는 토큰을 로그에 출력하지 않도록 주의해야 하며, 실패 로그는 journalctl에서 필요한 범위만 가져오는 방식이 안전합니다. 운영팀이 이미 사용하는 Slack, Telegram, 이메일, 사내 알림 API가 있다면 그 경로로 통일하는 것이 좋습니다.
운영 점검 체크리스트
- 작업 명령은 timer가 아니라 service의 ExecStart에 둔다.
- 수동 검증은 systemctl start 작업.service로 수행하고, 예약 상태는 list-timers로 확인한다.
- 네트워크나 마운트가 필요한 작업은 Wants, After, RequiresMountsFor 같은 의존성을 명시한다.
- 장시간 멈춤을 막기 위해 TimeoutStartSec를 설정한다.
- 여러 서버가 동시에 실행되지 않도록 RandomizedDelaySec를 검토한다.
- 중복 실행 위험이 있는 작업은 flock 또는 애플리케이션 레벨 잠금을 둔다.
- 비밀값은 unit 파일에 직접 쓰지 말고 권한이 제한된 EnvironmentFile이나 시크릿 관리 도구를 사용한다.
- journalctl 로그 확인 명령과 실패 알림 경로를 운영 문서에 남긴다.
정리하면 systemd timer는 단순한 일정 실행 도구가 아니라, 정기 작업을 운영 서비스처럼 다루게 해주는 장치입니다. service에는 실행 조건과 실패 정책을, timer에는 스케줄과 보정 정책을 담고, 로그와 알림까지 한 흐름으로 연결하면 작은 배치 작업도 추적 가능한 운영 자산이 됩니다. 새 서버에 정기 작업을 추가할 때는 먼저 cron 한 줄을 떠올리기보다, 이 작업이 실패했을 때 누가 어떻게 알 수 있는지, 다시 실행해도 안전한지, 다음 실행과 겹치지 않는지를 함께 점검하는 것이 핵심입니다.