Published on

리눅스 crontab 안 돈다? 로그·환경·쉘 차이

Authors

서버에서 crontab 이 “분명 등록했는데 안 돈다”는 상황은 대개 세 가지 축으로 수렴합니다.

  1. 로그가 없어서 실패 지점을 모른다
  2. 인터랙티브 셸과 크론 환경이 다르다
  3. 실행 셸(/bin/sh) 차이로 스크립트가 깨진다

이 글은 위 3가지를 한 번에 끝내는 진단 체크리스트와, 운영에서 재발을 막는 템플릿까지 제공합니다.

1) 크론은 실행됐는데 “조용히” 실패한다

크론은 기본적으로 표준 출력/표준 에러를 메일로 보내거나(설정에 따라), 아무 데도 남기지 않고 끝나 버립니다. 그래서 첫 단계는 반드시 로그를 남기는 것입니다.

1-1. 가장 먼저 해야 할 것: 리다이렉션으로 로그 고정

크론 엔트리에 아래처럼 로그를 붙입니다.

* * * * * /usr/local/bin/myjob.sh >> /var/log/myjob.log 2>&1
  • >> 는 append(누적)
  • 2>&1 는 표준 에러도 같은 파일로

로그가 커지는 게 걱정이면 logrotate 를 붙이거나, 날짜별 파일로 분리합니다.

* * * * * /usr/local/bin/myjob.sh >> /var/log/myjob-$(date +\%F).log 2>&1

주의: 크론에서 % 는 줄바꿈으로 해석되므로 \%F 처럼 이스케이프가 필요합니다.

1-2. 시스템 로그에서 크론 실행 여부 확인

배포판마다 다르지만 보통 아래 중 하나에 기록됩니다.

# Debian/Ubuntu 계열
sudo grep CRON /var/log/syslog | tail -n 50

# RHEL/CentOS/Amazon Linux 계열
sudo tail -n 50 /var/log/cron

# systemd-journald 사용 시
sudo journalctl -u cron -n 100 --no-pager
sudo journalctl -u crond -n 100 --no-pager

여기서 확인할 포인트는 두 가지입니다.

  • 스케줄 시각에 크론이 실제로 트리거 되었는가
  • 트리거 되었는데도 스크립트가 내부에서 실패했는가

트리거 자체가 없다면 cron/crond 서비스 상태부터 봐야 합니다.

sudo systemctl status cron
sudo systemctl status crond

2) “내 터미널에서는 되는데 크론에서는 안 된다”의 정체: 환경변수

크론은 로그인 셸이 아닙니다. 따라서 터미널에서 자동으로 잡히던 환경이 거의 없습니다.

2-1. PATH가 다르다: 명령을 못 찾는다

가장 흔한 실패는 PATH 차이입니다. 예를 들어 터미널에서는 python, node, aws 가 되는데 크론에서는 command not found 가 납니다.

크론에서 PATH 를 고정하는 가장 단순한 방법은 crontab 상단에 명시하는 겁니다.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

* * * * * myjob.sh >> /var/log/myjob.log 2>&1

또는 스크립트에서 실행 파일을 절대 경로로 호출하세요.

/usr/bin/python3 /opt/app/job.py

절대 경로는 다음으로 확인합니다.

command -v python3
command -v node
command -v aws

2-2. HOME, USER, LANG, TZ도 다르다

크론은 HOME 이 기대와 다르거나, LANGC 로 잡혀 인코딩 문제가 터질 수 있습니다. 특히 CSV/한글 로그/외부 파일을 다루는 작업에서 자주 재현됩니다.

아래처럼 크론 상단에 최소 환경을 고정해두면 디버깅이 쉬워집니다.

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/home/ubuntu
LANG=ko_KR.UTF-8
TZ=Asia/Seoul

* * * * * /usr/local/bin/myjob.sh >> /var/log/myjob.log 2>&1

인코딩 이슈가 의심되면 관련 글도 함께 참고하세요.

2-3. .bashrc/.profile 을 크론이 읽지 않는다

터미널에서 잘 되는 이유가 ~/.bashrc 에 있는 exportconda activate, nvm use 같은 초기화 때문이라면, 크론에서는 그대로 실패합니다.

해결책은 두 가지 중 하나입니다.

  1. 크론에 필요한 환경을 직접 정의
  2. 스크립트에서 필요한 초기화만 명시적으로 source

예시:

#!/usr/bin/env bash
set -euo pipefail

# 필요한 것만 명시적으로 로드
source /etc/profile
source "$HOME/.bashrc"

# 이후 작업
python3 /opt/app/job.py

다만 .bashrc 는 인터랙티브 전용 설정이 섞여 있을 수 있어, 운영 스크립트에서는 가급적 “작업 전용 env 파일”을 따로 두는 편이 안전합니다.

3) 크론의 기본 셸은 /bin/sh: Bash 문법이 깨진다

많은 시스템에서 크론은 기본적으로 /bin/sh 로 커맨드를 실행합니다. 그런데 스크립트가 Bash 전용 문법을 쓰면 크론에서만 실패합니다.

대표적인 예:

  • [[ ... ]] 테스트
  • source 키워드
  • 배열 문법
  • set -o pipefail 지원 여부

3-1. crontab에 SHELL을 지정하거나, 스크립트 shebang을 고정

