도커 초보의 우분투 Cron 삽질기

필자가 도커 초보인 동시에 우분투 초보라는 점을 감안하여 읽어 주셨으면 좋겠습니다. :)


이 글은 필자가 도커화Dockerize된 우부투에서 배치Batch 애플리케이션을 주기적으로 실행시키는 과정에서 겪었던 시행착오 과정을 정리한 것이다.

Cron과 Cron Job 그리고 Crontab

필자가 우분투에서 배치 애플리케이션을 주기적으로 실행 시키기 위해 선택한 것은  Cron이었다.

Cron은 리눅스 계열(이 글에서는 우부투) OS에서 제공하는 시간을 기반으로 하는 Job 스케줄러이다.[1] Job은 실행의 단위이며, Cron에 의해 주기적으로 실행되는 Job을 Cron Job이라고 한다.

이 글에서 Cron Job은 배치 애플리케이션을 실행을 의미한다.

Contab(Cron table) 파일은 Cron job과 실행 시간을 기술한 설정 파일이다. Cron은 Crontab 파일을 참조하여 Cron Job을 실행한다.

Crontab 파일 [출처 : https://en.wikipedia.org/wiki/Cron]

Crontab 파일 [출처 : https://en.wikipedia.org/wiki/Cron]

도커화된 우분투에서 Cron job 실행하기

맨땅에서 시작하기보다는 Run a cron job with Docker 글을 참고하여 시작했다.

Crontab 파일에 등록한 Cron job은 1분마다 수행되며, echo 명령어로 "Hello world"를 출력하고 이를 리다이렉션Redirection(>>)으로 /var/log/cron.log에 파일로 기록한다.

1
2
* * * * * root echo "Hello world" >> /var/log/cron.log
# An empty line is required at the end of this file for a valid cron file.

도커 이미지를 만드는데 필요한 Dockerfile은 우분투(ubuntu 14.04)를 기본 이미지로 작성하였다.

1
2
3
4
5
6
7
FROM ubuntu:14.04
# crontab 파일을 cron 디렉토리에 추가
ADD crontab /etc/cron.d/hello-cron
# 실행 권한 부여
RUN chmod 0644 /etc/cron.d/hello-cron
# Cron 실행
CMD cron

image2018-2-22_9-9-31

도커 build 명령어로 도커 이미지를 만들고, 도커 run 명령어로 도커 컨테이너를 -d 옵션(Detached mode : 보통 데몬 모드라고 부르며 컨테이너가 백그라운드로 실행 한다)로 실행시켰다.

image2018-3-6_9-21-32

도커 ps 명령어로 도커 컨테이너 잘 실행되고 있는지 확인해 보았는데...

image2018-2-21_9-27-5

실행 중인 컨테이너가 없다?!

ps 명령어에 -a 옵션(Show all containers - default shows just running)을 추가하여 전체 컨테이너 상태를 확인해 보면..

image2018-3-6_21-20-10

컨테이너가 종료(Exited) 되었다?!

왜 그럴까? 답은 김형준 님의 글(개발자가 처음 Docker 접할때 오는 멘붕 몇가지)에 잘 나와 있다.

Docker 컨테이너에서 실행되는 애플리케이션 서버(DB 서버 포함)은 back ground 모드가 아닌 fore ground 모드로 실행해야 한다.

그렇다. cron은 기본적으로 Backgroud 모드였던 것이었다. cron을 Foreground 모드로 실행하려면 -f 옵션 추가하여 실행해 주어야 한다.

출처 : http://manpages.ubuntu.com/manpages/trusty/en/man8/cron.8.html

출처 : http://manpages.ubuntu.com/manpages/trusty/en/man8/cron.8.html

Dockerfile을 수정하여 컨테이너를 다시 실행하면 컨테이너가 종료되지 않고 실행된다.

1
2
3
# ...
# Cron 실행
CMD cron -f

image2018-3-6_21-26-11

실제로 Crontab 파일에 등록한 Cron Job이 잘 수행되고 있는 확인하기 위해 필자는 실행 중인 도커 컨테이너에 도커 exec 명령어로 우분투에 직접 접속하여 로그 파일을 확인하였다.

image2018-3-6_21-31-39

로그는 도커 logs 명령어로 확인할 수 있도록 변경하기

도커 컨테이너에서 애플리케이션 로그는 파일로 기록하는 것이 아니라 표준 출력 또는 표준 에러로 출력해야 한다. 이 또한 김형준 님의 글에서 그 이유를 찾을 수 있다.

Docker 는 컨테이너에서 STDOUT나 STDERR로 출력하는 모든 메시지를 Host OS의 특정 디렉토리에 저장하고 이를 쉽게 조회할 수 있는 명령도 제공한다(docker logs 명령). 따라서 모든 로그를 표준 출력으로 보내면 쉽게 로그에 접근할 수 있게 된다.이 방식도 문제가 존재하는데 이 로그 파일을 하나의 파일로 관리하게 되면 파일이 너무 커지게 되어 스토리지를 모두 차지하게 되는 문제가 있다. 최근 버전의 Docker에서는 로그 파일을 롤링할 수 있는 기능을 제공하는데 이 방식을 사용할 경우 반드시 이 옵션을 사용하는 것을 권장한다. - 개발자가 처음 Docker 접할때 오는 멘붕 몇가지 중 일부 발췌

Cron Job에서 리다이렉션을 사용하여 파일로 남기던 부분을 제거하였다.

1
2
# * * * * * root echo "Hello world" >> /var/log/cron.log
* * * * * root echo "Hello world"

컨테이너를 다시 실행하고 도커 logs 명령어로 실행 중인 컨테이너의 로그를 확인해 보면..

image2018-2-22_10-22-21

필자가 기대했던 것과는 달리 아무것도 나오지 않는다.

분명히 echo 명령어는 표준 출력(standard output) 인데 왜 도커 logs 명령어에는 나오지 않을까?

출처 : http://manpages.ubuntu.com/manpages/trusty/en/man1/echo.1plan9.html

출처 : http://manpages.ubuntu.com/manpages/trusty/en/man1/echo.1plan9.html

가설을 세워 보았다.

docker logs 명령어는 Forgound로 실행된 프로세스의 표준 출력만 보여 주는 것은 아닐까?

증명해 보자.

Dockerfile을 보면 Forground 프로세스는 Cron이다. crontab 파일을 Cron이 사용하는 표준 출력으로 리다이렉션 하도록 변경하였다.

1
2
* * * * * root echo "Hello world" > /proc/1/fd/1 2>/proc/1/fd/2
# An empty line is required at the end of this file for a valid cron file.

리다이렉션 부분을 아래처럼 변경 후 컨테이너를 다시 실행하면 로그를 확인할 수 있다.

image2018-2-22_9-24-22

배치 애플케이션을 Cron job으로

Cron Job에서 "Hello World"를 출력하던 것을 배치 애플리케이션 실행하는 것으로 변경할 차례다.

배치 애플리케이션은 Java로 만들었으며, 실행 가능한 JARJava ARchive(batch-sample.jar)로 패키징 하였다. 그리고 Cron Job에서 실행할 수 있도록 쉘 스크립트를 만들었다.

1
2
#!/bin/bash
java -jar /app/batch-sample.jar

crontab 파일 역시 이에 맞추어 변경해 주었다.

1
2
* * * * * root . /app/batch-start.sh > /proc/1/fd/1 2>/proc/1/fd/2
# An empty line is required at the end of this file for a valid cron file.

JAR로 패키징된 배치 애플리케이션을 실행하기 위해서는 JDKJava Development Kit가 필요하다.

JDK를 따로 설치하지는 않고 우분투 이미지에 JDK가 더해진 이미지(pangpanglabs/java8)를 Dockerfile 기본 이미지로 변경하였다.

1
2
3
4
5
6
7
8
9
FROM pangpanglabs/java8
# Batch 애플리케이션
ADD ./batch-sample.jar /app/batch-sample.jar
ADD ./batch-start.sh /app/batch-start.sh
RUN chmod 755 /app/batch-start.sh
# cron
ADD crontab /etc/cron.d/batch-sample-cron
RUN chmod 0644 /etc/cron.d/batch-sample-cron
CMD cron -f

컨테이너를 다시 실행하고 로그를 확인해 보면..

image2018-2-27_8-58-44

image2018-3-12_9-44-47

java: not found

왜 java 명령어를 찾지 못하는 걸까?

batch-start.sh에서 java 명령어를 사용했다. 이렇게 사용하기 위해서는 JDK bin 디렉토리(pangpanglabs/java8 도커 이미지에서는 /usr/java/latest/bin)가 우부투 PATH 환경 변수에 추가되어 있어야 한다.

도커 exec 명령어로 실행 중인 컨테이너에 접속하여 PATH 환경 변수를 확인해 보면...

image2018-3-14_7-1-6

JDK bin 디렉토리가 PATH 환경 변수에 추가되어 있다.

왜 Cron Job에서는 java 명령어를 찾지 못하는 걸까?

다시 가설을 세워 보았다.

Cron Job 실행 할 때 PATH가 다른것은 아닐까?

증명해 보자. Crontab 파일을 변경하여 Cron Job이 실행될 때 PATH 환경 변수를 출력해 보았다.

1
2
* * * * * root echo $PATH > /proc/1/fd/1 2>/proc/1/fd/2
# An empty line is required at the end of this file for a valid cron file.

image2018-3-14_7-20-13

PATH 환경 변수가 다르다!

java 명령어를 아래처럼 절대 경로로 사용하면 손쉽게(?) 해결할 순 있겠지만 여전히 문제는 남아 있다.

1
2
#!/bin/bash
/usr/java/latest/bin/java -jar /app/batch-sample.jar

애플리케이션 환경 변수

애플리케이션이 실행되기 위해서 필요한 정보(예. 데이터베이스 접속 정보)는 일반적으로 설정 파일로 분리하여 관리한다. 이러한 애플리케이션 설정 파일은 애플리케이션이 배포되는 서버 환경(개발, 테스트, 스테이징, 운영 등)에 따라 달라진다.

일반적으로 서버 환경별로 설정 파일을 만들고 애플리케이션에서는 배포되는 서버 OS 환경 변수에 값에 따라 동적으로 설정 파일을 적용한다.

이미지 출처 : https://stackoverflow.com/questions/43295904/spring-boot-always-using-the-same-profile

이미지 출처 : https://stackoverflow.com/questions/43295904/spring-boot-always-using-the-same-profile

도커를 사용하는 경우 환경 변수를 도커 컨테이너 실행 시 -e 옵션으로 추가할 수 있다.

1
docker run -d --name batch-sample -e "APPLICATION_PROFILE=production" batch-sample:latest

이렇게 하면 도커 이미지를 애플리케이션이 배포되는 서버 환경마다 만들 필요 없이 환경 변수만 달리 설정하여 컨테이너를 실행하면 된다.

도커 명령어로 설정한 환경 변수를 Cron Job에서 사용하기

배치 애플케이션은 OS 환경 변수를 참조하여 애플리케이션 설정 파일을 동적으로 사용하고 있다. 그리고 Cron Job으로 배치 애플리케이션을 실행한다.

앞서 Cron job을 실행할 때 환경 변수가 다르다는 것을 확인하였다.

아래와 같이 애플리케이션 환경 변수를 추가하여 컨테이너를 실행하였을 때 Cron Job에서 'APPLICATION_PROFILE' 값을 어떻게 참조할 수 있을까?

1
docker run -d --name batch-sample -e "APPLICATION_PROFILE=production" batch-sample:latest

Access environment variables from crontab into a docker container에서 답을 찾을 수 있다.

도커 이미지를 만들 때 모든 환경 변수를 export 하여 쉘 스크립트로 만든다.(/root/envs.sh)

1
2
3
4
5
#!/bin/bash
# export all environment variables to use in cron
env | sed 's/^\(.*\)$/export \1/g' > /root/envs.sh
chmod +x /root/envs.sh
cron -f

envs.sh를  Cron Job에서 함께 실행시켜준다.

1
2
* * * * * root . /root/envs.sh;/app/batch-start.sh > /proc/1/fd/1 2>/proc/1/fd/2
# An empty line is required at the end of this file for a valid cron file.

이렇게 되면 java 명령어를 절대 경로로 기술해 줄 필요도 없게 된다.

1
2
#!/bin/bash
java -jar /app/batch-sample.jar

최종 Dockerfile은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
FROM pangpanglabs/java8
# Batch 애플리케이션
ADD ./batch-sample.jar /app/batch-sample.jar
ADD ./batch-start.sh /app/batch-start.sh
RUN chmod 755 /app/batch-start.sh
# cron
ADD crontab /etc/cron.d/batch-sample-cron
RUN chmod 0644 /etc/cron.d/batch-sample-cron
ADD bootstrap.sh /app/bootstrap.sh
RUN chmod +x /app/bootstrap.sh
CMD ["/app/bootstrap.sh"]

image2018-3-14_8-7-52

GitHub

이 글에서 사용한 모든 코드는 필자의 GitHub에서 확인할 수 있다.

덧붙여

필자가 사용해 보진 않았지만 쿠버네이트Kubernetes 를 사용하고 있다면 쿠버네이트 CronJob 사용을 검토해 보는 것도 좋을 것 같다.

주석

[1] https://en.wikipedia.org/wiki/Cron


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.