Docker Best Practices

Sunghoon Kang
Banksalad Tech
Published in
9 min readJun 5, 2019

뱅크샐러드의 대다수 서비스들은 모두 Kubernetes, AWS ECS 환경 위에서 Docker 컨테이너를 기반으로 띄워져 있습니다. 기존에 컨테이너로 띄워져 있지 않던 서비스들을 컨테이너로 띄우기 위해 Dockerfile을 작성하고, 최적화하면서 과거의 우리가 놓쳤던 것들, 그리고 Docker를 사용하면서 알게 된 것들에 대해 공유하고자 합니다.

thanks to @dana

mkdir + cd

간단한 것부터 시작해볼까요? 특정 폴더에서 작업할 때 mkdir로 폴더를 생성하고 cd로 이동하는 경우가 많았었는데요, 이 두 개의 커맨드는 아래와 같이 WORKDIR Instruction으로 합쳐질 수 있습니다.

WORKDIR를 사용하면 좀 더 명확하게 Instruction의 Context를 파악할 수 있고 Docker 1.10 버전부터 RUN, ADD, COPY Instruction만 Layer를 생성하기 때문¹에 Layer 수에 대한 고민 없이 WORKDIR를 사용해도 좋습니다.

단, WORKDIR을 통해 폴더가 생성될 때 USER Instruction이 무시되므로, 특정 User가 소유하는 폴더를 만들고 싶은 경우에는 mkdir로 폴더를 생성해야 합니다.

exec form

CMD Instruction의 경우 아래와 같이 shellform과 execform의 형태로 작성할 수 있습니다.

# shell form
CMD node server.js
# exec form
CMD ["node", "server.js"]

shell form의 경우 command shell을 호출하기 때문에 위의 커맨드는 /bin/sh -c node server.js 로 실행되게 됩니다. 이 경우 SIGTERM같은 Signal들이 executable (위의 예시에서는 node)로 전달되지 않을 수 있기 때문에, Graceful Shutdown을 코드 레벨에서 처리했다 하더라도 실제로 작동하지 않을 수 있습니다. 따라서 exec form 사용이 권장됩니다.

Transparency Matters

같은 Dockerfile을 빌드할 때 마다 매번 같은 이미지가 나올까요?

Python 프로젝트에서 Dependency를 아래와 같이 관리한다고 가정해보겠습니다.

이 경우에 dependency들을 설치하게 되면 어떤 일이 벌어질까요? mysqlclient, numpy 모두 실행 시점의 최신 버전이 설치되게 됩니다.

따라서 커맨드가 언제 실행되는지에 따라 매번 같은 이미지가 나오지 않을 수 있습니다. 이런 상황을 피하려면 최대한 명시적으로 의존성들을 관리하는 게 좋습니다.

  • latest 태그 사용을 지양하기

위에서 언급한 것처럼 Base Docker Image의 latest태그의 의미 자체가 빌드되는 시점의 최신 버전이기 때문에 빌드 시점마다 다른 내용을 가리킬 수 있습니다. 예를 들면 이 글을 작성하는 시점의 python:latest3.7.3버전을 가리키지만, 새로운 버전이 릴리즈되면 latest 태그는 새롭게 릴리즈된 버전을 가리키게 됩니다.

그리고 운영의 관점에서 보면 정확하게 어떤 버전의 이미지가 실행되고 있는지를 바로 확인하기 어렵고, Kubernetes를 사용하는 경우 latest 태그를 사용했을 때 imagePullPolicy 설정값에 따라 새로운 이미지를 못 불러오거나, 이미지를 노드에 Cache 하지 않아 매번 Registry에서 불러오는 비효율을 야기할 수 있습니다.

따라서 latest 태그 사용을 최대한 지양하는 게 좋습니다.

  • dependency 버전을 명시하기

위에서 언급한 상황뿐만 아니라 Dependency Hell을 피하기 위해 빌드, 실행을 위한 Dependency의 버전을 명시하는 게 중요합니다. yarn.lock, Pipfile.lock과 같은 Lockfile을 활용하면 더 좋겠죠?

이렇게 버전을 명시하는 것이 좋습니다 🐳

GitHub을 사용한다면 아래와 같이 사용 중인 의존성에 대한 취약점 알림도 받을 수 있기 때문에 버전 명시를 적극적으로 권장합니다.

Javascript, Python 등 다양한 언어들을 지원합니다.

추가로 사용하는 Package들도 아래와 같이 버전을 명시해서 관리하는 게 좋습니다.

Keep It Light

꼭 필요한 파일만 이미지에 존재하나요?

이미지를 빌드하는 시점의 State를 항상 보장할 수 없기 때문에 .dockerignore파일을 사용하고 있지 않다면 의심해봐도 좋습니다. 특히 폴더를 통째로 복사하는 경우 의도하지 않았던 파일들이 이미지로 복사될 수 있습니다. 가상의 프로젝트로 예시를 들어보겠습니다.

