🔍 Tech 🔍/Server

AWS EC2 Linux Ubuntu cron 스케줄러 + Node.js

Yeonhub 2024. 10. 2. 20:31

https://youtu.be/mxZ8yeV3z1A?si=144Je9Vo2nqepmqd

1. 스케줄러

매일 아침 같은 시간에 알람이 울리는 것처럼 서버에서도 정해진 날짜, 요일, 시간에 특정 기능을 해야 할 경우가 있습니다.
개발 서버를 예로 들면, 정기적인 백업, 주문 재고 알림, 가격 변동, 구독 계정 활성화/비활성화 등 특정 시간에 한 번만 실행이 되거나 주기적으로 실행이 될 필요가 있는 기능들이 있습니다.
이를 위해 필요한 것이 서버 스케줄러인데, 오늘은 제목에서 알 수 있듯이 JavaScript 파일을 Linux에 기본 내장된 기능인 cron 스케줄러에 등록해 보겠습니다.
 

2. cron

https://help.ubuntu.com/community/CronHowto

 

CronHowto - Community Help Wiki

Introduction Cron is a system daemon used to execute desired tasks (in the background) at designated times. A crontab file is a simple text file containing a list of commands meant to be run at specified times. It is edited using the crontab command. The c

help.ubuntu.com

AWS EC2 인스턴스를 Linux의 Ubuntu로 생성하면 기본적으로 cron이 설치되어있습니다.
만약 설치가 되어있는지 확인하고 싶다면 아래 명령어 중 하나로 확인 가능합니다.
 

2-1) 서비스 확인

systemctl status cron

정상적으로 설치되었고, 백그라운드에서 실행 중 일 때

● cron.service - Regular background program processing daemon
     Loaded: loaded (/lib/systemd/system/cron.service; enabled; vendor preset: enabled)
     Active: active (running) since ddd yyyy-mm-dd hh:mm:ss KST; mm months d days ago
       Docs: man:cron(8)
   Main PID: 443 (cron)
      Tasks: 1
     Memory: 39.5M
     CGroup: /system.slice/cron.service
             └─443 /usr/sbin/cron -f

 

2-2) 스케줄러 목록 확인

crontab -l

정상적으로 설치된 경우

# Edit this file to introduce tasks to be run by cron.
# 
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
# 
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
# 
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
# 
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
# 
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
# 
# For more information see the manual pages of crontab(5) and cron(8)
# 
# m h  dom mon dow   command
.
.
.

 

2-3) cron 패키지 설치 확인

dpkg -l | grep cron

cron 패키지가 존재할 경우

ii  cron                               3.0pl1-136ubuntu1                 amd64        process scheduling daemon

 
만약 위의 예시처럼 나오지 않고 'not found', 'command not found' 가 출력이 된다면 cron을 설치해야 합니다.

- Ubuntu 예시

// 1. 패키지 업데이트
sudo apt update

// 2. cron 설치
sudo apt install cron

// 3. cron 실행
sudo systemctl start cron

// 4. cron 자동 실행
sudo systemctl enable cron

 

3. JavaScript (Node.js)

HTML 안에서만 사용 가능한 JavaScript를 서버에서 사용하기 위해선 Node가 필수적입니다. 그러나 AWS EC2 Ubuntu 인스턴스 생성 시 기본적으로 Node가 설치되어 있지만, 버전이 상당히 낮습니다.
포스팅 날짜 기준으로 20 버전이 LTS인데 Ubuntu의 기본 Node는 12 또는 14 버전이 설치되어 있을 겁니다.
 

node -v 로 직접 확인해 보면 알 수 있습니다.
ubuntu@ip-000-00-00-000:~$ node -v
v20.13.1

 
(생략 가능)
Ubuntu 서버의 Node 버전 업데이트 방법은 검색하면 많이 나오니 쉽게 따라 할 수 있습니다.
개인적으로 여러 Node 버전을 쉽게 설치하고 필요할 때마다 버전을 변경할 수 있는 NVM (node version manager) 방식을 추천합니다.
 
https://github.com/nvm-sh/nvm

 

GitHub - nvm-sh/nvm: Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions

Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions - nvm-sh/nvm

github.com

 
아래는 네이버 증권에서 KOSPI 지수를 스크래핑하는 간단한 코드 예시입니다.

// scrapingKOSPI.js

const axios = require('axios');
const cheerio = require('cheerio');

const getKOSPI = async () => {
    const result = await axios.get('https://finance.naver.com/sise/sise_index.nhn?code=KOSPI');
    const $ = cheerio.load(result.data);
    const kospiValue = $('#now_value').text();
    console.log(`KOSPI value: ${kospiValue}`);
};

