당연시하고 넘어갔던 exec 명령어의 역할
애플리케이션을 개발할 때 종종 docker-compose를 통해서 로컬 개발환경을 구성하곤 한다. docker-compose를 사용하는 이유는 환경 종속을 덜 받고, 필요한 구성요소를 빠르게 올려서 테스트할 수 있기 때문이다. 이때 프로세스를 돌리기 위해서 명령어를 직접 수행할 수도 있고, 엔트리포인트(Entrypoint)를 지정해서 컨테이너를 실행할 수 있다.
엔트리포인트에는 실행가능(executable)한 명령어를 넣을 수 있는데, 일부 오픈소스에서 보면 쉘(Shell) 스크립트를 넣는 경우들이 있다. 예컨대, postgres의 Dockerfile을 보면 다음과 같이 docker-entrypoint.sh 스크립트를 엔트리포인트로 지정해서 사용한다.
이렇게 엔트리 포인트를 사용하면 기본 명령어를 고정하여, 프로그램을 수행하기 전에 필요한 작업들을 고정적으로 수행할 수 있다. 예컨대, python 프로그램을 수행하기 전에 설정에 필요한 파일을 외부에서 받아오거나, 환경마다 서로 다른 작업을 미리 수행하고 python 프로그램을 수행해야 하는 경우가 생길 수 있다. 이런 경우에는 엔트리포인트로 쉘 스크립트를 지정하고, 쉘 스크립트에서 해당 작업들을 수행하면 된다.
엔트리포인트를 쉘로 지정하고 프로그램을 수행하면, 쉘 스크립트가 1번 프로세스가 된다. 따라서 컨테이너가 시그널을 받으면, 1번 프로세스인 쉘 스크립트가 시그널을 받아서 처리한다. 이상적이라면, 자신이 수행한 자식 프로세스들에 모두 시그널을 보내주고, 그 이후에 자신이 시그널 처리를 해야 한다. 그래야 자식 프로세스가 고아가 되지 않고 정상적으로 종료 과정을 거칠 수 있다.
문제는 쉘이 시그널을 자식에게 전달하지 않는다는 것이다.
정확하게 말하면, 백그라운드에서 동작하는 경우에 자식 프로세스에게 시그널을 전달하지 않는다.
테스트를 위해서, 다음과 같이 샘플 프로그램을 수행해 보자.
main.py 파일을 만들고 다음과 같이 코드를 작성한다. 프로그램을 5초마다 1번씩 로그를 남기고, 시그널을 받으면 어떤 시그널을 받았는지를 로그로 남긴 후 종료된다.
다음은 엔트리포인트로 사용할 쉘 스크립트이다.
먼저, 포그라운드(Foreground)에서 수행한 후 kill 명령어로 쉘 프로세스를 종료시켜 보자.
쉘 스크립트를 sh 명령어로 수행하고, Ctrl + C를 통해서 바로 종료 시그널을 보내면 된다. 그러면 마지막 로그처럼 Python 프로그램이 시그널을 받고 종료된다.
이번에는 백그라운드(Background)에서 수행해 보도록 하겠다.
쉘 스크립트를 수행한 프로세스는 97117번이고, 자식 프로세스(Python 프로그램)는 97121번이다. kill -15 명령어를 통해 쉘 프로세스에 SIGTERM을 보내면 쉘 자체는 종료되지만, Python 프로그램은 정상적으로 동작하는 것을 볼 수 있다. 부모가 종료된 이후 프로세스 찾아보면, 새롭게 1번 프로세스가 부모로 지정되어 있는 것을 볼 수 있다.
이와 같은 문제는 docker-compose를 통해 프로그램을 수행하는 경우에 동일하게 나타난다. docker-compose를 통해서 프로세스를 수행한 후, docker-compose down 명령어를 통해 컨테이너를 종료하면 다음과 같이 exited with code 137 메시지를 볼 수 있다.
137번 코드로 exit 하는 이유는 docker-compose 가 컨테이너를 내릴 때 타임아웃이 발생했기 때문이다. docker-compose는 -t 옵션을 통해서 타임아웃 옵션을 지정하지 않으면 docker cli의 기본 타임아웃 설정을 따라간다. docker는 운영체제에 따라서 서로 다른 타임아웃 옵션을 가지고 있는데, 리눅스의 경우 10초이다.
즉, docker-compose down을 통해 1번 프로세스인 쉘 프로세스가 SIGTERM을 받았지만, Python 프로세스가 계속해서 종료되지 않아서 10초가 지난 시점에 강제 종료가 발생한 것이다.
이 문제를 해결하기 위해서는 실제 동작을 수행하는 Python 프로세스가 시그널을 받도록 하면 된다. 가장 간단한 방법은 쉘이 아니라 Python 프로세스가 1번 프로세스가 되도록 만드는 것이다.
이때 사용하는 명령어가 바로 exec이다.
exec 명령어는 주로 유닉스 계열 운영체제의 셸에서 사용되는 명령어이다. 이 명령어는 주어진 명령어를 실행하고, 현재 셸 프로세스를 대체하는 역할을 한다. 다시 말해, exec 명령어를 실행하면 새로운 명령어가 실행되고, 원래 셸 프로세스는 종료된다.
그러면 이전에 사용했던 쉘 스크립트에서 exec 명령어를 추가한 후, 동일하게 docker-compose로 애플리케이션을 수행해 보도록 하자.
쉘 스크립트는 다음과 같이 Python 명령어 앞에 exec만 붙이면 된다.
이전과 동일하게 docker-compose up을 통해 컨테이너를 실행하고, 다시 docker-compose down으로 종료를 해보도록 하겠다.
위 사진에서 보는 것처럼, 컨테이너의 1번 프로세스가 Python 프로세스가 되었다. 그 덕분에 컨테이너 종료 시, Python 프로세스가 정상적으로 SIGTERM(15)을 받고 프로그램을 종료했다. 로그를 보면, Python 프로그램 수행 전에 쉘 스크립트도 정상적으로 수행되었음을 볼 수 있다.
이러한 장점 때문에, 앞서 언급했던 postgres와 같은 오픈소스에서도 exec 명령어를 사용하는 것을 볼 수 있다.
지금까지 Docker 엔트리포인트 사용 시에 exec 명령어를 사용하는 이유에 대해서 알아보았다. 언뜻 보면 그냥 지나칠 수도 있는 명령어지만, 해당 명령어가 제공해 주는 큰 이점에 대해서 이해하면 더욱 유용하게 활용할 수 있으리라 생각한다.