위와 같은 구조를 가진 프로젝트에서 만약 .dockerignore없이 COPY . /some/dir Instruction을 통해 /some/dir 로 파일을 복사하게 되면 실행 환경에서 불필요한 파일과 폴더(e.g. .git, Dockerfile)들, 혹은 의도하지 않았던 Dependency가 node_modules안에 포함된 상태로 이미지에 포함될 수 있습니다. 특히 민감한 Credential이 이미지에 담겨있다는 걸 인지하지 못한 채로 배포되는 경우 더 큰 문제가 발생할 수 있기 때문에 Layer로 복사할 파일들을 명시하거나 .dockerignore를 활용하는 것이 좋습니다.

만약 빌드 시점에는 필요하지만, 실행 시점에는 필요 없는 파일들의 경우에는 어떻게 처리할 수 있을까요? Multi-stage build를 활용하면 깔끔하게 해결할 수 있습니다!

Web Application을 Serving 해야 하는 프로젝트를 예시로 들어보겠습니다. 대부분의 경우 Application을 bundling 하기 위해 다양한 툴(e.g. webpack, postcss…)들이 필요한데, 이런 툴들은 Runtime 환경에서 필요하지 않습니다. 아래와 같이 build stage와 runtime stage로 분리하면,

  • 불필요한 dependency와 파일들이 이미지에 포함되는 것을 방지할 수 있고
  • 아래 예시처럼 base image를 빌드환경에서는 stretch로, 실행환경에서는 alpine으로 작성해서

보다 효율적인 이미지를 만들 수 있습니다.

추가로, 한 이미지에 여러 컴포넌트를 담는 것 보다는 컴포넌트들을 최대한 분리하는 것이 좋습니다. 예를 들어 nginx와 웹 서버를 띄워야 하는 상황에서 하나의 이미지에 두 컴포넌트를 다 넣기보다는 각각의 이미지로 분리하면

  • nginx를 다른 로드밸런서 (e.g. envoy, haproxy)로 변경해야 하는 상황에서 Side Effect에 대한 걱정 없이 유연하게 변경할 수 있고,
  • 여러 컴포넌트 중 하나의 컴포넌트에 대해 변경이 필요한 경우 이미지 태그를 더 투명하게 관리할 수 있기 때문에 (Single Responsibility)

컴포넌트를 최대한 분리하는 것을 권장합니다.

IP Collision

레이니스트에서는 Docker Image를 기반으로 작업환경을 만들어 인프라 관련 Operation(e.g. Ansible provisioning)들을 진행하고 있습니다. 그러던 어느 날, 새로 구성된 네트워크의 인스턴스에 접근할 수 없는 문제가 발생했습니다. 원인은 로컬 작업 환경의 컨테이너의 IP 대역과 새로운 네트워크의 IP 대역이 겹쳤기 때문이었습니다. 그렇다면 컨테이너에서 같은 IP 대역에 있는 컨테이너 외부 리소스를 어떻게 접근할 수 있을까요?

  • Host proxy

컨테이너 내부에서 host.docker.internal로 Host에 접근할 수 있다는 점을 이용한 방법인데요,

ncat -c "ncat ${TARGET_INSTANCE_IP} ${TARGET_INSTANCE_PORT}" -l ${HOST_PORT} -k

위와 같이 ncat을 사용해 해당 Resource에 접근하는 Proxy를 컨테이너의 Host에 만들면 host.docker.internal:PORT 를 통해 같은 IP 대역에 있는 컨테이너 외부 리소스에 접근할 수 있습니다.

  • Bridge CIDR Block 설정

Bridge Network의 IP 대역의 변경을 통해서도 문제를 해결할 수 있습니다. Docker 설정 > Daemon > Advanced에서 bip를 명시하면 됩니다. 다만 이 방법의 경우 변경한 대역과 또다시 충돌하는 경우 빈 대역으로 변경해줘야 하는 불편함이 있겠죠?

만약 설정이 잘못되는 경우 기존의 설정들이 초기화되니 주의하세요 😢

추가로 보면 좋은 문서

  • 파일을 이미지로 복사할 때 ADD를 써야할까요, 아니면 COPY 를 써야할까요? — ADD or COPY

글을 맺으며

2년 전 Kubernetes를 처음 도입하면서부터 Docker를 개발 환경에서부터 운영 환경까지 다양한 범위에서 사용하며 얻은 점들을 이제서야 공유하게 되었는데요, 컨테이너는 소프트웨어 개발 및 운영에서 꼭 필요한 기술이 되었다고 생각합니다. 이 글을 통해 여러분이 Docker를 도입하는 데 도움이 되었으면 좋겠습니다. 😊 여러분들도 Docker를 사용하면서 얻은 팁들이 있다면, 댓글을 통해 공유해주세요!

마지막으로 사내 개발자, 고객분들께 더 나은 환경과 제품에 대한 경험을 제공할 Developer Productivity Engineer, Site Reliability Engineer, Infra Engineer를 찾고 있습니다! 👨‍💻👩‍💻 언제든지 아래 채용공고를 통해 지원해주세요. 감사합니다 🤗

--

--

Sunghoon Kang
Banksalad Tech

Software engineer who is interested in developer productivity and happiness