서비스 · 프로세스 관리 35 / 50 · Part 4. 로그·스케줄링·운영
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

34편에서 Ubuntu의 e2scrub_all cron 작업이 "systemd 환경이면 실행하지 않는다"는 조건을 달고 있었다. 같은 작업을 systemd timer 가 대신하기 때문이다. 최근 배포판은 로그 로테이션, 임시 파일 정리, 패키지 캐시 갱신 같은 시스템 작업을 점점 timer로 옮기고 있다.

이번 글에서는 timer가 cron과 어떻게 다른지 정리하고, systemd-analyze calendar로 일정 표현식을 검증한 뒤, 20초마다 실행되는 백업 timer 와 systemd-run으로 만드는 일회성 timer 를 직접 만들어 동작을 확인한다.


2. 핵심 개념

2-1. cron과 timer 비교

항목cronsystemd timer
구성crontab 한 줄.timer + .service 두 파일
실행 기록CMD 한 줄 (출력 없음)서비스 출력 전체가 journal에, 상태·종료 코드 확인 가능
최소 간격1분1초 미만도 가능
놓친 작업건너뜀 (anacron 별도)Persistent=true
자원 제한·샌드박스없음서비스의 모든 기능 (18·30·38편)
중복 실행가능 (flock 필요)같은 서비스가 실행 중이면 중복 시작 안 됨
목록 확인파일 여러 곳systemctl list-timers 한 번
학습 비용낮음상대적으로 높음

2-2. [Timer] 주요 설정

설정의미
OnCalendar=달력 시각 (daily, Mon *-*-* 09:00, *:0/15)
OnBootSec=부팅 후 경과 시간
OnActiveSec=timer가 활성화된 뒤 경과 시간
OnUnitActiveSec=대상 서비스가 마지막으로 시작된 뒤 경과 시간
Persistent=꺼져 있던 동안 놓친 OnCalendar 실행을 부팅 후 보충
RandomizedDelaySec=무작위 지연 (anacron의 RANDOM_DELAY)
AccuracySec=실행 시각 오차 허용 (기본 1분)
Unit=실행할 서비스 (기본: 같은 이름의 .service)

3. 동작 원리

systemd timer = .timer (언제) + .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 까지 확인해야 한다.


4. 명령어 실습

# 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의 임시 디렉터리 지정자 로 먼저 해석되었기 때문이다.


5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — 등록된 타이머 목록

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — 타이머 직접 만들기 (service + timer)

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — 일회성 임시 타이머 (systemd-run)

텍스트 원본(실제 출력):

[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.

6. 결과 해석

관찰의미
dnf-makecache.timer, systemd-tmpfiles-clean.timerRocky 최소 설치에서 기본 동작하는 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:4515분 간격
enable → timers.target.wants/lab-backup.timer 링크timer도 enable 하면 부팅 시 자동 활성화된다
LAST 12:29:29, NEXT 12:29:4920초 간격이 정확히 지켜지고 있다 (AccuracySec=1s)
journal backup run at 12:29:29 → Deactivated successfully → Finished서비스 출력과 성공 여부가 모두 기록된다. cron의 CMD 한 줄보다 훨씬 풍부하다
service 상태 inactive (dead), TriggeredBy: ● lab-backup.timeroneshot은 실행 후 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=를 명시한다.


7. 보안 관점

주제내용
지속성악성 timer + service 조합은 cron보다 덜 알려져 있어 점검에서 누락 되기 쉽다 (MITRE T1053.006 Systemd Timers)
사용자 timer~/.config/systemd/user/에 timer를 두면 일반 계정만으로 주기 실행된다. loginctl enable-linger가 켜져 있으면 로그아웃 후에도 동작한다
transient timersystemd-run --on-calendar로 만든 timer는 파일이 /run에만 있어 재부팅 시 사라지지만, 그 전까지는 cron 점검에 보이지 않는다
이름 위장systemd-*-update.timer처럼 정상 이름을 흉내 낸다. 반드시 ACTIVATES 서비스의 ExecStart를 본다
장점반대로 관리자는 timer 서비스에 User=와 샌드박스를 적용해 cron보다 안전한 예약 작업 을 만들 수 있다

8. 보안관제 관점

[점검]  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편 절차

9. 실무에서 자주 발생하는 실수

실수결과예방
unit 파일에서 date +%T/tmp로 치환 (실측)%%T
timer만 enable, 서비스 확인 안 함서비스 오류를 모름journalctl -u NAME.service
서비스에 [Install] 추가 후 enable부팅 시 서비스가 따로 한 번 더 실행서비스는 static, timer만 enable
AccuracySec 기본값 무시최대 1분 늦게 실행필요 시 명시
OnCalendar 표현식 검증 없이 적용예상과 다른 시각systemd-analyze calendar

10. 실습 체크리스트

[ ] systemctl list-timers 로 NEXT / LAST 를 확인했다
[ ] systemd-analyze calendar 로 일정 표현식을 검증했다
[ ] .timer + .service 로 20초 주기 작업을 만들었다
[ ] 서비스 출력과 결과가 journal 에 남는 것을 확인했다
[ ] unit 파일 안의 % 가 지정자로 해석되는 함정을 이해했다
[ ] systemd-run --on-active 로 일회성 timer 를 만들었다

11. 핵심 정리

  • systemd timer는 .timer(언제) + .service(무엇을)로 구성되며, 서비스의 모든 기능을 활용할 수 있다.
  • OnCalendar(달력), OnBootSec·OnUnitActiveSec(경과 시간), Persistent(놓친 실행 보충)를 조합한다.
  • systemd-analyze calendar로 일정을 검증하고, AccuracySec 기본 1분 오차를 기억한다.
  • unit 파일 안의 %는 systemd 지정자이므로 %%로 쓴다.
  • timer 점검 시 반드시 연결된 서비스의 ExecStart·패키지 소유·사용자 timer·transient timer까지 본다.

12. 다음 편 예고

다음 글 「36. at으로 일회성 작업 예약」 에서는 한 번만 실행되는 작업을 예약하는 at을 다룬다. at 작업이 예약 당시의 환경변수를 통째로 저장 한다는 점, 스풀 파일 위치와 접근 제어, 그리고 공격자가 at을 쓰는 이유를 확인한다.


참고 자료


시리즈 이동

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글