도커 트러블슈팅 - 컨테이너 실행환경 디버깅
run, exec, commit 명령어 활용하기
들어가며: 컨테이너 실행환경 디버깅
도커Docker를 사용하다 보면 컨테이너가 제대로 동작하고 있는지 확인해야하는 일이 자주 생깁니다. 개발 과정에서 컨테이너의 상태를 확인하거나, 살아있는 컨테이너의 상태를 확인해야할 경우도 있습니다. 때로는 이미 죽어버린 컨테이너를 해부해봐야할 수도 있습니다. 이 글에선 도커 컨테이너 디버깅 용도로 자주 사용하는 run
, exec
, commit
명령어들을 소개하고자 합니다.
이 글은 도커 사용법을 어느 정도 이해하고 있다고 가정하고 있습니다. 도커가 처음이신 분들은 다음 글을 추천합니다.
docker run
: 도커 기본 중의 기본
docker run
은 컨테이너를 실행하는 가장 기본적인 명령어입니다. 이 명령어는 컨테이너를 디버깅하기보다는 이미지를 탐색하는 용도로 사용됩니다. 디버깅을 위한 가장 기본적인 활용법은 -it
옵션을 붙여 셸을 실행하는 방법입니다.
$ docker run -it ubuntu:latest bash
root@a8747c46330d:/#
이 명령어는 ubuntu:latest
이미지에 bash
셸을 실행합니다. 프롬프트가 root@a8747c46330d
로 바뀐 지점부터 컨테이너 환경에서 내부를 살펴볼 수 있습니다. 디렉터리를 탐색하거나 파일을 검색해보거나 동작중인 프로세스들도 확인할 수 있습니다.
컨테이너의 파일 탐색 및 추가 사이트 패키지 설치
개인적으로 자주 사용하는 명령어 중 하나는 find
입니다. 이 명령어로 컨테이너 전체에서 파일을 찾아보곤 합니다. 컨테이너 환경은 일반 OS보다는 훨씬 작기 때문에 파일 시스템 전체를 대상으로 부담없이 find
를 실행해볼 수 있습니다.
root@a8747c46330d:/# find / -name "*lsb-release*"
/etc/lsb-release
필요하다면 apt-get
으로 필요한 유틸리티를 설치할 수도 있습니다. 예를 들어 컨테이너의 네트워크 환경에서 DNS 문제를 디버깅해보고 싶다면, nslookup을 설치해 dns 쿼리를 해볼 수 있습니다.
root@a8747c46330d:/# apt-get update
root@a8747c46330d:/# apt-get -y dnsutils
root@a8747c46330d:/# nslookup google.com
Server: 192.168.65.1
Address: 192.168.65.1#53
Non-authoritative answer:
Name: google.com
Address: 172.217.25.110
Name: google.com
Address: 2404:6800:4004:809::200e
당연한 이야기입니다만, 리눅스 유틸리티나 패키지 관리자는 리눅스 배포판 별로 다릅니다. 센트OSCentOS 계열이라면 yum
을 사용하고 알파인Alpine에서는 apk
를 사용합니다.
알파인(alpine
)이나 스크래치(scratch
) 같은 극단적으로 최소화된 이미지들을 사용할 경우 문제가 생겼을 때 실행환경을 디버깅하는 게 어려울 수 있습니다. 이런 이유로 개인적으로 용량 차이에도 불구하고 ubuntu
나 centos
와 같은 좀 더 일반적인 배포판 이미지를 추천하는 편입니다.
셸 명령어 지정하기
도커 이미지에는 기본 명령어(CMD
)라는 개념이 있습니다. 예를 들어 ubuntu
의 기본 명령어는 bash
입니다. 이에 대해서는 docker image inspect
로 확인해볼 수 있습니다.*
* 여기선 json 형식의 출력 결과를 탐색하기 위해 JSON 필터링 명령어 jq를 사용하고 있습니다. jq에 대해서는 커맨드라인 JSON 프로세서 jq : 기초 문법과 작동원리를 참고해주세요.
$ docker image inspect ubuntu:latest | jq '.[].ContainerConfig.Cmd'
[
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"/bin/bash\"]"
]
따라서 docker run
을 실행할 때 bash
를 생략해도 bash
가 실행됩니다.
$ docker run -it ubuntu
root@9cc74dd97e88:/#
도커 예제들을 보면 bash
대신 /bin/bash
처럼 전체 경로를 입력하는 경우도 자주 만날 수 있습니다. 이는 패스 설정이 되어있지 않더라도 명령어를 찾기 위한 방어적인 방법이라고 볼 수 있습니다. 앞에서 확인했듯이 반드시 전체경로를 입력할 필요는 없습니다. 이미지에 설정된 $PATH
환경변수 상에 명령어가 있다면 경로는 생략해도 무방합니다. 이 역시 docker image inspect
로 확인해볼 수 있습니다.
$ docker image inspect ubuntu:latest | jq '.[].ContainerConfig.Env'
[
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
]
이미지에 배시 셸이 없는 경우도 있습니다. 예를 들어 alpine
이미지에는 bash
셸이 없습니다. 이럴 때는 셸을 찾아봅니다.
$ docker run -it alpine sh
$ docker run -it alpine /bin/sh
$ docker run -it alpine
셋 다 결과는 같습니다. sh
를 실행해서 디버깅해볼 수 있습니다. sh
라도 있는 알파인은 양반이지만, 정말 아무것도 없는 scratch
기반의 이미지는 훨씬 더 디버깅하기가 까다롭습니다.
이미지에 ENTRYPOINT
가 설정되어 있는 경우
이미지에 ENTRYPOINT
가 설정되어있는 경우 조금 까다롭지만 우회해서 셸을 실행할 수 있습니다. ENTRYPOINT
가 설정된 예제 이미지를 만들어보겠습니다.
FROM ubuntu:latest
ENTRYPOINT ls
이미지를 빌드합니다.
$ dokcer build -t ubuntu:ls .
ENTRYPOINT
를 사용하면 도커 이미지를 마치 유틸리티처럼 사용할 수 있습니다. 이제 이 이미지의 기본 명령어가 변경된 것을 확인할 수 있습니다.
docker image inspect ubuntu:ls | jq '.[].ContainerConfig.Cmd'
[
"/bin/sh",
"-c",
"#(nop) ",
"ENTRYPOINT [\"/bin/sh\" \"-c\" \"ls\"]"
]
이 상태에서는 docker run
의 마지막 인자로 bash
를 넘겨도 셸이 실행되지 않습니다.
$ docker run -it ubuntu:ls bash
bin dev home lib64 mnt proc run srv tmp var
boot etc lib media opt root sbin sys usr
이런 이미지에서 셸을 실행하고자 한다면, --entrypoint
옵션으로 빈 값을 넘겨주면 됩니다. 다음과 같이 실행하면 셸을 실행할 수 있습니다.
docker run -it --entrypoint '' ubuntu:ls bash
root@25bb74dcd91d:/#
Bash 셸이 정상적으로 실행되었습니다. 이제 컨테이너의 실행환경을 탐색할 수 있습니다.
docker exec
: 살아있는 컨테이너에 다른 명령어 실행하기
docker run
은 사실 컨테이너보다는 이미지를 검증하고 탐색하기 위한 용도로 사용됩니다. 반면 docker exec
명령어를 사용하면 실제로 실행중인 컨테이너에 또 다른 명령어를 실행할 수 있습니다. exec
는 실행중인 컨테이너에서만 사용할 수 있으므로, 먼저 컨테이너를 하나 준비합니다.
$ docker run -d -p 8000:80 --name nginx nginx
80ff0d0c4752d82f55ecafddfac94966644b96df62288ef49d9c03fb1b0157a0
docker ps
명령어를 사용해 nginx 서버가 정상적으로 실행중인지 확인해봅니다.
$ docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
80ff0d0c4752 nginx "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 0.0.0.0:8000->80/tcp nginx
이 컨테이너의 변경사항을 확인해보겠습니다. docker diff
명령어를 사용하면 컨테이너가 실행된 이후 변경된 파일들을 확인할 수 있습니다.
$ docker diff 80ff0d0c4752
C /run
A /run/nginx.pid
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
예를 들어 nginx
이미지에는 /run/nginx.pid
파일이 존재하지 않습니다.
$ docker run -it nginx:latest bash
root@129128a11ed6:/# cat /run/nginx.pid
cat: /run/nginx.pid: No such file or directory
root@129128a11ed6:/#
하지만 위에서 실행한 nginx 컨테이너에 exec
를 사용해 셸을 실행해 탐색해보면 /run/nginx.pid
파일이 존재하는 것을 확인할 수 있습니다.*
* exec
명령어로 셸을 실행할 때도 run
과 마찬가지로 -it
옵션이 사용되는 것을 잊지마세요.
$ docker exec -it 80ff0d0c4752 bash
root@80ff0d0c4752:/# cat /run/nginx.pid
1
root@80ff0d0c4752:/#
이를 통해 80ff0d0c4752
컨테이너가 실행중인 상태에서 다른 (셸) 프로세스가 실행된 것을 확인할 수 있습니다. 이 상태에서는 컨테이너가 동작하면서 변경된 사항을 탐색할 수 있습니다. nginx.pid
의 값이 1인 게 흥미롭게 느껴질 수도 있습니다. 컨테이너의 PID는 기본적으로 1로 실행됩니다. 이는 PID 네임스페이스가 분리되어있기 때문입니다. 반면에 exec
로 실행된 명령어의 pid는 1번이 아닙니다.
echo $$
7
이는 컨테이너에 할당된 PID 네임스페이스를 공유해서 새로운 셸 프로세스가 실행되었기 때문입니다. 여기서 PID 네임스페이스에 대해서 자세히 소개하지는 않습니다만, 더 자세한 동작 원리가 궁금한 분은 다음 글을 참고해주세요.
도커가 처음 나왔을 때는 exec
명령어가 없어서 컨테이너의 상태를 확인하려면, LXC의 유틸리티를 사용하는 등 좀 더 복잡한 방법이 사용되었습니다. exec
명령어는 컨테이너를 사용하는 입장에서는 축복과 같습니다.
docker commit
: 종료된 컨테이너도 되살려주는 명령어
docker exec
명령어에는 한 가지 치명적인 문제가 있습니다. 바로 컨테이너가 실행중인 상태에서만 컨테이너 환경에 접근할 수 있다는 점입니다. 안타깝게도 시스템 관리자에게는 살아있는 컨테이너의 실행환경을 탐색하는 경우보다, 죽어있는 컨테이너의 상태를 부검하는 일이 더 중요합니다. 그리고 내부적인 문제로 죽은 컨테이너는 restart
로 재실행하더라도 바로 죽어버리는 경우가 많습니다. 따라서 죽어있는 컨테이너에 한해서는 exec
명령어가 무용지물입니다.
이럴 때 고려해볼 수 있는 방법이 docker commit
입니다. commit
은 컨테이너의 특정 상태를 그대로 이미지로 만들어주는 명령어입니다. 이 명령어는 도커 빌드 과정에서 사용됩니다만, 일반적으로 직접 사용할 일은 거의 없습니다. 이미지 빌드 원리와 commit
에 대한 더 자세한 내용은 다음 글을 참고해주세요.
아이디어는 간단합니다. 죽어있는 컨테이너를 docker commit
명령어로 새로운 이미지로 만들고, 이 새로운 이미지에서 docker run
으로 셸을 실행해서 컨테이너의 환경을 탐색합니다. 먼저 앞서 만든 nginx 컨테이너를 죽이고, exec
명령어를 사용해보겠습니다.
$ docker kill 80ff0d0c4752
80ff0d0c4752
$ docker exec -it 80ff0d0c4752 bash
Error response from daemon: Container 80ff0d0c4752 is not running
위에서 설명한 대로, 컨테이너가 실행중이 아니라고 에러가 발생합니다. 이 죽은 컨테이너를 커밋해서 이미지로 만들겠습니다.
$ docker commit 80ff0d0c4752 nginx:killed
sha256:67edfba59ed618badfe5dde7105e25473f68cb28f50608aaca3e39bcc824eb20
$ docker images | grep nginx
nginx killed 67edfba59ed6 32 seconds ago 126MB
...
nginx:killed
이미지가 생성된 것을 확인할 수 있습니다. 이제 docker run
으로 셸을 실행해보겠습니다.
docker run -it nginx:killed bash
root@621c34ab41ee:/# cat /run/nginx.pid
1
root@621c34ab41ee:/#
nginx가 실행되다가 강제로 종료되었기 때문에 컨테이너 실행시에 생성된 /run/nginx.pid
가 그대로 남아있는 것을 확인할 수 있습니다. 이제 이 셸에서 죽은 컨테이너의 상태를 탐색해볼 수 있습니다.
같이 읽으면 좋은 문서들
다음은 도커 명령어들의 레퍼런스 문서들입니다.