RHEL 중급 #6 작업 스케줄링 — cron, systemd timer, at
운영하다 보면 시간 기반으로 돌려야 하는 작업이 끝없이 생깁니다. 매일 새벽 백업, 매주 보고서 메일, 매분 헬스 체크, 한 시간 뒤에 한 번만 돌릴 스크립트. RHEL 9에는 이런 작업을 다루는 도구가 cron, anacron, at, systemd timer로 나뉘어 있습니다. 이 글에서는 각각을 언제 쓰는지가 핵심입니다.
RHEL 중급 시리즈에서 이번 글의 위치:
- #1 SELinux 입문 — Enforcing/Permissive, 라벨, 트러블슈팅
- #2 LVM — PV/VG/LV, 스냅샷, 확장
- #3 스토리지 심화 — Stratis, NFS, Samba
- #4 네트워킹 — NetworkManager (nmcli), bonding, teaming
- #5 로그 관리 — journald, rsyslog, log rotation
- #6 작업 스케줄링 — cron, systemd timer, at ← 이번 글
- #7 컨테이너 입문 — Podman/Buildah/Skopeo (Docker와의 차이)
네 가지 도구를 한 줄로 #
| 도구 | 용도 | 머신 꺼져 있던 시간 | 비고 |
|---|---|---|---|
| cron | 반복 작업 (매분/매시/매일/…) | 그냥 건너뜀 | 가장 전통, 가장 단순 |
| anacron | 머신이 꺼졌다 켜졌어도 보장된 일/주/월 단위 실행 | 놓친 작업 보충 | 노트북,재택 서버 필수 |
| at | 단발성 예약 (한 시간 뒤 한 번 실행) | 머신 켜진 후 즉시 실행 | 일회성 |
| systemd timer | 반복 + 의존성 + 로그 통합 | Persistent=true로 보충 | 모던 표준 |
새 작업을 잡을 때 흐름:
- 반복 + 단순 → cron
- 반복 + 머신 켜져 있을지 불확실 → anacron 또는 systemd timer (
Persistent=true) - 반복 + 의존성/journald 통합 필요 → systemd timer
- 딱 한 번 → at
cron — 전통의 출발점 #
cronie 패키지가 RHEL 9에 기본 설치돼 있고, crond 서비스가 항상 실행됩니다.
$ systemctl status crond
● crond.service - Command Scheduler
Loaded: loaded (/usr/lib/systemd/system/crond.service; enabled; ...)
Active: active (running)cron이 보는 파일은 두 갈래:
/etc/crontab ← 시스템 전역 (관습)
/etc/cron.d/<name> ← 시스템 전역 (drop-in, 권장)
/etc/cron.{hourly,daily,weekly,monthly}/ ← 디렉터리에 스크립트만 두면 자동 실행
/var/spool/cron/<user> ← 사용자별 crontab (직접 편집 ✗, crontab -e로)crontab 문법 #
다섯 칸의 시각 + 명령. 처음 보면 어지럽지만 자주 쓰면 손에 익습니다.
* * * * * 명령
│ │ │ │ │
│ │ │ │ └─ 요일 (0-7, 0과 7은 일요일)
│ │ │ └─── 월 (1-12)
│ │ └───── 일 (1-31)
│ └─────── 시 (0-23)
└───────── 분 (0-59)자주 쓰는 패턴:
# 매분
* * * * * /usr/local/bin/healthcheck.sh
# 매일 새벽 3시 15분
15 3 * * * /usr/local/bin/backup.sh
# 평일(월~금) 오전 9시
0 9 * * 1-5 /usr/local/bin/morning-report.sh
# 5분 간격
*/5 * * * * /usr/local/bin/ping.sh
# 9~18시, 30분 간격
0,30 9-18 * * * /usr/local/bin/business-hours.sh
# 매월 1일 자정
0 0 1 * * /usr/local/bin/monthly-cleanup.sh
# 약식 표현
@reboot /usr/local/bin/on-startup.sh
@hourly /usr/local/bin/each-hour.sh
@daily /usr/local/bin/each-day.sh
@weekly /usr/local/bin/each-week.sh
@monthly /usr/local/bin/each-month.sh
@yearly /usr/local/bin/each-year.sh사용자 crontab #
각 사용자가 자기 작업을 등록할 수 있습니다.
# 등록/수정 (에디터로 열림)
$ crontab -e
# 보기
$ crontab -l
# 비우기
$ crontab -r
# 다른 사용자 것 (root만)
$ sudo crontab -u alice -lcrontab -e로 편집해야 하는 이유: /var/spool/cron/<user>를 직접 vim으로 열면 cron이 변경을 감지하지 못합니다. crontab -e는 변경 후 cron에 시그널을 보내요.
시스템 crontab vs 사용자 crontab #
/etc/crontab과 /etc/cron.d/*는 사용자 컬럼이 하나 더 있습니다.
# 분 시 일 월 요일 사용자 명령
*/10 * * * * root /usr/local/bin/sync.sh운영 권장:
- 사용자 작업 →
crontab -e - 패키지/시스템 작업 →
/etc/cron.d/<name>(drop-in 파일) /etc/crontab직접 수정은 피함 (관습상 시스템 전용, 패키지가 덮어쓸 수 있음)
cron 환경변수 #
cron이 돌릴 때 셸 환경은 로그인 셸이 아닙니다. $PATH도 다르고 $HOME도 제한적입니다. 매뉴얼로 실행했을 때 잘 되던 스크립트가 cron에서 실패하는 가장 흔한 이유.
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
15 3 * * * root /usr/local/bin/backup.shMAILTO=가 비어 있지 않으면 cron이 작업의 stdout/stderr를 메일로 보냅니다. 메일 전송이 막혀 있는 환경에서는 스크립트 안에서 직접 로그 파일로 redirect 하는 게 표준입니다.
15 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1권한 제어 #
/etc/cron.allow와 /etc/cron.deny로 사용자별 cron 사용을 제어합니다.
cron.allow가 있으면 → 거기에 적힌 사용자만 사용 가능cron.allow가 없고cron.deny가 있으면 → 거기에 적히지 않은 사용자만 사용 가능- 둘 다 없으면 (RHEL 9 기본) → root만 사용 가능
운영에선 cron.allow를 쓰는 화이트리스트 방식이 일반적입니다.
anacron — 머신이 꺼졌다 켜졌어도 보장 #
cron의 약점: 머신이 새벽 3시에 꺼져 있으면 새벽 3시 백업은 그냥 건너뜁니다. 다음날까지 기다려야 합니다. 노트북, 재택 서버, 가끔 켜는 워크스테이션에서 치명적.
anacron은 “마지막으로 이 작업이 실행된 게 언제인가” 를 디스크에 기록합니다. 머신이 켜질 때마다 anacron이 작동해 “마지막 실행에서 지정 기간이 지났으면 지금 실행” 합니다.
SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
RANDOM_DELAY=45
START_HOURS_RANGE=3-22
# 주기 지연(분) 작업ID 명령
1 5 cron.daily nice run-parts /etc/cron.daily
7 25 cron.weekly nice run-parts /etc/cron.weekly
@monthly 45 cron.monthly nice run-parts /etc/cron.monthly읽는 법:
1 5 cron.daily ...— 매일(period 1), 머신 켜진 후 5분 뒤,/etc/cron.daily/*의 모든 스크립트 실행7 25 ...— 매주(period 7)RANDOM_DELAY=45— 부하 분산용 랜덤 지연 0~45분
RHEL 9의 동작: anacron은 cronie 패키지에 함께 들어와 systemd timer(anacron.timer)로 실행됩니다. 그러니 /etc/cron.daily/*에 스크립트만 두면 cron + anacron이 함께 작동해 머신이 꺼져 있어도 다음 부팅 후 보충 실행됩니다.
서버급 머신은 어차피 24/7 켜져 있어 anacron이 큰 의미가 없지만, 모르고 두 번 실행되는 사고를 막으려면 cron.daily 작업의 멱등성(idempotency)을 항상 신경 써야 합니다.
at — 한 번만 예약 #
“30분 뒤에 이 명령 한 번만” 같은 단발성 예약은 at의 영역.
# 패키지 (RHEL 9 기본 설치 ✗ → 직접 설치)
$ sudo dnf install -y at
$ sudo systemctl enable --now atd
# 30분 뒤
$ at now + 30 minutes
at> /usr/local/bin/restart-app.sh
at> <Ctrl-D>
# 특정 시각
$ at 23:30
$ at 23:30 today
$ at 9am tomorrow
$ at 11:00 2026-05-01
# 큐 보기
$ atq
# 큐 내용 보기 (작업번호로)
$ at -c 3
# 취소
$ atrm 3at도 cron처럼 권한 제어 파일이 있습니다: /etc/at.allow, /etc/at.deny.
at은 batch 라는 변형도 제공합니다. 시스템 부하가 낮을 때(load average < 1.5) 실행되는 모드.
$ batch
at> /usr/local/bin/heavy-job.sh
at> <Ctrl-D>야간 백업 같은 무거운 작업을 시스템이 한가할 때만 돌리고 싶을 때 유용합니다.
systemd timer — 모던 표준 #
cron이 1975년 만들어진 도구라면, systemd timer는 2010년대의 모던 대안입니다. RHEL 9의 패키지들이 점점 cron에서 timer로 옮겨가고 있습니다. logrotate, dnf-makecache, fstrim, 모두 timer로 실행됩니다.
timer가 cron보다 좋은 점 #
- journald 통합 — 모든 출력이 journald에 자동 기록 (cron은 메일 또는 직접 redirect)
- 의존성 —
After=network-online.target같은 systemd 의존성 그대로 사용 - 자원 제어 —
MemoryMax=,CPUQuota=등 service unit의 제어 옵션 그대로 Persistent=true— 머신이 꺼져 있던 시간을 anacron처럼 보충OnCalendar=— cron보다 표현력이 넓고 사람이 읽기 쉬움- trigger 검증 —
systemd-analyze calendar로 다음 실행 시각을 미리 확인
timer + service 짝 #
systemd timer는 항상 두 unit이 짝을 이룹니다. .service (실제로 할 일) + .timer (언제 실행할지).
[Unit]
Description=Daily backup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Group=backup
Nice=10[Unit]
Description=Run daily backup at 03:15
Requires=backup.service
[Timer]
OnCalendar=*-*-* 03:15:00
RandomizedDelaySec=15min
Persistent=true
Unit=backup.service
[Install]
WantedBy=timers.target활성화는 timer만 enable 합니다 (service는 timer가 트리거).
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now backup.timer
$ systemctl list-timers --allOnCalendar 문법 #
cron의 * * * * *와 비교했을 때 systemd가 한결 명확합니다.
# 매분
*-*-* *:*:00
# 매일 03:15
*-*-* 03:15:00
03:15 # 약식
# 평일 09:00
Mon..Fri *-*-* 09:00:00
Mon..Fri 09:00 # 약식
# 5분 간격
*-*-* *:00/5:00
*:0/5 # 약식
# 매월 1일
*-*-01 00:00:00
monthly # 별칭
# 별칭들
hourly / daily / weekly / monthly / yearly문법 검증과 다음 실행 시각 미리보기:
$ systemd-analyze calendar 'Mon..Fri 09:00'
Original form: Mon..Fri 09:00
Normalized form: Mon..Fri *-*-* 09:00:00
Next elapse: Mon 2026-04-27 09:00:00 KST
From now: 4 days left규칙을 적용하기 전에 의도한 시각인지 확인하는 습관이 운영 사고를 많이 막습니다.
timer 트리거 종류 #
OnCalendar 외에도 여러 트리거가 있습니다.
| 트리거 | 의미 |
|---|---|
OnCalendar= | 달력 시각 (cron 같은 절대 시각) |
OnBootSec= | 부팅 후 N초 |
OnStartupSec= | systemd 시작 후 N초 |
OnUnitActiveSec= | service가 마지막 활성화된 후 N초 |
OnUnitInactiveSec= | service가 종료된 후 N초 |
예를 들어 OnUnitActiveSec=10min은 “이전 실행 후 10분 뒤” — cron의 */10 * * * *와 비슷하지만 이전 실행 시간 기준이라 작업이 길게 끌리면 다음 실행이 자연스럽게 밀립니다. cron은 그냥 두 번 실행됩니다.
Persistent — anacron 역할까지 흡수 #
[Timer]
OnCalendar=daily
Persistent=truePersistent=true는 “마지막 실행 시각을 디스크에 기록하고, 머신이 꺼져 있어 시각을 놓쳤으면 부팅 후 보충 실행” 의미. anacron이 하던 일을 timer가 그대로 흡수합니다.
사용자 timer #
루트 권한 없이 자기 작업만 timer로 돌리는 것도 가능합니다.
$ mkdir -p ~/.config/systemd/user/
# ~/.config/systemd/user/myjob.service, myjob.timer 작성
$ systemctl --user daemon-reload
$ systemctl --user enable --now myjob.timer
$ systemctl --user list-timers사용자 세션이 종료된 후에도 작업이 돌아가려면 loginctl enable-linger <user>로 lingering을 켜야 합니다.
흔히 보는 시스템 timer #
$ systemctl list-timers
NEXT LEFT LAST ... UNIT
Wed 2026-04-23 04:00:00 KST 5h 12min left Tue 2026-04-22 04:00:00 KST ... dnf-makecache.timer
Wed 2026-04-23 06:30:00 KST 7h 42min left Tue 2026-04-22 06:30:00 KST ... logrotate.timer
Wed 2026-04-23 ... ... fstrim.timer
...dnf 메타데이터 갱신, logrotate, fstrim, anacron — RHEL 9 기본 설치만으로도 timer 여러 개가 이미 돌고 있습니다.
cron vs systemd timer — 어느 쪽? #
| 기준 | cron 유리 | timer 유리 |
|---|---|---|
| 단순한 “매분/매일” | ✓ | |
| 셸 한 줄짜리 작업 | ✓ | |
| journald로 출력 통합 | ✓ | |
| 네트워크/마운트 의존성 | ✓ | |
| 자원 제한 (CPU/메모리) | ✓ | |
| 패키지 표준 도구 | ✓ (RHEL 9 트렌드) | |
| 학습 곡선 | ✓ (5분이면 충분) |
운영 권장:
- 기존 작업이 cron으로 돌고 있고 단순하면 그냥 둠
- 새로 추가하는 시스템 작업은 timer로 작성
- journald 통합과 의존성 제어가 필요하면 timer 일찍 도입
디버깅 — 작업이 안 돌아갈 때 #
cron 작업이 안 돌 때 #
# 1. crond가 살아 있나
$ systemctl status crond
# 2. 작업이 등록돼 있나
$ crontab -l # 사용자
$ sudo cat /etc/cron.d/* # 시스템
# 3. cron 자체 로그
$ sudo journalctl -u crond --since "1 hour ago"
# 4. 작업의 stdout/stderr를 어디로 보내는지 확인
# (MAILTO 비어 있으면 메일 안 옴, redirect 없으면 출력 사라짐)
# 5. PATH 문제 — 매뉴얼은 되는데 cron만 실패하면 99% 이것
# crontab 안에서 PATH= 명시하거나 명령에 절대경로 사용systemd timer 작업이 안 돌 때 #
# 1. timer가 active인가
$ systemctl status backup.timer
$ systemctl list-timers backup.timer
# 2. 다음 실행 예정이 의도한 시각인가
$ systemd-analyze calendar 'OnCalendar 표현식'
# 3. service가 실제로 돌았을 때 어떻게 됐나
$ journalctl -u backup.service --since "1 day ago"
# 4. 수동으로 한 번 trigger 해보기
$ sudo systemctl start backup.service
# 5. timer 자체 로그
$ journalctl -u backup.timer수동 trigger는 **“timer 문제인가, service 자체 문제인가”**를 가르는 가장 빠른 검증 방법입니다.
흔한 함정 #
- cron + 환경변수: 매뉴얼 셸과 cron 셸이 다르다는 점을 잊으면 디버깅이 길어집니다. 절대경로 + crontab 안의
PATH=가 가장 안전. - 타임존: cron은 시스템 타임존(
/etc/localtime)으로 동작합니다. 컨테이너에서 UTC로 설정된 호스트에 KST가정 작업을 넣으면 9시간 어긋나요. - DST(서머타임): KST엔 없지만 다른 지역 머신은 매년 두 번 이슈가 생깁니다.
OnCalendar는 DST 전환 처리 규칙이 명시돼 있어 cron보다 안전. - **timer의
Persistent=true**를 켜면 머신을 며칠 꺼뒀다 켰을 때 밀린 작업이 한 번에 실행됩니다. 백업이 동시에 여러 번 돌면 곤란하면 잠금(flock) 또는 멱등 처리. - at 작업은 atd가 죽어 있으면 그대로 사라집니다.
systemctl enable --now atd확인.
기억해 둘 명령 #
| 작업 | 명령 |
|---|---|
| 사용자 crontab 편집 | crontab -e |
| 사용자 crontab 보기 | crontab -l |
| 모든 timer 보기 | systemctl list-timers --all |
| timer 다음 실행 시각 확인 | systemd-analyze calendar '<expr>' |
| at 큐 보기 | atq |
| at 작업 취소 | atrm <id> |
| cron로그 보기 | journalctl -u crond |
| timer 짝 service로그 | journalctl -u <name>.service |
| 수동 trigger | systemctl start <name>.service |
정리 #
- cron — 단순한 반복 작업의 전통 도구.
crontab -e로 사용자 작업,/etc/cron.d/*로 시스템 작업. - anacron — cron이 머신이 꺼져 있어 놓친 일/주/월 작업을 부팅 후 보충. RHEL 9 기본.
- at — 한 번만 예약.
at now + 30 minutes같은 단발성. atd 서비스 필요. - systemd timer —
.timer+.service짝. journald 통합, 의존성, 자원 제어,Persistent=true로 anacron 흡수. 새 작업은 가능하면 여기. - 디버깅 핵심: cron 환경변수, timer는 service 단독 trigger로 분리 검증.
다음 — 컨테이너 입문 #
여기까지가 RHEL 9에서 시간 기반 작업을 운영하는 도구들이었습니다. 다음은 한 머신 안에서 격리된 환경을 여러 개 띄우는 컨테이너로 넘어갑니다.
#7 컨테이너 입문 — Podman/Buildah/Skopeo (Docker와의 차이)에서는 RHEL 9의 컨테이너 표준인 Podman을 다룹니다. Docker와 거의 같은 명령어를 쓰면서도 데몬 없이 동작하는 구조, rootless 컨테이너, Buildah로 이미지를 빌드하는 흐름, Skopeo로 레지스트리 사이를 옮기는 작업까지 한 사이클로 정리합니다.