cron 작업이 실행되지 않을 때 확인할 5가지 원인
cron 작업이 전혀 실행되지 않는다면 PATH, 이스케이프하지 않은 percent 기호, 잘못된 crontab, mail 출력, login shell 의존성을 순서대로 확인합니다.
cron 작업이 실행되지 않는 이유
“전혀 실행되지 않는” cron 작업도 거의 항상 한 번은 실행된 적이 있다. 셸과 다른 환경에서 실행되었고, 첫 1초 안에 실패했으며, 메시지는 사용자가 확인하지 않는 곳으로 전달되었을 수 있다. 대부분의 문제는 다음 5가지 원인으로 설명된다. 검색 경로, percent 기호, 잘못된 crontab 파일, mail로 전달된 출력, login 세션을 전제로 하는 스크립트다.
cron은 crontab 파일을 읽고 일정에 따라 명령을 시작하는 daemon(백그라운드 서비스)이다. cron은 사용자의 .bashrc를 읽지 않고, 터미널을 열지 않으며, login shell을 시작하지도 않는다. 또한 명령이 실패했음을 알려 주지도 않는다. 아래의 각 원인은 이 4가지 사실에서 비롯된다.
다음 항목을 순서대로 확인하되, 먼저 모든 항목에 해당하는 질문부터 시작해야 한다. cron이 실제로 실행을 시작했는가? “cron이 작업을 시작하지 않았다”와 “작업이 시작된 후 종료되었다”는 서로 다른 문제이며 공통점이 없다. 따라서 먼저 이 질문에 답해야 한다.
cron이 실제로 실행되었는가?
배포판 계열에 따라 데몬의 unit 이름이 다르다. 두 이름을 모두 확인한 다음 로그를 읽는다.
systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"Debian과 Ubuntu에서는 unit을 cron이라고 한다. Fedora, Rocky, Alma에서는 crond이라고 한다. 특정 시스템에는 두 이름 중 하나만 존재하므로, 두 명령 중 하나가 알 수 없는 unit이라고 보고하는 것은 정상이며 오류가 아니다.
시스템이 직접 기록한 항목을 읽는다. 가이드에서 복사한 문구를 찾지 않는다. cron 구현과 로깅 설정에 따라 문구가 다르기 때문이다. 확인할 사항은 2가지뿐이다. 스케줄에 지정한 분에 항목이 있는지 확인한다. 해당 항목에 사용자가 지정한 명령이 표시되는지도 확인한다. 명령이 표시된 항목이 있으면 cron의 작업은 완료된 것이며, 실패 원인은 명령 내부에 있다. 항목이 전혀 없으면 cron이 사용자의 스케줄을 읽지 못한 것이며, 이는 아래의 원인 3이다.
일부 이미지는 cron 메시지를 rsyslog를 통해 journal이 아닌 파일에 기록한다. /var/log에서 cron 또는 syslog 이름이 포함된 파일을 찾은 다음 파일의 끝부분을 읽는다.
ls -l /var/log
sudo tail -n 50 /var/log/syslogunit과 로그가 모두 없으면 cron이 설치되지 않았을 수 있다. 최소 구성의 cloud 이미지와 컨테이너에는 cron이 포함되지 않은 경우가 많다.
dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron원인 1: cron에 PATH가 없습니다
대화형 셸은 /etc/profile, ~/.profile, ~/.bashrc와 이 파일들이 source하는 모든 파일에서 PATH를 구성합니다. cron 작업에서는 이러한 과정이 실행되지 않습니다. cron은 자체적으로 구성한 짧은 환경에서 명령을 시작하므로 표준 시스템 디렉터리 외부에 있는 프로그램을 찾지 못합니다. /usr/local/bin, /opt 아래의 항목, 언어 버전 관리자, Python 가상 환경 또는 Go workspace가 원인일 수 있습니다. 작업은 첫 번째 줄에서 실패하고, 셸은 "not found" 형식의 오류를 기록합니다. 정확한 문구는 해당 명령을 실행한 셸에 따라 다릅니다.
작업에서 사용하는 모든 명령의 실제 경로를 확인합니다.
command -v docker
command -v node
readlink -f "$(command -v node)"그런 다음 해당 절대 경로를 작업에 직접 작성하거나, crontab 맨 위에서 PATH를 한 번 설정합니다.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool runecho "$PATH"을 사용해 자신의 시스템에서 목록을 가져온 다음, 대화형 세션에서만 존재하는 항목은 제거합니다. 여기서 중요한 규칙이 하나 있습니다. cron은 이러한 할당 줄에서 변수를 확장하지 않습니다. PATH=$PATH:/usr/local/bin는 $PATH:/usr/local/bin라는 리터럴 텍스트를 저장하므로, 작업의 검색 경로에는 사용할 수 있는 디렉터리가 하나도 남지 않습니다. 전체 목록을 직접 작성해야 합니다.
버전 관리자는 경로만으로 충분하지 않습니다. nvm, pyenv, rbenv 및 asdf는 .bashrc에서 셸 함수 또는 shims 디렉터리를 설치하며, cron 작업은 이 파일을 읽지 않습니다. 버전이 지정된 바이너리를 절대 경로로 호출하거나, 자체 스크립트의 첫 번째 줄에서 관리자의 init 스크립트를 source해야 합니다.
원인 2: 퍼센트 기호가 명령을 끝낸다
crontab의 명령 필드에서 %은 일반 문자가 아니다. 이스케이프되지 않은 첫 번째 %이 명령을 끝낸다. 그 뒤의 모든 내용은 명령의 표준 입력으로 전달되고, 이후의 각 %은 줄 바꿈으로 변환된다. 이는 짧은 입력을 프로그램에 전달하기 위한 실제 cron 기능이다. 동시에 날짜가 포함된 파일 이름이 crontab에서 자주 깨지는 이유이기도 하다.
0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site을 작성하면 tar는 형식이 적용된 날짜를 받지 못한다. cron은 첫 번째 %에서 줄을 자르므로, 셸은 완료되지 않은 명령 치환을 받는다. 줄의 나머지 부분은 표준 입력으로 전달된다. 모든 퍼센트 기호를 백슬래시로 이스케이프해야 한다.
0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site하나의 줄을 두 계층이 순서대로 읽는다. \%은 cron 규칙이며, cron이 어떤 것도 시작하기 전에 적용한다. $(date +\%F)은 명령 치환이며, cron이 시작한 셸이 나중에 적용한다. 각 문자를 어느 계층이 처리하는지 아는 것이 핵심이다.
더 안전한 방법은 로직을 crontab에 전혀 넣지 않는 것이다. 스크립트에 넣으면 퍼센트 기호에 특별한 의미가 없다.
#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site그러면 crontab 줄에는 경로와 리디렉션만 남는다. 한눈에 읽을 수 있는 crontab은 디버깅하기도 쉽다.
원인 3: 어떤 crontab을 편집했는가?
crontab은 하나만 있는 것이 아니다. 소유자가 다르고 필드 수도 다른 여러 파일이 있으며, 잘못된 파일에 작업을 작성하면 해당 작업이 보이지 않는다.
crontab -e는 명령을 실행한 사용자의 crontab을 편집한다.sudo crontab -e는 root의 crontab을 편집한다. 같은 서버를 디버깅하는 두 사람이 서로 다른 파일을 읽게 되는 경우가 많다.sudo crontab -l -u deploy은 다른 사용자의 crontab을 나열한다. 작업을 실행해야 하는 계정에 실제로 어떤 설정이 설치되어 있는지 확인할 때 사용한다./etc/crontab과/etc/cron.d의 모든 파일에는 일정과 명령 사이에 필드가 하나 더 있다. 해당 작업을 실행할 사용자다. 사용자 crontab의 5개 필드로 된 한 줄을/etc/cron.d에 붙여 넣으면 명령의 첫 단어가 사용자 이름으로 해석된다./etc/cron.d의 파일 이름에는 문자, 숫자, 밑줄, 하이픈만 사용할 수 있다.backup.sh또는site.conf라는 파일은 이름만으로도 건너뛴다.backup으로 이름을 바꾼 다음 로그를 다시 확인한다./etc/cron.d의 파일은 root가 소유해야 하며 그룹이나 다른 사용자가 쓸 수 없어야 한다.ls -l /etc/cron.d를 사용하면 이 두 가지를 한 번에 확인할 수 있다./etc/cron.daily과 관련 디렉터리에 배치한 스크립트에도 같은 이름 규칙이 적용되며 실행 비트도 있어야 한다. 실행 비트가 없으면 조용히 건너뛴다./etc/cron.allow과/etc/cron.deny은 crontab을 설치할 수 있는 사용자를 결정한다. 서버에 둘 중 하나라도 있으면 사용자가 crontab을 사용할 수 있다고 가정하기 전에 해당 파일을 읽어야 한다.
스풀 파일을 직접 편집하지 말고 crontab 명령으로 사용자 crontab을 설치한다. crontab이 설치하기 전에 파일을 구문 분석하기 때문이다. 저장할 때는 명령이 출력하는 내용을 확인한다. 파일을 거부하면 이전 버전이 계속 활성 상태로 남고 변경 사항은 적용되지 않는다. 이 상태는 cron이 명령을 무시하는 것과 정확히 같아 보인다.
소유자는 권한도 결정한다. root의 crontab에 있는 작업은 root 소유의 파일을 만들며, 해당 파일을 읽는 애플리케이션이 그 파일에 쓰지 못할 수 있다. 일반 사용자의 crontab에 있는 작업은 root 전용 디렉터리를 읽을 수 없다. 작업에 맞는 소유자를 선택해야 한다. 애플리케이션 유지 관리 작업은 애플리케이션 자체 계정으로 실행해야 한다. 이것이 WordPress wp-cron을 system cron 작업으로 교체하기의 근거다. 작업이 만드는 파일의 모드는 작업이 상속한 umask에서 결정된다. 이 값은 셸의 umask와 다를 수 있다. 작업의 출력 파일을 읽을 수 없다면 umask가 파일 권한을 설정하는 방식을 읽어 보는 것이 좋다.
원인 4: 아무도 읽지 않는 메일로 출력이 전송됨
cron은 작업이 standard output과 standard error에 기록하는 모든 내용을 수집한다. 작업이 무엇이든 출력하면 cron은 해당 텍스트를 로컬 메일 시스템으로 전달한다. 수신자는 crontab 소유자이거나 MAILTO이 지정하는 대상이다. 최소한의 구성만 적용한 VPS에는 일반적으로 MTA(mail transfer agent)가 설치되어 있지 않다. 따라서 아무것도 전달되지 않는다. 오류 메시지는 잠시 존재한 뒤 사라진다. 고장 난 작업이 아무런 메시지도 내지 않는 것처럼 보이는 이유는 이것이다.
출력을 직접 관리하는 파일로 보내야 한다.
0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1>>는 standard output을 파일에 추가한다. 2>&1는 standard error를 현재 standard output이 가리키는 대상으로 보낸다. 따라서 리디렉션 뒤에 작성해야 한다. 반대로 2>&1 >> file와 같이 작성하면 standard error는 원래 대상을 유지한다. 그러면 확인하려는 오류가 파일에 기록되지 않는다.
journal도 적합한 대상이다. logger는 선택한 태그를 사용해 syslog에 기록한다.
0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-sitejournalctl -t backup-site로 기록된 내용을 다시 확인한다. 이렇게 하면 작업의 자체 출력이 cron 항목과 가까운 위치에 남으므로 시간 순서를 쉽게 확인할 수 있다. 어떤 사용자가 어떤 명령을 서버에서 실행했는지도 기록해야 한다면 별도 시스템이 필요하다. 서버에서 사용자 명령 감사에서 해당 내용을 다룬다.
crontab 맨 위에 MAILTO=""를 작성하면 그 아래 작업에 대한 메일 전송이 중지된다. MAILTO을 실제 주소로 설정해도 정상적으로 동작하는 MTA가 있어야만 효과가 있다. 따라서 메일 전송에 의존하기 전에 실제로 서버 밖으로 메일이 전송되는지 확인해야 한다.
디버깅할 때 지켜야 할 규칙이 하나 있다. 절대로 > /dev/null 2>&1를 추가하지 않는다. 모든 crontab에서 가장 흔히 볼 수 있는 줄이며, 가지고 있는 유일한 증거를 삭제한다. 작업이 정상적으로 동작한 뒤 필요하다면 다시 추가한다.
원인 5: 스크립트가 cron에서 제공하지 않는 환경을 가정한다
명령을 찾고 출력을 캡처했다면, 이제 남은 문제는 세션이 자동으로 제공하던 나머지 환경이다.
- 셸이 bash가 아닐 수 있다.
ls -l /bin/sh으로 확인한다. Debian과 Ubuntu에서는 이 값이 dash를 가리키므로 이중 대괄호 테스트, 배열 및source은 구문 오류로 실패한다. 스크립트에#!/bin/bash줄을 지정하고 스크립트를 호출하거나, crontab의 맨 위에서SHELL을 설정한다. - 작업 디렉터리가 원래 작업하던 디렉터리가 아니다. 모든 경로에 절대 경로를 사용하거나, 스크립트의 첫 줄에서
cd를 사용해 디렉터리를 변경한다. 상대 경로는 작업이 “직접 실행하면 동작하는” 가장 흔한 단일 원인이다. - 로캘이 세션의 로캘과 다르다. 날짜나 숫자의 형식을 지정하거나 텍스트를 정렬하는 작업은 다른
LANG에서 다른 출력을 만들 수 있다. 이후 단계에서 해당 출력을 구문 분석한다면 추측하지 말고 스크립트에서 로캘을 설정한다. - TTY(터미널)가 없다. 확인을 요청하거나 편집기를 열거나 진행률 표시줄을 그리는 명령은 멈추거나 종료될 수 있다. 도구가 제공하는 비대화형 플래그를 추가한다.
- SSH agent가 없다.
SSH_AUTH_SOCK은 cron 환경에 없으므로, agent가 로드되어 있어 동작하던ssh또는rsync명령이 이제 인증에 실패한다. 작업 전용 키를 제공하고, 해당 키의 소유자를 작업을 실행하는 사용자로 설정한다. - 사용자 세션 버스가 없으므로 cron 작업에서
systemctl --user은XDG_RUNTIME_DIR이 설정될 때까지 실패한다. systemd unit을 사용하는 편이 더 낫다.
Fedora, Rocky 및 Alma에는 확인할 원인이 하나 더 있다. SELinux는 cron 작업을 제한하므로, 파일 권한이 올바르게 보여도 예상과 다른 레이블이 지정된 경로에 작업이 접근하면 거부된다. sudo ausearch -m avc -ts recent으로 거부 기록을 확인하고, 비활성화하기 전에 서버를 위한 SELinux 기본 사항을 읽는다.
cron의 환경을 확인하는 1분 프로브
cron 환경에 무엇이 들어 있는지 추측하지 말고 직접 확인한다. 모든 내용을 덤프하는 스크립트를 작성하고, 이를 1분마다 실행하도록 예약한 다음 잠시 기다렸다가 파일을 읽는다.
cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.sh실제 작업을 실행하는 사용자의 crontab에 양쪽 경로를 모두 절대 경로로 지정한 한 줄을 추가한다.
* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&11분 기다린 다음 /home/deploy/cron-probe.log를 읽고, 동일한 명령을 자신의 셸에서 실행한 결과와 비교한다. PATH 줄, 작업 디렉터리, 로케일만으로도 대개 실패 원인을 파악할 수 있다. 이 설정에서 다음 2가지는 주의해야 한다. 퍼센트 기호는 cron의 규칙이 적용되지 않는 스크립트 내부에 있고, 로그 경로는 작업 사용자가 쓸 수 있는 경로다.
원인을 확인한 즉시 해당 crontab 줄을 삭제한다. 1분마다 실행되면서 파일에 내용을 추가하는 작업은 작은 디스크를 가득 채울 수 있으며, 그 과정이 조용히 진행된다.
스케줄이 의도한 것과 같은지 확인한다
사용자 crontab의 한 줄은 5개의 필드로 시작한다. 순서는 분, 시, 일, 월, 요일이다. 이 중 2개는 함께 사용할 때 예상과 다르게 동작할 수 있다.
일과 요일을 모두 제한하면, 즉 둘 다 *가 아니면 cron은 두 필드 중 하나라도 일치할 때 작업을 실행한다. 0 0 13 * 5는 "13일의 금요일"을 의미하지 않는다. 매월 13일 자정과 매주 금요일 자정에 실행된다. 특정한 하루에만 실행하려면 두 필드 중 하나를 *로 두고, 다른 필드는 스크립트 내부에서 확인한다.
cron은 시스템 시간대를 사용한다. 많은 VPS 이미지가 UTC(협정 세계시)로 설정되어 제공되므로, 03:00으로 예약한 작업이 03:00 UTC에 실행될 수 있다. 이 시간은 사용자의 현지 시간으로 오후일 수 있다. timedatectl을 실행하면 시스템이 실제로 사용하는 시간대를 출력한다. 노트북과 같다고 가정하지 말고 서버의 설정을 직접 확인한다.
알아 두어야 할 스케줄 관련 함정이 2가지 더 있다. @reboot은 cron 자체가 시작될 때 실행된다. 이는 네트워크가 준비되는 시점과 같지 않다. 따라서 DNS나 원격 호스트가 필요한 작업은 부팅 시 실패하고, 이후 수동으로 실행하면 매번 성공할 수 있다. 또한 이전 작업이 아직 실행 중이어도 느린 작업이 다시 시작되는 것을 막아 주지 않는다. 이런 작업은 잠금으로 감싼다.
*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1flock -n은 잠금이 이미 사용 중이면 즉시 종료한다. 따라서 중복 실행이 첫 번째 실행 위에 계속 쌓이지 않고 중단된다.
systemd timer가 더 적합한 경우
cron은 한 가지 작업에는 적합하다. 지정한 시간에 명령을 실행하는 작업이다. 그 외의 기능은 취약하다. timer를 사용하면 redirect 없이 journal을 확인할 수 있고, 나중에 조회할 수 있는 종료 상태도 제공된다. 또한 network-online.target에 따른 실행 순서를 지정할 수 있고, 100대의 서버가 모두 같은 초에 시작하지 않도록 무작위 지연도 설정할 수 있다. 작업에 이러한 기능이 필요하다면 VPS에서 systemd service와 timer 사용하기가 crontab 한 줄을 계속 관리하는 것보다 수월하다. service 부분을 작성할 때는 cron에는 없는 질문을 하나 확인해야 한다. 즉, unit이 작업이 실제로 시작되었다는 사실을 어떻게 인식하는지 정해야 한다. 따라서 먼저 simple, forking 및 notify unit에서 Type=의 의미를 읽어야 한다. 기본 type에서 스크립트가 자체적으로 daemonize하면 unit은 뒤에서 실행 중인 프로세스가 없는데도 active 상태로 남기 때문이다. 재시도 동작도 이 설정에 포함해야 한다. systemd restart 정책이 실패 후의 동작을 결정하기 때문이다. cron에는 이 질문에 대한 기능이 전혀 없다.
간단한 작업에는 cron을 계속 사용한다. 의존성이나 재시도 정책이 있는 작업은 timer로 옮긴다. 두 방식은 같은 서버에서 함께 실행할 수 있다. 따라서 이 마이그레이션을 한 번에 완료할 필요는 없다.
FAQ
cron에서 수동으로 실행하면 작동하지만 cron에서 실행하면 실패하는 이유는 무엇입니까?
셸과 cron의 환경이 다르기 때문입니다. 로그인 셸은 /etc/profile 및 ~/.bashrc를 읽고, 이 파일들이 PATH, 로캘, 에이전트 변수를 설정합니다. cron은 이러한 설정 없이 명령을 시작하며, 작업 디렉터리도 다르고 경우에 따라 다른 셸을 사용합니다. 모든 명령에 절대 경로를 사용하고, 필요한 값을 crontab의 상단이나 스크립트 내부에서 설정합니다. 또한 1분 후 실행되는 테스트 작업을 예약하고, 이 작업에서 env | sort, pwd 및 id을 로그 파일에 기록합니다. 그러면 추측하지 않고 cron의 실제 환경을 확인할 수 있습니다.
cron이 실제로 작업을 실행했는지 어떻게 확인합니까?
데몬의 로그를 확인합니다. Debian 및 Ubuntu에서는 journalctl -u cron을 사용하고, Fedora, Rocky 및 Alma에서는 journalctl -u crond을 사용합니다. 일부 이미지에서는 rsyslog를 통해 메시지를 /var/log 아래의 파일로 전달하기도 합니다. 일정에 지정한 분에 해당하는 항목을 찾고, 해당 항목에 명령이 표시되는지 확인합니다. 항목이 없으면 cron이 해당 일정을 읽지 못한 것입니다. 올바른 crontab을 편집했는지 확인합니다. 항목은 있지만 결과가 없으면 명령이 시작된 후 종료된 것입니다. 리디렉션을 사용해 출력을 수집합니다.
crontab 안에서 date +%Y이 작동하지 않는 이유는 무엇입니까?
cron은 명령 필드에서 %을 특수 문자로 처리합니다. 이스케이프되지 않은 첫 번째 %에서 명령이 끝나고, 그 뒤의 모든 내용은 해당 명령의 표준 입력으로 전달됩니다. 이후의 각 %은 줄 바꿈으로 변환됩니다. 따라서 날짜 형식을 사용한 파일 이름은 해당 프로그램에 전달되지 않습니다. 각 퍼센트 기호를 \%로 이스케이프하거나, 명령을 스크립트로 옮긴 후 cron에서 해당 스크립트를 호출합니다. 스크립트 내부에서는 퍼센트 기호에 특별한 의미가 없습니다.
cron 작업의 출력은 어디로 전달됩니까?
출력은 로컬 메일 시스템으로 전달되며, 수신자는 crontab 소유자 또는 MAILTO에 지정된 대상입니다. 대부분의 VPS 이미지에는 메일 전송 에이전트가 설치되어 있지 않으므로 메시지가 삭제되고 작업이 아무 출력 없이 실행된 것처럼 보입니다. >> /path/to/log 2>&1을 사용해 출력을 파일로 리디렉션합니다. 표준 오류가 표준 출력을 따르도록 이 순서를 유지합니다. 또는 logger -t myjob을 통해 전달한 후 journalctl -t myjob으로 다시 확인합니다. 아직 디버깅 중이라면 > /dev/null 2>&1을 사용하지 않습니다.
cron과 systemd timer 중 무엇을 사용해야 합니까?
고정된 시간에 간단한 명령을 실행하려면 cron을 사용합니다. 특히 systemd를 실행하지 않는 시스템으로 작업을 옮겨야 할 가능성이 있다면 cron이 적합합니다. 리디렉션 없이 출력을 journal에 기록하거나, 종료 상태를 조회하거나, 네트워크가 준비된 후 실행하도록 순서를 지정하거나, 시작 지연 시간을 무작위화하거나, 실패 후 재시도 정책을 사용하려면 timer를 사용합니다. 두 방식은 같은 서버에서 함께 실행할 수 있습니다. 따라서 필요성이 생긴 작업부터 하나씩 이전할 수 있습니다.