getKOSPI();

 
만약 위 코드를 실행하려면 커멘드에 node scrapingKOSPI.js 를 입력해서 해당 함수를 호출할 텐데 해당 파일을 실행시키는 방법은 서버에서도 똑같고 스케줄러에 등록 후 실행되는 것 또한 node scrapingKOSPI.js가 됩니다.

> node scrapingKOSPI.js
> KOSPI value: 2,578.52

 
만약 10분마다 해당 함수가 실행되도록 스케줄러에 등록하면, 서버는 10분마다 같은 명령어를 내게 됩니다.

node scrapingKOSPI.js
// 10분
node scrapingKOSPI.js
// 10분
node scrapingKOSPI.js
// 10분
node scrapingKOSPI.js
// 10분
node scrapingKOSPI.js
.
.
.

 

4. crontab

cron을 사용할 때 언제, 어떤 파일을 실행시킬지 스케줄러에 등록하는 것은 crontab에서 진행합니다.
 
이제 cron 스케줄러에 scrapingKOSPI.js 파일을 등록할 텐데 등록에 앞서 cron은 날짜, 요일, 날짜 기반으로 실행되기 때문에 작성법을 간단하게 알아야 합니다.

java67.com

Linux Ubuntu의 경우 총 5자리를 입력하게 되는데, 앞에서부터 분-시-일-월-요일이 됩니다.

* * * * *
| | | | |
| | | | +---- 요일 (0 - 7) (0과 7은 일요일)
| | | +------ 월 (1 - 12)
| | +-------- 일 (1 - 31)
| +---------- 시 (0 - 23)
+------------ 분 (0 - 59)

# 매일 자정에 명령 실행
0 0 * * * /path/to/

# 매시간 15분마다 명령 실행
*/15 * * * * /path/to/

# 매주 월요일 오전 9시에 명령 실행
0 9 * * 1 /path/to/

# 매달 1일 오전 6시에 명령 실행
0 6 1 * * /path/to/

# 매일 오후 5시 30분에 명령 실행
30 17 * * * /path/to/

 
시간을 정했으면 어떤 실행 파일로 어떤 경로에 있는 파일을 실행시킬지 등록해 주면 됩니다.
서버 시간 기준으로 평일 08시부터 18시까지 1시간마다 node scrapingKOSPI.js 를 하도록 등록해 보겠습니다.
 

4-1)  스케줄러 목록 수정

crontab -e

 
0 : 매시 정각
8-18 : 09시-18시
* : 매일
* : 매달
1-5 : 월-금
 

# 은 주석입니다.
ubuntu@ip-000-00-00-000:~$ crontab -e
# Edit this file to introduce tasks to be run by cron.
# 
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
# 
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
# 
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
# 
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
# 
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
# 
# For more information see the manual pages of crontab(5) and cron(8)
# 
# m h  dom mon dow   command

# KOSPI
0 8-18 * * 1-5 node /home/ubuntu/kospi/scrapingKOSPI.js

 
등록 완료 시 CTRL + X 로 저장 후 종료
 

4-2) 스케줄러 목록 확인

crontab -l
ubuntu@ip-000-00-00-000:~$ crontab -l
# Edit this file to introduce tasks to be run by cron.
# 
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
# 
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
# 
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
# 
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
# 
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
# 
# For more information see the manual pages of crontab(5) and cron(8)
# 
# m h  dom mon dow   command

# KOSPI
0 8-18 * * 1-5 node /home/ubuntu/kospi/scrapingKOSPI.js

 

5. 로그 기록

현재 코드는 코스피 지수를 console.log로만 출력하기 때문에 직접 실행하지 않는 이상 확인이 불가능하고, 기록되지 않기 때문에 의미가 없습니다.
 
따라서 Node에 기본 내장된 모듈인 fs을 활용해 로그를 기록하도록 하겠습니다.

const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');

// 로그 기록 함수
// 성공 시 - logs/kospi.log 파일에 기록
// 실패 시 - logs/error.log 파일에 기록
const logger = (isError, log) => {
    const logPath = isError ? `${dir}/error.log` : `${dir}/kospi.log`;
    const currentTime = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' });
    fs.appendFile(logPath, `${currentTime} - ${log}\n`, (error) => {
        if (error) console.error(error);
    });
}

const getKOSPI = async () => {
    try {
        const result = await axios.get('https://finance.naver.com/sise/sise_index.nhn?code=KOSPI');
        const $ = cheerio.load(result.data);
        const kospiValue = $('#now_value').text();
        console.log(`KOSPI value: ${kospiValue}`);
        logger(false, `KOSPI value: ${kospiValue}`);
    } catch (error) {
        console.error(error);
        logger(true, `Failed : ${error}`);
    }
};

