본문 바로가기

도커 파일(Dockerfile) 작성 Best Practices

출처

이 글은 도커 엔지니어의 포스팅을 원저자의 허락을 받고 번역한 것입니다. 

적절한 번역이 떠오르지 않는 부분에는 옆에 원문을 적었습니다.

원문은 다음 링크에서 살펴보실 수 있습니다.

https://blog.docker.com/2019/07/intro-guide-to-dockerfile-best-practices/

 

Intro Guide to Dockerfile Best Practices - Docker Blog

Learn from Docker experts to simplify and advance your app development and management with Docker. Stay up to date on Docker events and new version announcements!

blog.docker.com

Intro Guide To Dockerfile Best Practices

오늘날 깃헙에는 수백만개에 달하는 도커파일들이 있습니다. 그러나 모든 도커 파일들은 같은 방식으로 만들어지지 않았습니다. 효율성은 중요한 이슈이기 때문에 이 글에서는 도커 파일을 효율적으로 작성하기 위해서 중요한 다섯 가지 영역들에 대해 다뤄볼 것입니다. 다섯 영역으로는 빌드 시간, 이미지 크기, 유지 보수성, 보안, 반복성이 있습니다. 이 포스팅은 도커 입문자들에게 적합하며, 다음 포스팅에선 좀 더 심화된 내용을 다뤄볼 예정입니다.

 

주의

이어지는 예시 코드들은 Java maven 프로젝트의 도커 빌드 파일을 조금씩 고쳐나가면서 Best practice를 소개할 예정입니다. 마지막 버전의 도커 파일이 권장되어지는 형식입니다. 중간에 등장하는 코드들은 best practice의 일부분만 담고 있으니, 주의하시기 바랍니다.

 

빌드 시간 깎기 (Incremental build time)

개발을 하다보면 흔히 도커 이미지를 빌드하고, 코드를 수정하고, 다시 빌드하는 과정을 반복하게 됩니다. 그러므로 캐싱을 잘 활용하는 것이 중요합니다. 캐싱을 활용하면 불필요한 빌드 과정을 반복하는 것을 방지해줍니다.

 

Tip #1. 순서는 캐싱에 중요하다.

도커 명령어를 수행하는 순서는 매우 중요합니다. 왜냐하면 특정 스텝에서 파일의 변화나 도커 파일의 수정 등으로 캐시가 무효화 될 경우 이어지는 스텝들의 캐시가 모두 깨져버리기 때문입니다. 그러므로 가장 변하지 않는 스텝을 먼저, 자주 변경되는 스텝은 아래로 배치하는 것이 캐싱을 최적화 시킵니다.

 

Tip #2. 더 구체적인 COPY는 캐시 부담을 줄여준다.

필요한 부분만 카피하세요. 가능하면 "COPY . "과 같은 명령어는 피하세요 . 파일들을 도커 이미지로 복사할 때, 당신이 무엇을 카피하고자 하는지를 최대한 명확하게 하세요. 이미지에 복사된 파일들 가운데 하나라도 변경될 경우 캐시가 무효화 됩니다. 위의 예시에서는 이미 빌드를 마친 jar 파일만 복사하는 것을 볼 수 있습니다. 이 경우 관련이 없는 파일들의 변화는 캐시에 영향을 주지 않습니다.

 

Tip #3. 캐시할 수 있는 단위들을 구별해내라 

각각의 RUN 명령어들은 캐시할 수 있는 단위로 볼 수 있습니다. 너무 많은 캐시 단위들은 불필요합니다만, 하나의 RUN 명령어에 모든 명령어를 다 연결시키는 것 역시 캐시가 쉽게 무효화 되게 하여 개발 사이클에 좋지 못합니다. 패키지 매니저로부터 패키지들을 다운받을 때에 항상 업데이트와 인스톨을 하나의 RUN 안에 묶어주세요. 그러면 그들은 하나의 캐시 단위가 됩니다. 그렇지 않다면 당신은 예전 버전의 패키지를 다운받을 위험이 있습니다.

 

