최근에 쿠버네티스 자격증을 공부하고 있는데, CMD와 ENTRYPOINT의 차이점에 대한 내용이 나왔다. 강의를 듣다 보니까 세상에, 이런 사실을 자세히 모르고 있었다니... 대충 ENTRYPOINT를 사용할 때에는 런타임에 주입하는 경우라고만 알고 있었는데, 자세히 알고 보니까 생각보다 중요한 내용이었다.
그리고 생각해 보니 예전에 재미있는 일이 있었다. 한 2년 전쯤의 일이다. node.js 프로그램을 docker container로 띄우기 위해서 아래와 같이 CMD를 입력했는데 안 됐다.
CMD ["tsc", "main.ts", "&&", "node", "main.js"]
ts를 통해서 타입스크립트를 빌드하고, 빌드된 js를 실행하는 명령어를 CMD로 나타냈는 데, 안 되는 것이다. 도저히 안되어 node의 설정 파일인 package.json에 다음과 같이 정의했다.
"scripts": {
"start": "tsc main.ts && node main.js",
"build": "tsc main.ts"
},
그러고 나서 CMD ["yarn", "start"]로 고쳐쓰니 정상적으로 도커가 돌아갔다. 그 당시에는 왜 안되지? 에 대한 고민을 할 시간이 없이 빨리 공지사항 스크래퍼를 만들어야 했기에 이러한 고민을 생각할 시간조차 없었다. 스포일러를 약간 하자면 CMD 명령어는 쉘 명령어를 사용할 수 없다! 그리고 위와 같은 구조는 package.json를 건드는 순간 dockerfile도 망가질 수 있기 때문에, 배포 관련된 내용은 완전히 격리되어야 한다!
이번 포스팅에서 다룰 내용은 컨테이너 기반의 근본적인 명령어 처리 방법이다. 우리가 docker exec -it <container> /bin/bash를 통해서 컨테이너에 접근하는 이유, 그리고 docker image를 통해서 확장 가능하게 설계하는 이유를 자세하기 알기 위해서는 이 내용이 꼭 필요하다. CMD와 ENTRYPOINT 한 줄에 엄청난 디테일이 숨어 있다.
본론
위 내용은 dockerfile reference에 있는 표를 가져온 것이다. ENTRYPOINT와 CMD를 사용했을 때의 실제 예시를 2차원 표로 나타내었다. 레퍼런스는 아래에 있다.
- [docker] Dockerfile reference : https://docs.docker.com/reference/dockerfile/
Dockerfile reference
Find all the available commands you can use in a Dockerfile and learn how to use them, including COPY, ARG, ENTRYPOINT, and more.
docs.docker.com
dockerfile reference에 따르면 CMD는 commands를 명시한다고 하고, ENTRYPOINT는 executable을 명시한다고 한다. 쉽게 설명하면 아래와 같다.
- ENTRYPOINT : 컨테이너 내에서 실행되는 프로세스
- CMD : ENTRYPOINT 프로세스에 제공되는 기본 인수 집합
정말 리눅스에 대해서 정통한 개발자거나, docker를 많이 써봤다면 여기까지만 들어도 알 것이다. 하지만 나를 포함하여 여기까지만 글로써 봤을 때에는 이해하기가 어려울 것이다. ( 내가 글을 잘 쓰지 못하는 부분도 있다 )
앞으로 다양한 예시를 들며 CMD와 ENTRYPOINT의 차이를 확인하고, 어떻게 활용하는 지를 다뤄보자.
CMD - Specify default commands
먼저, CMD는 ENTRYPOINT보다 한 층 더 높은 레벨로써, 도커 이미지를 실행할 때, 어떤 기본 명령이나 인자를 줄 것인가?를 정의한다.
잠깐만, 근본적인 부분부터 생각해보자. CMD는 어떤 기본 명령이나 인자를 줄 것인가? 이다. 그리고 엔트리포인트보다 한층 더 높은 레벨이다. 뭔가 이상한 점이 있다. 그것은 바로 기본 명령이나 인자를 입력하기 전에 셸 프로그램이 있어야 하는데, 그것이 없다는 점이다. 그렇지만 생각해 보면 CMD만 있어도 생각대로 잘 동작하는데, 어떻게 그런 일이 가능한 것일까?
그것은 바로 쉘이 숨어있기 때문이다. dockerfile에서는 엔트리포인트를 지정하지 않으면 기본적으로 /bin/sh를 실행한다. 아래의 dockerfile reference의 표를 살펴보자.
No ENTRYPOINT에서 CMD를 사용하면 /bin/sh이 들어 있다. 이제 JSON으로 전달하는 방식과 문자열로 전달하는 방식을 보면서 살펴보자.
exec 형식 ( JSON 배열 형식 )
쉘을 사용하지 않고 명령어를 직접 실행하는 방식이다. 따라서 쉘 특성을 사용할 수 없다.
FROM alpine:latest
CMD ["echo", "hello"]
위 이미지는 동작할까? 정답은 동작한다이다. 위 커맨드의 명령어를 exec 형식으로 직접 실행한다.
FROM alpine:latest
CMD ["echo", "hello", ">", "app.py"]
위 코드의 동작 결과는 어떨까? 에러가 발생할까? 아니면 우리가 의도한 대로 hello를 app.py에 집어넣을까? 정답은 2024-12-09 10:31:30 hello > app.py를 출력한다.
이렇게 CMD를 통해서 JSON 형식으로 전달하는 경우 그저 명령어만을 실행하기 때문에 쉘의 특성을 사용할 수 없다. 따라서 > 외에도 | 이나 & 등을 사용할 수 없다. docker image의 layer를 보면 아래와 같다.
맨 처음의 레이어에 깔려있는 CMD ["/bin/sh"]만 있기에 아래의 명령어를 그대로 exec의 형태로 실행만 한다.
shell 형식 ( 일반 문자열 형식 )
일반 문자열 형식으로 넣으면 쉘 기능을 사용할 수 있다고 한다. 한 번 확인해 보자.
FROM alpine:latest
CMD echo hello > app.py
이 dockerfile은 어떨까? 정답은 우리가 예상한 대로 hello라는 문자열이 app.py에 들어가게 된다. 그리고 터미널에 어떠한 것도 남지 않는다. docker image layer는 아래와 같다.
앞서 확인했던 JSON의 형태로 CMD에 전달했던 것과는 다르게, 앞에 /bin/sh -c가 몰래 들어가 있다. 따라서 그 이후에 들어오는 문자열을 쉘 스크립트처럼 실행한다. 따라서 쉘에서 사용하는 파이프, 변수, 리다이렉션 등의 문법을 사용할 수 있다.
ENTRYPOINT, Specify default executable
ENTRYPOINT는 CMD보다 한 층 낮은 레벨이다. executable이란, 컨테이너가 실행될 때 반드시 실행되어야 하는 명령을 뜻하고, Dockerfile에 하나만 사용할 수 있다.
여기서 ENTRYPOINT는 덮어씌우기가 불가능하지만, CMD는 가능하다. 즉 ENTRYPOINT는 낮은 레벨로 수정이 불가능하며 중복될 수 없고, CMD는 그보다는 높은 레벨이고 여러 번 실행될 수 있다는 것이다.
위 예시를 들어 보면 ENTRYPOINT와 문자열 형식으로 사용하는 경우에는 CMD가 전부 무시되는 것을 확인할 수 있다.
shell 형식 ( 일반 문자열 형식 )
위에 쓰였던 예시처럼 두 개 다 정의하는 경우 CMD가 작동하지 않는다! 아래와 같은 docker image layer를 보자.
위와 같이 정의되어 있는데도, 2024-12-09 10:47:48 entry라는 결과만 나온다. 또한 그리고 JSONArgsRecommended라는 warning이 뜬다.
2 warnings found (use docker --debug to expand):
- JSONArgsRecommended: JSON arguments recommended for ENTRYPOINT to prevent unintended behavior related to OS signals (line 2)
- JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 3)
이렇듯 예상하지 못한 동작이 발생할 수 있기 때문에 JSON의 형식으로 선언하는 것을 권장한다. 상식적으로 ENTRYPOINT와 CMD를 저렇게 명시했는데, ENTRYPOINT만 나오는 것은 이상하다.
exec 형식 ( JSON 배열 형식 )
그렇다면 가장 권장되는 방식인 ENTRYPOINT를 앞에다 두고, CMD를 뒤에다 둔 후에 둘 다 JSON 형식으로 설정하면 어떻게 될까?
FROM alpine:latest
ENTRYPOINT ["echo","entry"]
CMD ["echo","cmd"]
위 실행 결과는 바로 다음과 같다. 2024-12-09 10:51:42 entry echo cmd이다. 위 표에 따라 엔트리포인트 뒤에 이어서 온다. 레이어는 아래와 같다.
자, 여기까지 왔으면 대충 어떻게 동작하는지는 이해했을 것이다. ENTRYPOINT는 앞에다 두고, CMD는 뒤에다 둔다. ( 같이 쓸 일이 있으면 ) 그리고 JSON의 형식으로 쓰는 게 좋다. 이 정도일 것이다.
다음 단계에서는 CMD를 덮어쓸 수 있다는 점을 통해서 확장 가능한 docker image를 만드는 방법에 대해서 다뤄보자. 이 부분이 반드시 devops로서 알아야 하는 부분이라고 생각한다.
ENTRYPOINT와 CMD의 활용 방법
앞서 이야기했던 내용에 따르면 CMD는 덮어쓸 수 있다고 한다. 확장성을 고려한 아키텍처를 설계할 때에는 이 dockerfile의 한줄한줄의 디테일에 엄청난 내용이 들어가 있다.
.env 등의 파일을 통해서 환경 변수를 제어한다는 것을 알고 있을 것이다. 그리고 환경 변수를 외부에서 주입해 줌으로써 하나의 이미지를 확장하여 다양한 부분에서 쓰는 것이 기본이다.
docker를 이용해서 이미지를 실행시킬 때, 인자(parameter)를 넣어줄 수 있다. 아래와 같이 dockerfile이 정의되어 있다면?
FROM alpine:latest
ENTRYPOINT ["echo"]
그렇다면 docker run <이미지명> <인자>를 통해서 값을 넣어줄 수 있다. 여기서 CMD를 함께 사용하여 기본 값을 정의할 수 있다. 그렇다면 아래와 같은 최종적인 도커파일이 만들어진다.
FROM alpine:latest
ENTRYPOINT ["echo"]
CMD ["default massage"]
앞서 표를 다시 생각하면 echo를 통해서 쉘 스크립트로 받아들이고 뒤에 default massage가 들어가게 되어 우리의 예상대로 코드가 잘 동작하게 된다.
➜ home docker run final
default massage
이제, 뒤에 인자를 넣어주면 CMD를 덮어쓰게 된다. 따라서 entrypoint에 이어서 뒤의 인자가 오게 된다.
➜ home docker run final i do not want default massage
i do not want default massage
여기까지가 ENTRYPOINT와 CMD를 조화롭게 쓰는 내용의 끝이다.
외부에서 파라미터를 주입하는 경우
docker-compose에서 사용하는 경우
version: '3'
services:
echo-service:
build: .
command: ["this is docker-compose"]
위 코드를 보면 command 옵션으로 this is docker-compose를 전달할 수 있다.
Kubenetes를 사용하는 경우
apiVersion: v1
kind: Pod
metadata:
name: echo-pod
spec:
containers:
- name: echo-container
image: echo-test-image:latest
args: ["this is from kubernetes"]
restartPolicy: Never
쿠버네티스를 통해서 pod을 띄우는 경우 이와 같이 args를 주입해 줌으로써 외부에서 간단하게 해당 docker image를 확장성 있게 사용할 수 있다.
물론 쿠버네티스에서는 command라는 옵션을 통해서 ENTRYPOINT를 설정할 수 있지만, 쓰지 않는 것을 권장한다. 베스트 프랙티스의 관점에서 봤을 때에는 CMD로 쓰인 부분만 args로 바꿔주면서 위와 같이 주입시키는 것이 안전하다.
앞서 다양하게 ENTRYPOINT에 대해서 설명하고 CMD에 대해서 설명한 이유가 바로 이것이다. 컨테이너 실행 로직을 ENTRYPOINT로 고정하고, 디폴트 동작(명령/파라미터)을 CMD로 설정하는 것이다. 이렇게 하면 K8s나 docker-compose 등의 오케스트레이션 환경에서 다양한 옵션을 통해서 쉽게 런타임 파라미터를 주입하고 유연하게 환경별 동작을 컨트롤할 수 있다.
즉, Dockerfile에서는 기본 동작을 깔끔하게 정의하고, 오케스트레이션 도구에서는 그 기본 동작을 상황에 따라 바꾸어 다양한 환경에서 사용할 수 있는 방법이 권고된다.
마치며
dockerfile을 처음 접했을 때에는 그냥 돌아가는 것마저 마냥 신기하고, 대충 스크립트를 작성했지만.. 지금 와서 보니까 정말 한줄한줄 심혈을 기울이지 않으면 안 되는 쉽지 않은 일인 것 같다.
참고
- [James walker] docker ENTRYPOINT and CMD : https://spacelift.io/blog/docker-entrypoint-vs-cmd
- [Docker] dockerfile : https://docs.docker.com/reference/dockerfile/
감사합니다.
'DevOps > docker' 카테고리의 다른 글
docker image의 환경 변수 그리고 우선 순위에 대하여 (0) | 2025.02.04 |
---|---|
docker은(는) 사용자의 컴퓨터를 손상시킵니다. "'com.docker.vmnetd'에 악성 코드가 포함되어 있어서 열리지 않았습니다." 문제 해결 방법 (2) | 2025.01.14 |
docker multi-stage build에 대해서 ( spring ) (0) | 2024.11.25 |
[Jenkins] DinD vs DooD 그리고 DooD 설정법 (1) | 2024.10.30 |
docker의 경량화 버전인 enroot (0) | 2024.07.13 |