getKOSPI();

 
로그 기록 경로를 'logs/error.log', 'logs/kospi.log'로 지정했기 때문에 실제 logs 폴더 경로와 log 파일 경로에 주의해야 합니다.

📦kospi
 ┣ 📂logs
 ┃ ┣ 📜error.log
 ┃ ┗ 📜kospi.log
 ┣ 📂node_modules
 ┣ 📜package-lock.json
 ┣ 📜package.json
 ┗ 📜scrapingKOSPI.js

 

fs.appendFile은 log 파일이 없다면 생성하지만, 경로 폴더가 존재하지 않는다면 에러가 발생합니다.
이를 방지하기 위해 폴더 확인 함수 existsSync와 폴더 생성 함수 mkdirSync 함수를 사용해도 좋습니다.

 

const logger = (isError, log) => {
    const dir = 'logs';
    
    // 폴더 존재 확인
    if (!fs.existsSync(dir)) {
    
    // 폴더 생성
        fs.mkdirSync(dir);
    }
    const logPath = isError ? `${dir}/error.log` : `${dir}/kospi.log`;
    const currentTime = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' });
    fs.appendFile(logPath, `${currentTime} - ${log}\n`, (error) => {
        if (error) console.error(error);
    });
}

 
기록된 log 예시

// kospi.log

2024. 10. 1. 오후 3:00:00 - KOSPI value: 2,565.22
2024. 10. 1. 오후 4:00:00 - KOSPI value: 2,565.22
2024. 10. 1. 오후 5:00:00 - KOSPI value: 2,565.22

 

6. 스케줄러에 등록한 파일이 실행되지 않을 때

 
우선 직접 터미널에서 실행해 본다.

node scrapingKOSPI.js

 

6-1)  직접 터미널에서 실행 시 오류

6-1-1) 서버에 node 설치 확인

node -v


6-1-2) 올바른 경로에서 명령어 입력했는지 확인

ubuntu@ip-000-00-00-000:~/kospi node scrapingKOSPI.js​


6-1-3) 실행하려는 파일의 라이브러리가 서버에 설치되었는지 확인
node_modules 폴더

6-1- 4) 실행하려는 파일의 라이브러리의 경로가 맞게 설정되었는지 확인
예) const axios = require('axios');

6-1- 5) 파일을 실행했을 때 함수를 호출하는지 확인

const getKOSPI = async () => {

			(생략)
            
};

// 함수 호출
getKOSPI();​

 
6-2) 직접 터미널에서 실행 시 성공

터미널에서 직접 실행 시 성공했으나 스케줄러에 등록했을 때 되지 않는다면 한 가지 확인해야 할 것이 있습니다.

로컬이나 서버 터미널에서 node scrapingKOSPI.js 명령어 실행 시 Node가 설치되어 있다면, PATH 변수가 자동으로 설정됩니다.

우리가 자주 사용하는 npm, npx, yarn, node, git 등 명령어 입력 시 가장 앞에 들어가는 파일명은 알아서 찾아주는 게 아닌 직접 등록해주어야 하거나 설치 시 등록된 것들입니다.

https://dev.to/aonkar/cypress-complete-setup-guide-5d1c

 
따라서 서버의 쉘 터미널과 독립적인 환경에서 cron 작업이 이루어지기 때문에 PATH 변수가 적용되지 않을 수 있습니다.
한마디로 cron은 node가 어디에 있는지 모르기 때문에 절대 경로를 직접 입력해주어야 합니다.

6-2-1) Node 절대 경로 찾기

which node​

 
Unix 계열인 Linux 서버의 경우 which 명령어로 찾을 수 있습니다.

ubuntu@ip-000-00-00-000:~$ which node
// *사용자 마다 다를 수 있음*
/usr/local/bin/node​


6-2-2) cronteb 스케줄러 수정

crontab -e​
# KOSPI
0 8-18 * * 1-5 node /home/ubuntu/kospi/scrapingKOSPI.js

->

# KOSPI
0 8-18 * * 1-5 /usr/local/bin/node /home/ubuntu/kospi/scrapingKOSPI.js​

 

7. 추가

이번 예시에선 서버에 간단하게 log만 남기도록 구현했지만 nodemailer로 이메일을 보낸다던가, DB에 값을 저장하거나, 문자 혹은 카카오톡 알림 등 여러 방면으로 로직을 추가할 수 있습니다.