이미지 크기 줄이기 (Reduce Image size)

이미지 크기는 중요합니다. 왜냐하면 더 작은 이미지는 곧 더 빠른 배포와 더 작은 공격 취약점이니까요.

 

Tip #4. 불필요한 의존성을 제거하라

불필요한 의존성은 제거하고 디버깅 툴들은 설치하지 마세요. 필요하다면 디버깅 툴들은 언제는 나중에 설치할 수 있습니다. apt와 같은 패키지 매니저는 자동으로 사용자가 특정된 패키지가 추천하는 패키지들을 설치하고, 이는 불필요한 발자국들을 남깁니다. --no-install-recommends 플래그를 통해서 이들이 자동으로 설치되는 것을 막을 수 있습니다.

 

Tip #5. 패키지 매니저 캐시를 제거하라

패키지 매니저들은 스스로의 캐시를 유지합니다. 이는 최종 도커 이미지에 그대로 남을 수 있습니다. 한 가지 해결 방안은 패키지를 설치한RUN 명령어 안에서 캐시를 지워주는 것입니다. 다른 RUN 명령어에서 지워주는 것은 이미지의 크기를 줄여주지 못합니다.

 

 유지보수성 (Maintainability)

Tip #6. 가능하면 공식 이미지를 사용하라

공식 이미지는 유지 보수에 들어가는 시간을 많이 절감해줍니다. 왜냐하면 모든 설치 과정들이 완료되어 있고 best practice가 적용되었기 떄문입니다. 만약 당신이 여러 프로젝트들을 가지고 있다면, 같은 base image를 사용할 경우, 이러한 설치 과정이 적용된 layer들을 공유할 수 있습니다.

 

Tip #7. 더 구체적인 태그를 사용하라

latest 태그는 사용하지 말아라. 이는 도커 허브의 공식 이미지들에 항상 적용할 수 있는 편의성은 있습니다. 하지만 시간이 흐름에 따라서 급격한 변화가 있을 수 있습니다. 이 경우 캐시가 없다면 당신의 빌드가 실패할 위험이 있습니다.

 

대신에 더 구체적인 태그를 사용하세요. 예시 코드에서는 openjdk를 사용합니다. 많은 태그들이 가능하니, 모든 태그들을 리스팅 해놓은 openjdk 이미지의 도커 허브 문서를 살펴보세요.

 

Tip #8. 가장 작은 이미지를 고르라 (Look for minimal flavors)

몇몇 태그들은 훨씬 작은 크기를 가졌습니다. slim 태그 이미지는 striped down Debian에 기초해서 만들어진 이미지입니다. alpine 태그 이미지는 더 작은 알파인 리눅스를 기반으로 만들어졌습니다. 주목할 만한 차이는 데비안은 여전히 GNU libc를 사용하는 반면 알파인은 훨씬 더 작지만 호환성 문제의 소지가 있는 musl libc를 사용한다는 것입니다. openjdk의 경우에는 jre 태그들은 오직 자바 런 타임만 포함하고, sdk를 포함하지 않습니다. 이 또한 이미지 크기를 절감하는데 큰 영향을 줍니다.

 

재현성 (Reproducibility)

지금까지 예시 도커파일은 jar 파일이 호스트에서 빌드 되어있음을 가정하였습니다. 이는 도커 컨테이너가 주는 환경의 일관성을 제대로 활용하지 못하는 것입니다. 예를들어 만약 당신의 자바 어플리케이션이 특정한 라이브러리에 의존한다면, 빌드하는 컴퓨터에 따라서 예상치 못한 비일관성이 발생할 수 있습니다.

 

Tip #9. 일관된 환경에서 소스 코드를 빌드하라

먼저 당신의 어플리케이션을 빌드하기 위해 필요한 것들을 찾아내는 것에서 시작해야합니다. 예시 속 자바 어플리케이션의 경우 Maven과 JDK를 필요로 합니다. 그러므로 도커 허브의 공식 maven 이미지들 가운데 JDK를 포함하고, 크기가 가장 작은 것을 찾아 기초로 삼습니다. 만일 추가적으로 의존 라이브러리가 필요하다면 RUN 명령어를 통해 설치합니다.

 

