
서비스 · 프로세스 관리 35 / 50 · Part 4. 로그·스케줄링·운영
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
34편에서 Ubuntu의 e2scrub_all cron 작업이 "systemd 환경이면 실행하지 않는다"는 조건을 달고 있었다. 같은 작업을 systemd timer 가 대신하기 때문이다. 최근 배포판은 로그 로테이션, 임시 파일 정리, 패키지 캐시 갱신 같은 시스템 작업을 점점 timer로 옮기고 있다.
이번 글에서는 timer가 cron과 어떻게 다른지 정리하고, systemd-analyze calendar로 일정 표현식을 검증한 뒤, 20초마다 실행되는 백업 timer 와 systemd-run으로 만드는 일회성 timer 를 직접 만들어 동작을 확인한다.
| 항목 | cron | systemd timer |
|---|---|---|
| 구성 | crontab 한 줄 | .timer + .service 두 파일 |
| 실행 기록 | CMD 한 줄 (출력 없음) | 서비스 출력 전체가 journal에, 상태·종료 코드 확인 가능 |
| 최소 간격 | 1분 | 1초 미만도 가능 |
| 놓친 작업 | 건너뜀 (anacron 별도) | Persistent=true |
| 자원 제한·샌드박스 | 없음 | 서비스의 모든 기능 (18·30·38편) |
| 중복 실행 | 가능 (flock 필요) | 같은 서비스가 실행 중이면 중복 시작 안 됨 |
| 목록 확인 | 파일 여러 곳 | systemctl list-timers 한 번 |
| 학습 비용 | 낮음 | 상대적으로 높음 |
| 설정 | 의미 |
|---|---|
OnCalendar= | 달력 시각 (daily, Mon *-*-* 09:00, *:0/15) |
OnBootSec= | 부팅 후 경과 시간 |
OnActiveSec= | timer가 활성화된 뒤 경과 시간 |
OnUnitActiveSec= | 대상 서비스가 마지막으로 시작된 뒤 경과 시간 |
Persistent= | 꺼져 있던 동안 놓친 OnCalendar 실행을 부팅 후 보충 |
RandomizedDelaySec= | 무작위 지연 (anacron의 RANDOM_DELAY) |
AccuracySec= | 실행 시각 오차 허용 (기본 1분) |
Unit= | 실행할 서비스 (기본: 같은 이름의 .service) |

systemctl enable --now lab-backup.timer
→ timers.target.wants/ 에 링크 (부팅 시 자동 활성)
→ timer 활성: OnActiveSec=5 → 5초 후 lab-backup.service start
→ service(oneshot) 실행 → 종료 → inactive
→ OnUnitActiveSec=20 → 마지막 시작 20초 후 다시 start
timer의 실행 대상은 서비스 이므로, 25편에서 배운 User=, 28편의 drop-in, 38편의 샌드박스를 그대로 적용할 수 있다. 관제 관점에서는 timer를 볼 때 반드시 연결된 서비스의 ExecStart 까지 확인해야 한다.
# 1) 등록된 timer 와 일정 검증
systemctl list-timers --all --no-pager
systemctl cat systemd-tmpfiles-clean.timer --no-pager | grep -v '^#'
systemd-analyze calendar 'Mon..Fri *-*-* 02:30:00' 'hourly' '*:0/15' --iterations=2
# 2) service + timer 직접 만들기 (unit 파일 안의 % 는 %% 로)
printf '[Unit]\nDescription=Lab backup job\n[Service]\nType=oneshot\nExecStart=/bin/bash -c "echo backup run at $(date +%%%%T); du -sh /etc | tail -1"\n' > /etc/systemd/system/lab-backup.service
printf '[Unit]\nDescription=Lab backup every 20s\n[Timer]\nOnActiveSec=5\nOnUnitActiveSec=20\nAccuracySec=1s\nPersistent=true\n[Install]\nWantedBy=timers.target\n' > /etc/systemd/system/lab-backup.timer
systemctl daemon-reload; systemctl enable --now lab-backup.timer; sleep 28
systemctl list-timers lab-backup.timer --no-pager
journalctl -u lab-backup.service --no-pager -o short-iso | grep -E 'backup run|Finished|Deactivated' | tail -4
systemctl status lab-backup.service --no-pager | sed -n '1,4p'
# 3) 일회성 timer (at 대체)
systemd-run --on-active=5 --timer-property=AccuracySec=1s --unit=lab-once /bin/bash -c 'echo once $(date +%T)'
systemctl list-timers lab-once.timer --no-pager | head -2
sleep 7; journalctl --since '-12s' -o cat --no-pager | grep -E '^once|lab-once'
printf 안의
%%%%T는 printf가%%T로, systemd가 다시%T로 바꿔 bash에 전달한다. 처음에%%T(printf 결과%T)로 작성했을 때는 로그에backup run at /tmp가 찍혔다.%T가 systemd의 임시 디렉터리 지정자 로 먼저 해석되었기 때문이다.



