RHEL 중급 #6 작업 스케줄링 — cron, systemd timer, at

10 분 소요

운영하다 보면 시간 기반으로 돌려야 하는 작업이 끝없이 생깁니다. 매일 새벽 백업, 매주 보고서 메일, 매분 헬스 체크, 한 시간 뒤에 한 번만 돌릴 스크립트. RHEL 9에는 이런 작업을 다루는 도구가 cron, anacron, at, systemd timer로 나뉘어 있습니다. 이 글에서는 각각을 언제 쓰는지가 핵심입니다.

RHEL 중급 시리즈에서 이번 글의 위치:

네 가지 도구를 한 줄로 #

도구용도머신 꺼져 있던 시간비고
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이 보는 파일은 두 갈래:

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)

자주 쓰는 패턴:

crontab 예제
# 매분
* * * * * /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
# 등록/수정 (에디터로 열림)
$ crontab -e

# 보기
$ crontab -l

# 비우기
$ crontab -r

# 다른 사용자 것 (root만)
$ sudo crontab -u alice -l

crontab -e로 편집해야 하는 이유: /var/spool/cron/<user>를 직접 vim으로 열면 cron이 변경을 감지하지 못합니다. crontab -e는 변경 후 cron에 시그널을 보내요.

시스템 crontab vs 사용자 crontab #

/etc/crontab/etc/cron.d/*사용자 컬럼이 하나 더 있습니다.

/etc/cron.d/myjob
# 분 시 일 월 요일  사용자  명령
*/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.sh

MAILTO=가 비어 있지 않으면 cron이 작업의 stdout/stderr를 메일로 보냅니다. 메일 전송이 막혀 있는 환경에서는 스크립트 안에서 직접 로그 파일로 redirect 하는 게 표준입니다.

로그 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이 작동해 “마지막 실행에서 지정 기간이 지났으면 지금 실행” 합니다.

/etc/anacrontab
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의 영역.

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 3

at도 cron처럼 권한 제어 파일이 있습니다: /etc/at.allow, /etc/at.deny.

at은 batch 라는 변형도 제공합니다. 시스템 부하가 낮을 때(load average < 1.5) 실행되는 모드.

batch — 부하 낮을 때 실행
$ 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 (언제 실행할지).

/etc/systemd/system/backup.service
[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
/etc/systemd/system/backup.timer
[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 --all

OnCalendar 문법 #

cron의 * * * * *와 비교했을 때 systemd가 한결 명확합니다.

OnCalendar 예제
# 매분
*-*-* *:*: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=true

Persistent=true는 “마지막 실행 시각을 디스크에 기록하고, 머신이 꺼져 있어 시각을 놓쳤으면 부팅 후 보충 실행” 의미. anacron이 하던 일을 timer가 그대로 흡수합니다.

사용자 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 #

현재 활성 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 작업이 안 돌 때 #

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 작업이 안 돌 때 #

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
수동 triggersystemctl 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로 레지스트리 사이를 옮기는 작업까지 한 사이클로 정리합니다.

X