pom.xml과 src 폴더를 카피해옵니다. 그리고 RUN 안에서 mvn package 명령어를 수행하여 app.jar를 생성합니다. (-e 플래그는 에러 로그를 보여주는 옵션, -B는 batch 모드로 실행한다는 옵션입니다.)

 

이로써 환경 불일치 문제는 해결했습니다. 그러나 다른 문제가 있습니다. 매번 코드가 변할때마다 pom.xml에 있는 의존성들이 모두 fetch 되어진다는 것입니다. 그래서 다음 팁을 준비했습니다.

 

Tip #10. Fetch dependencies in a separate step

캐시 가능한 실행 단위를 다시 한번 떠올려보자. 우리는 의존성들을 fetch 해오는 것이 소스 코드의 변화와는 별개로 pom.xml의 변화에만 의존하는 캐시 단위라는 것을 알 수 있습니다. 두 COPY 스텝 사이의 RUN 스텝만이 유일하게 메이븐에게 의존성을 fetch 해오라고 알려줍니다.

 

일관된 환경을 구성하는 데에는 한 가지 문제가 더 있습니다. 바로 우리의 이미지가 런 타임에는 필요하지 않고 빌드 시에만 필요한 의존성들을 포함하느라 크기가 더 커져버리게 된 것입니다.

 

Tip #11. 빌드 의존성을 제거하기 위해서 다단계 빌드(Multi-stage build)를 사용하라

다단계 빌드란 여러개의 FROM 구문을 사용할 수 있습니다. 각각의 FROM은 새로운 단계를 시작합니다. AS 키워드로 이름을 붙일 수 있는데, 위 예시에서는 첫 단계에 builder라는 이름을 붙여서 나중에 다시 참조될 수 있도록 했습니다. 이는 우리의 모든 빌드 의존성들이 일관된 환경에 포함될 수 있도록 합니다.

 

두 번째 단계는 우리의 마지막이자 최종 이미지를 만들어내는 단계입니다. 여기서는 런 타임에 엄격하게 필요로 하는 것들만 포함하며, 예시에서는 알파인 리눅스 기반의 최소 JRE(Java Runtime)에 해당합니다. 중간의 builder 단계는 캐시되지만 최종 이미지에는 포함되지 않습니다.  중간 빌드 결과물들을 우리의 최종 이미지에 포함시키기 위해서는 COPY --from=STAGE_NAME 구문을 사용하며, 여기서 STAGE_NAME은 builder입니다.

다단계 빌드는 빌드 시점 의존성을 제거하기 위한 해결책(go-to solution)입니다.

 

우리는 부풀려지고 일관성이 없는 이미지를 빌드하는 것에서 시작해 작고 일관성있으며 캐시를 잘 활용하는 이미지를 빌드하는 과정을 살펴보았습니다. 다음 포스팅에서는 다단계 빌드를 사용하는 것을 더 집중적으로 다뤄보겠습니다.

 

마치며

너무 쉬우면서도 유익한 내용을 담고 있는 포스팅을 번역할 수 있어서 좋았습니다.

그동안 얼마나 마구잡이로 도커 파일을 작성했는지 반성이 되네요 :)

 

저자에게 번역 허락을 구하는 이메일에 대한 답장에 이렇게 적혀 있었습니다.

"번역하는 것은 얼마든지 좋지만, 이 블로그 포스팅은 몇 편 더올라갈 꺼야! 그러니 친구 너가 이어지는 시리즈도 번역해주면 좋겠어! 이번 포스팅은 초보자들을 위한 거지만, 더 심화된 그렇지만 아주 중요한 내용들도 앞으로 계속 다룰꺼거든!"

 

이번 포스팅을 유익하게 보셨다면 다음 포스팅도 기대해주세요!

감사합니다.

 

PS.

오역이나 해석이 애매한 부분들, 더 나은 번역 표현들이 있다면 댓글이나 이메일을 남겨주세요.