crontab 상단에 지정:

SHELL=/bin/bash

* * * * * /usr/local/bin/myjob.sh >> /var/log/myjob.log 2>&1

또는 스크립트 첫 줄을 명확히:

#!/usr/bin/env bash

그리고 Bash 스크립트라면 실패 전파를 확실히 하기 위해 아래 패턴을 추천합니다.

#!/usr/bin/env bash
set -euo pipefail

trap 'echo "failed at line=$LINENO"' ERR

some_command | another_command

set -e 가 기대대로 동작하지 않는 파이프라인/서브셸 케이스는 별도 함정이 많습니다. 아래 글을 함께 보면 “크론에서는 조용히 성공처럼 보이는데 실제론 실패” 같은 상황을 줄일 수 있습니다.

4) 경로/작업 디렉터리 문제: 상대경로가 깨진다

크론은 보통 작업 디렉터리가 / 이거나, 사용자의 홈이 아닐 수 있습니다. 그래서 상대경로(./data, ../config)는 크론에서 자주 깨집니다.

4-1. 스크립트 시작 시 작업 디렉터리를 고정

#!/usr/bin/env bash
set -euo pipefail

cd /opt/app

./bin/run-task.sh

또는 “스크립트 위치 기준”으로 이동:

#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
cd "$SCRIPT_DIR"

./run-task.sh

5) 권한/실행 비트/소유자: 실행 자체가 막힌다

  • 스크립트에 실행 비트가 없거나
  • 실행 사용자가 파일/디렉터리에 접근 권한이 없거나
  • sudo 가 비밀번호를 요구하는데 TTY가 없어 멈추는 경우

점검:

ls -l /usr/local/bin/myjob.sh
id

크론에서 sudo 를 써야 한다면 sudoers 에서 NOPASSWD를 최소 권한으로 부여하고, 가능한 한 “크론은 해당 권한을 가진 계정으로 직접 실행”하는 구조가 낫습니다.

6) crontab 문법/시간대/개행 함정

6-1. 시간대(TZ) 불일치

서버가 UTC인데 사람은 KST로 보고 있는 경우가 흔합니다.

  • 시스템 시간 확인
date
timedatectl
  • 크론에 TZ=Asia/Seoul 을 명시하거나(지원되는 구현에서), 시스템 시간대를 맞춥니다.

6-2. 개행/CRLF 문제

윈도우에서 만든 스크립트가 CRLF면 크론에서 bad interpreter 류로 터질 수 있습니다.

file /usr/local/bin/myjob.sh
sed -n '1p' /usr/local/bin/myjob.sh

필요 시 변환:

sed -i 's/\r$//' /usr/local/bin/myjob.sh

7) 재현 가능한 “진단용 크론” 템플릿

문제가 복잡할수록, 한 번에 많은 걸 하려다 원인을 놓칩니다. 아래처럼 “환경 덤프 + 최소 작업”으로 재현하세요.

* * * * * env | sort > /tmp/cron-env.txt
* * * * * (date; whoami; pwd; command -v python3) >> /tmp/cron-probe.log 2>&1
  • cron-env.txt 를 터미널에서의 env | sort 결과와 비교하면 차이가 바로 보입니다.

8) 운영에서 재발 방지: 스크립트를 서비스처럼 다루기

크론을 단순 스케줄러로만 쓰면, 실패 감지/재시도/중복 실행 방지가 약합니다. 최소한 아래 3가지는 갖추는 편이 좋습니다.

8-1. 락으로 중복 실행 방지

* * * * * flock -n /tmp/myjob.lock /usr/local/bin/myjob.sh >> /var/log/myjob.log 2>&1
  • -n 은 이미 실행 중이면 즉시 종료

8-2. 타임아웃으로 무한 대기 방지

* * * * * timeout 300 /usr/local/bin/myjob.sh >> /var/log/myjob.log 2>&1

8-3. 실패 시 알림(최소 메일이라도)

크론의 MAILTO 를 이용하면 간단히 알림을 받을 수 있습니다.

MAILTO=ops@example.com

* * * * * /usr/local/bin/myjob.sh

메일 환경이 없다면, 슬랙 웹훅/알림 시스템으로 보내는 방식으로 확장하세요.

9) 빠른 체크리스트(이 순서대로 보면 대부분 잡힌다)

  1. 시스템 로그에서 크론 트리거 여부 확인(syslog, /var/log/cron, journalctl)
  2. 크론 엔트리에 >> ... 2>&1 로 로그 고정
  3. PATH, HOME, LANG, TZ 등 환경변수 차이 확인(env | sort 비교)
  4. 상대경로 제거, cd 로 작업 디렉터리 고정
  5. 기본 셸이 /bin/sh 인지 확인하고 SHELL=/bin/bash 또는 shebang 고정
  6. 실행 파일 절대 경로 사용, 권한/소유자/CRLF 점검
  7. flock, timeout, 알림으로 운영 안정성 보강

크론 이슈는 “원인이 하나”인 경우도 많지만, 실제로는 PATH 문제와 셸 문법 문제가 동시에 숨어 있는 식으로 겹치는 경우가 흔합니다. 위 템플릿대로 로그와 환경을 먼저 고정해두면, 다음부터는 같은 문제를 훨씬 빠르게 끝낼 수 있습니다.