텍스트 원본(실제 출력):
[root@rocky9-lab ~]# systemctl list-timers --all --no-pager
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-09-24 13:13… 43min left Thu 2026-09-24 12:10… 19min ago dnf-makecache.timer dnf-makecache.service
Fri 2026-09-25 12:04… 23h left Thu 2026-09-24 12:04… 25min ago systemd-tmpfiles-clea… systemd-tmpfiles-clea…
2 timers listed.
[root@rocky9-lab ~]# systemctl cat systemd-tmpfiles-clean.timer --no-pager | grep -v '^#'
[Unit]
Description=Daily Cleanup of Temporary Directories
Documentation=man:tmpfiles.d(5) man:systemd-tmpfiles(8)
ConditionPathExists=!/etc/initrd-release
[Timer]
OnBootSec=15min
OnUnitActiveSec=1d
[root@rocky9-lab ~]# systemd-analyze calendar 'Mon..Fri *-*-* 02:30:00' 'hourly' '*:0/15' --iterations=2
Normalized form: Mon..Fri *-*-* 02:30:00
Next elapse: Fri 2026-09-25 02:30:00 UTC
From now: 14h left
Iter. #2: Mon 2026-09-28 02:30:00 UTC
From now: 3 days left
Original form: hourly
Normalized form: *-*-* *:00:00
Next elapse: Thu 2026-09-24 13:00:00 UTC
From now: 30min left
Iter. #2: Thu 2026-09-24 14:00:00 UTC
From now: 1h 30min left
Original form: *:0/15
Normalized form: *-*-* *:00/15:00
Next elapse: Thu 2026-09-24 12:30:00 UTC
From now: 6s left
Iter. #2: Thu 2026-09-24 12:45:00 UTC
From now: 15min left
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab backup job\n[Service]\nType=oneshot\nExecStart=/bin/bash -c "echo backup run at $(date +%%%%T); du -sh /etc | tail -1"\n' > /etc/systemd/system/lab-backup.service
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab backup every 20s\n[Timer]\nOnActiveSec=5\nOnUnitActiveSec=20\nAccuracySec=1s\nPersistent=true\n[Install]\nWantedBy=timers.target\n' > /etc/systemd/system/lab-backup.timer
[root@rocky9-lab ~]# systemctl daemon-reload; systemctl enable --now lab-backup.timer; sleep 28
Created symlink /etc/systemd/system/timers.target.wants/lab-backup.timer → /etc/systemd/system/lab-backup.timer.
[root@rocky9-lab ~]# systemctl list-timers lab-backup.timer --no-pager
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-09-24 12:30:40 UTC 18s left Thu 2026-09-24 12:30:20 UTC 1s ago lab-backup.timer lab-backup.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.
[root@rocky9-lab ~]# journalctl -u lab-backup.service --no-pager -o short-iso | grep -E 'backup run|Finished|Deactivated' | tail -4
2026-09-24T12:29:59+0000 rocky9-lab systemd[1]: Finished Lab backup job.
2026-09-24T12:30:20+0000 rocky9-lab bash[5585]: backup run at 12:30:20
2026-09-24T12:30:20+0000 rocky9-lab systemd[1]: lab-backup.service: Deactivated successfully.
2026-09-24T12:30:20+0000 rocky9-lab systemd[1]: Finished Lab backup job.
[root@rocky9-lab ~]# systemctl status lab-backup.service --no-pager | sed -n '1,4p'
○ lab-backup.service - Lab backup job
Loaded: loaded (/etc/systemd/system/lab-backup.service; static)
Active: inactive (dead) since Thu 2026-09-24 12:30:20 UTC; 1s ago
TriggeredBy: ● lab-backup.timer
[root@rocky9-lab ~]# systemd-run --on-active=5 --timer-property=AccuracySec=1s --unit=lab-once /bin/bash -c 'echo once $(date +%T)'
Running timer as unit: lab-once.timer
Will run service as unit: lab-once.service
[root@rocky9-lab ~]# systemctl list-timers lab-once.timer --no-pager | head -2
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-09-24 12:30:27 UTC 4s left - - lab-once.timer lab-once.service
[root@rocky9-lab ~]# sleep 7; journalctl --since '-12s' -o cat --no-pager | grep -E '^once|lab-once'
once 12:30:27
lab-once.service: Deactivated successfully.
lab-once.timer: Deactivated successfully.
| 관찰 | 의미 |
|---|---|
dnf-makecache.timer, systemd-tmpfiles-clean.timer | Rocky 최소 설치에서 기본 동작하는 timer 2개. NEXT·LEFT·LAST·PASSED로 다음·마지막 실행 을 한눈에 본다 |
tmpfiles-clean: OnBootSec=15min, OnUnitActiveSec=1d | 부팅 15분 후 첫 실행, 이후 하루마다. cron 없이 anacron 같은 동작을 한다 |
Mon..Fri *-*-* 02:30:00 → 다음 금 02:30, 그다음 월 02:30 | 주말을 건너뛰는 것을 적용 전에 검증 했다 |
hourly → *-*-* *:00:00 | 약어를 정규화된 형식으로 보여 준다 |
*:0/15 → 12:30, 12:45 | 15분 간격 |
enable → timers.target.wants/lab-backup.timer 링크 | timer도 enable 하면 부팅 시 자동 활성화된다 |
LAST 12:29:29, NEXT 12:29:49 | 20초 간격이 정확히 지켜지고 있다 (AccuracySec=1s) |
journal backup run at 12:29:29 → Deactivated successfully → Finished | 서비스 출력과 성공 여부가 모두 기록된다. cron의 CMD 한 줄보다 훨씬 풍부하다 |
service 상태 inactive (dead), TriggeredBy: ● lab-backup.timer | oneshot은 실행 후 inactive가 정상이다. 어떤 timer가 이 서비스를 실행하는지 표시된다 |
systemd-run --on-active=5 → lab-once.timer + lab-once.service | 임시 timer와 서비스가 함께 만들어졌다 (/run/systemd/transient) |
once 12:30:27 → timer·service Deactivated | 한 번 실행 후 스스로 사라졌다. at(36편)의 대안이다 |
첫 시도에서는
AccuracySec를 주지 않아 lab-once가 7초 안에 실행되지 않았다. timer의 기본 오차가 1분 이라 전력 절약을 위해 실행을 모아 처리하기 때문이다. 짧은 간격이 중요하면AccuracySec=를 명시한다.
| 주제 | 내용 |
|---|---|
| 지속성 | 악성 timer + service 조합은 cron보다 덜 알려져 있어 점검에서 누락 되기 쉽다 (MITRE T1053.006 Systemd Timers) |
| 사용자 timer | ~/.config/systemd/user/에 timer를 두면 일반 계정만으로 주기 실행된다. loginctl enable-linger가 켜져 있으면 로그아웃 후에도 동작한다 |
| transient timer | systemd-run --on-calendar로 만든 timer는 파일이 /run에만 있어 재부팅 시 사라지지만, 그 전까지는 cron 점검에 보이지 않는다 |
| 이름 위장 | systemd-*-update.timer처럼 정상 이름을 흉내 낸다. 반드시 ACTIVATES 서비스의 ExecStart를 본다 |
| 장점 | 반대로 관리자는 timer 서비스에 User=와 샌드박스를 적용해 cron보다 안전한 예약 작업 을 만들 수 있다 |
[점검] systemctl list-timers --all --no-pager ← 시스템 timer
for u in $(ls /home); do ls /home/$u/.config/systemd/user/*.timer 2>/dev/null; done
ls /run/systemd/transient/*.timer 2>/dev/null ← 임시 timer
loginctl list-users; loginctl show-user <user> -p Linger ← 로그아웃 후 동작 여부
↓
[각 timer 마다]
systemctl cat <x>.timer <x>.service ← 일정 + 실행 명령
rpm -qf /etc/systemd/system/<x>.timer ← 패키지 소유
journalctl -u <x>.service -n 20 ← 실제 실행 결과
↓
[판단] 패키지 미소유 · 최근 생성 · ExecStart 가 외부 다운로드·/tmp 실행 → 46편 절차
| 실수 | 결과 | 예방 |
|---|---|---|
unit 파일에서 date +%T | /tmp로 치환 (실측) | %%T |
| timer만 enable, 서비스 확인 안 함 | 서비스 오류를 모름 | journalctl -u NAME.service |
서비스에 [Install] 추가 후 enable | 부팅 시 서비스가 따로 한 번 더 실행 | 서비스는 static, timer만 enable |
| AccuracySec 기본값 무시 | 최대 1분 늦게 실행 | 필요 시 명시 |
| OnCalendar 표현식 검증 없이 적용 | 예상과 다른 시각 | systemd-analyze calendar |
[ ] systemctl list-timers 로 NEXT / LAST 를 확인했다
[ ] systemd-analyze calendar 로 일정 표현식을 검증했다
[ ] .timer + .service 로 20초 주기 작업을 만들었다
[ ] 서비스 출력과 결과가 journal 에 남는 것을 확인했다
[ ] unit 파일 안의 % 가 지정자로 해석되는 함정을 이해했다
[ ] systemd-run --on-active 로 일회성 timer 를 만들었다
.timer(언제) + .service(무엇을)로 구성되며, 서비스의 모든 기능을 활용할 수 있다.OnCalendar(달력), OnBootSec·OnUnitActiveSec(경과 시간), Persistent(놓친 실행 보충)를 조합한다.systemd-analyze calendar로 일정을 검증하고, AccuracySec 기본 1분 오차를 기억한다.%는 systemd 지정자이므로 %%로 쓴다.다음 글 「36. at으로 일회성 작업 예약」 에서는 한 번만 실행되는 작업을 예약하는 at을 다룬다. at 작업이 예약 당시의 환경변수를 통째로 저장 한다는 점, 스풀 파일 위치와 접근 제어, 그리고 공격자가 at을 쓰는 이유를 확인한다.