도커/쿠버네티스 컨테이너 개발 실전 입문 · · 10 min read
CH09. 가벼운 도커 이미지 만들기
- 도커 이미지의 최적하 보다는 어플리케이션의 동작을 우선시했다.
- 일정 규모 이상의 어플리케이션을 본격적으로 구축하려면 도커 이미지 크기에도 신경 써야 한다.
1. 가벼운 도커 이미지가 왜 필요할까
- 이미지 크기 증가에 따라 나타는 문제
- 이미지 크기가 미치는 영향
- 이미지 빌드 시간(기반 이미지 다운로드 시간 포함)
- 이미지를 도커 레지스트리에 등록하는 시간
- 컨테이너를 실행할 호스트 혹은 노드에서 이미지를 다운로드하는 시간
- 나비효과
- 클러스터를 구성하는 노드의 디스크 용량 낭비
- CI 소요 시간 증가
- 개발 중 시행착오 소요 시간 증가로 인한 생산성 저하
- 오토 스케일리으로 컨테이너가 투입되는 소요 시간 증가(노드에 이미지가 없는 경우 새로 받아와야 하므로)
- 새 노드에 얼마나 빨리 도커 이미지를 배치할 수 있는가가 중요하다.
- 이미지 크기가 미치는 영향
2. 기반 이미지를 가볍게
- scratch
- INTRO
- 빈 도커 이미지로, 도커가 이름을 예약한 특수 이미지다.
- Dockerfile의 FROM 인스트럭션에서 참조만 가능하다.
- 컨테이너 외부로부터 파일을 주입받은 후에야 실체를 갖게 된다.
- 현재 존재하는 모든 도커 이미지의 기반 이미지를 따라가 보면 결국 이 scratch 이미지에 이르게 된다.
- scratch 이미지는 모든 이미지의 조상 격이다.
- scratch 이미지로 만든 이미지 빌드 들여다보기
- ubuntu:trusty 이미지의 Dockerfile
FROM scratch ADD ubuntu-trusty-core-cloudimg-amd64-root.tar.gz / RUN set -xe \ \ && echo '#!/bin/sh' > /usr/sbin/policy-rc.d \ && echo 'exit 101' >> /usr/sbin/policy-rc.d \ && chmod +x /usr/sbin/policy-rc.d \ \ && dpkg-divert --local --rename --add /sbin/initctl \ && cp -a /usr/sbin/policy-rc.d /sbin/initctl \ && sed -i 's/^exit.*/exit 0/' /sbin/initctl \ \ && echo 'force-unsafe-io' > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup \ \ && echo 'DPkg::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true"; };' > /etc/apt/apt.conf.d/docker-clean \ && echo 'APT::Update::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial*.deb /var/cache/apt/*.bin || true"; };' >> /etc/apt/apt.conf.d/docker-clean \ && echo 'Dir::Cache::pkgcache ""; Dir::Cache::srcpkgcache "";' >> /etc/apt/apt.conf.d/docker-clean \ \ && echo 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/docker-no-languages \ \ && echo 'Acquire::GzipIndexes "true"; Acquire::CompressionTypes::Order:: "gz";' > /etc/apt/apt.conf.d/docker-gzip-indexes \ \ && echo 'Apt::AutoRemove::SuggestsImportant "false";' > /etc/apt/apt.conf.d/docker-autoremove-suggests RUN rm -rf /var/lib/apt/lists/* RUN sed -i 's/^#\s*\(deb.*universe\)$/\1/g' /etc/apt/sources.list CMD ["/bin/bash"]- ubuntu-trusty-core-cloudimg-amd64-root.tar.gz : 우분투 운영체제를 구성하는 최소한의 파일을 디렉토리 구조 그대로 압축한 것
- ADD 인스트럭션은 COPY와 다르게 압축해제 기능이 있다.
- CMD 인스트럭션에 /bin/bash 명령을 지정하여 컨테이너가 /bin/bash를 포어그라운드로 실행하므로 우분투 서버 자체가 상주 형태로 실행되는 것처럼 보인다.
- ubuntu:trusty 이미지의 Dockerfile
- 어플리케이션을 실행하는데 운영 체제를 구성하는 파일을 모두 사용하는 것은 아니기 때문에 대부분의 파일은 쓸모없이 이미지의 용량만 차지한다.
- 간단한 이미지 만들기
- hello.go 작성
package main import "fmt" func main() { fmt.Println("hello, scratch!") } - 바이너리를 복사할 수 있도록 컨테이너 밖에서 컴파일한다.
$ GOOS=linux GOARCH=amd64 go build hello.go - Dockerfile 작성
FROM scratch COPY hello / CMD ["/hello"] - 이미지 빌드
$ docker image build ch09/hello:latest . - 컨테이너 실행
$ docker container run -t ch09/hello:latest - 이미지 크기 확인
$ docker image ls | grep hello - 프로그래밍 언어로 된 어플리케이션은 언어 런타임이나 라이브러리 등을 이미지에 포함해야 하므로 Go 언어에 비해서 이미지가 커지는 경향이 있다.
- hello.go 작성
- 네이티브 라이브러리 링크
- C언어나 Go 언어에서 빌드한 실행 바이너리를 컨테이너에 복사하기만 하면 된다.
- 어플리케이션이 동적 링크를 사용하는 경우 주의해야 한다.
- 빌드시 네이티브 의존 라이브러리를 정적 링크하도록 하면 이미지를 만드는 수고를 크게 덜 수 있다.
- 루트 인증서
- TLS/SSL이 적용된 웹 사이트에 접근
- hello.go 수정
package main import ( "fmt" "io/ioutil" "net/http" ) func main() { fmt.Println("hello, scratch!") resp, err := http.Get("https://gihyo.jp/") if err != nil { panic(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(string(body)) } - 이 어플리케이션을 빌드하고 컨테이너로 실행하면 인증서 오류가 발생한다.
- TLS/SSL이 적용된 HTTPS 웹 사이트에 접근하려면 루트 인증서가 필요하다.
- 일반적인 운영체제라면 루트 인증서가 포함되지만 scratch 이미지에는 루트 인증서 자체가 없기 때문에 오류가 발생한다.
- Dockerfile 수정
FROM scratch COPY hello / COPY cacert.pem /etc/ssl/certs/ CMD ["/hello"] - 운영 체제가 포함된 이미지라면 패키지 관리자를 통해 필요한 라이브러리나 도구를 추가할 수 있지만 scratch 이미지에는 패키지 관리자 같은 것이 존재하지 않는다.
- scratch 이미지의 실용성
- scratch 이미지에서 동작하려면 빌드부터 시작해야 하는 것은 물론, 다양한 의존 모듈이 필요하다.
- 디버그를 하려고 해도 셸이 없기 때문에 컨테이너 안의 상황을 파악하기가 어렵다.
- 이 점은 이미지의 크기와 트레이드 오프 관계를 가지므로 이러한 부분을 포기하더라도 작은 이미지가 필요한 상황이라면 scratch도 선택지에 포함시킬 수 있다.
- INTRO
- busybox
- INTRO
- busybox는 임베디드 시스템에서 많이 사용하는 리눅스 배포판이다.
- busybox는 수백 종 이상의 기본 유틸리티를 갖추고 있다.
- 일반적인 운영 체제와 달리, 단일 바이너리 파일(/bin/busybox)에 모든 유틸리티가 포함된 형태다.
- 모든 명령어 파일이 /bin/busybox로 하드 링킹되어 있다.
- 표준 C 라이브러별 이미지
- glibc
- GNU C 라이브러리를 의미한다.
- 다양한 스펙 및 독자적인 확장 기능을 제공하기 때문에 유연하지만 그만큼 라이브러리 크기가 크다.
- 잘 사용하지 않는 기능 때문에 디스크 용량을 낭비한다는 문제가 있다.
- uclibc
- 임베디드 환경에 특화된 가벼운 라이브러리로 추린 것이다.
- musl
- musl은 정적 링크에 최적화되어 있어 어플리케이션을 이동에 유리한 단일 바이너리로 빌드하기에 좋다.
- POSIX 표준을 더 충실히 구현한다.
- 아키텍처나 운영 체제가 다른 환경에서 glibc로 빌드한 바이너리는 uClibc나 musl을 사용한 컨테이너에서 실행이 되지 않는다.
- 빌드 환경과 실행 환경의 차이를 배제하기 위해 -glibc, -uclibc, -musl과 같은 라이브러리별 이미지를 제공한다.
- glibc
- busybox의 실용성
- 최소한의 운영 체제 기능을 제공하면서도 운영 체제로 인한 이미지 크기 증가는 거의 제로에 가깝게 줄여준다.
- busybox를 포함한 이미지는 scratch로 만든 이미지보다 1MB 정도 크기가 크지만, 셸을 포함하고 있어 컨테이너 안에서 디버깅 작업이 가능하다.
- 에이전트 타입 어플리케이션이나 명령행 도구를 사용하는데 사용할 수 있다.(scratch와 마찬가지로)
- 패키지 관리자가 없다.
- 이미지 크기에 구애받지 않거나 이미지 빌드가 까다로운 경우에는 우분투나 CentOS 등의 이미지를 사용하는 것이 편리하다.
- INTRO
- alpine
- INTRO
- busybox를 기반으로 한 리눅스 배포판으로 ‘보안, 간결함, 리소스 효율을 중시하는 파워 유저’를 대상으로 설계했으며, 표준 C 라이브러리 구현체 중 musl을 사용한다.
- 리눅스 배포판의 표준이라고 할 수 있다.
- 패키지 관리자인 apk를 사용할 수 있다.
- Dockerfile의 내용이 너무 장황하거나 이미지 빌드에 필요한 사전 준비가 지나치게 늘어나면 이미지 전체 내용을 파악하기 어려워진다. 이런 문제를 해결하려면 쓸 만한 패키지 관리자가 반드시 필요하다.
- alpine 컨테이너 실행하기
$ docker container run -it alpine:3.7 sh - 패키지 관리자 apk 사용법
- apk의 패키지 리포지토리가 중앙 집권적이라는 특징이 있다.
- apk update : 로컬에 캐싱된 apk 패키지 인덱스를 업데이트하는 명령.
- apk search : 현재 사용할 수 있는 패키지를 검색하는 명령.
- apk add : 패키지를 설치하는 명령.
--no-cache옵션- /var/cache/apk 디렉터리에 저장된 apk 인덱스 대신 새로 받아온 인덱스 정보를 이용해 패키지를 설치한다.
- /var/cache/apk 디렉터리에 캐시를 저장하지 않으므로 이 디렉터리의 내용을 따로 삭제할 필요도 없다.
--virtual옵션- 여러 패키지를 합쳐 하나의 별명을 붙이는 기능을 제공한다.
- 빌드 시에만 필요한 이미지를 그대로 남겨두면 이미지 크기가 불필요하게 증가할 수 있으므로 이 라이브러리를 제거하는 것이 좋다.
- 불필요한 패키지를 한번에 제거할 수 있다.(
apk del [별명])
- apk del : 설치된 패키지를 제거하는 명령
- 알파인 리눅스 기반 도커 이미지 만들기
- 2장의 echo를 사용한다.
- main.go
import ( "fmt" "log" "net/http" ) func main() { log.Println("start server") bind = ":8080" http.HandleFunc("/", func(w http.ResponseWriter, r * http.Request){ log.Println("received request") fmt.Fprintf(w, "Hello Docker!!!") }) http.ListenAndServe(bind, nil) } - Dockerfile
FROM alpine:3.7 # (0) workspace 등록 및 환경 변수 등록 WORKDIR / ENV GOPATH /go # (1) 빌드 시에만 필요한 라이브리 및 도구 설치 RUN apk add --no-cache --virtual=build-devs go git gcc g++ # (2) 실행 시에도 필요한 라이브리 및 도구 설치 RUN apk add --no-cache ca-certificates # (3) echo를 빌드해 실행 파일을 만듬 COPY . /go/src/github.com/lameld/echo RUN cd /go/src/github.com/lameld/echo && go build -o bin/echo main.go RUN cd /go/src/github.com/lameld/echo && cp bin/echo /usr/local/bin/ # (4) 빌드 시에만 필요한 라이브러리 및 도구 제거 RUN apk del --no-cache build-devs CMD ["echo"] - 이미지 빌드
$ docker image build -t ch09/echo:latest .
- INTRO
3. 가벼운 도커 이미지 만들기
- 배포 대상 어플리케이션 크기 줄이기
- 어플리케이션 크기 최적화
- 불필요한 파일 삭제
- 불필요한 프로그램 최소화
- 의존 라이브러리 최소화
- 웹 어플리케이션의 애셋(주로 이미지) 용량 줄이기
- .dockerignore 파일
- 불필요한 파일 및 디렉터리가 컨텡너에 들어가지 않게 하는 것이 중요하다.
- .git 디렉토리로 대표되는 불필요함 숨김 파일이 컨테이너에 들어가기 쉽다.
- Dockerfile과 같은 디렉토리에에 위치차 .dockerignore 파일에 컨테이너 포함하지 않을 파일이나 디렉토리를 정의하는 기능이다.
- .dockerignore
.git .idea *.swp *.log .DS_STORE
- 어플리케이션 크기 최적화
- 도커 이미지의 레이어 구조 고려하기
- INTRO
- 이미지의 크기를 최적화하려면 이 내부 구조까지 깊이 이해할 필요가 있다.
- echo 이미지를 빌드해 보면 다음과 같은 내용이 출력된다.
- 각 단계별로 Dockerfile에 기술된 명령이 실행되는 것을 알 수 있다.
$ docker image build -t ch09/echo:latest . Sending build context to Docker daemon 3.072kB Step 1/10 : FROM alpine:3.7 ---> 6d1ef012b567 Step 2/10 : WORKDIR / ---> Using cache ---> 96f74ddcf3b7 Step 3/10 : ENV GOPATH /go ---> Using cache ---> acaeee72e27d Step 4/10 : RUN apk add --no-cache --virtual=build-devs go git gcc g++ ---> Using cache ---> fa4f43acbc98 Step 5/10 : RUN apk add --no-cache ca-certificates ---> Using cache ---> 1ad9964de84c Step 6/10 : COPY . /go/src/github.com/lameld/echo ---> Using cache ---> f84186eefbc7 Step 7/10 : RUN cd /go/src/github.com/lameld/echo && go build -o bin/echo main.go ---> Using cache ---> 77ffccfd40f7 Step 8/10 : RUN cd /go/src/github.com/lameld/echo && cp bin/echo /usr/local/bin/ ---> Using cache ---> 56242e05b9ce Step 9/10 : RUN apk del --no-cache build-devs ---> Using cache ---> 9d06ac4d82e2 Step 10/10 : CMD ["echo"] ---> Using cache ---> 7bce58ed9dea Successfully built 7bce58ed9dea Successfully tagged ch09/echo:latest - 해당 이미지가 어떤 레이어로 구성되었는지 자세한 정보 보기 :
docker image history$ docker image history ch09/echo:latest IMAGE CREATED CREATED BY SIZE COMMENT 7bce58ed9dea 2 minutes ago /bin/sh -c #(nop) CMD ["echo"] 0B 9d06ac4d82e2 2 minutes ago /bin/sh -c apk del --no-cache build-devs 34kB 56242e05b9ce 2 minutes ago /bin/sh -c cd /go/src/github.com/lameld/echo… 7.52MB 77ffccfd40f7 2 minutes ago /bin/sh -c cd /go/src/github.com/lameld/echo… 7.52MB f84186eefbc7 2 minutes ago /bin/sh -c #(nop) COPY dir:afb826e60552ed0da… 713B 1ad9964de84c 2 minutes ago /bin/sh -c apk add --no-cache ca-certificates 515kB fa4f43acbc98 2 minutes ago /bin/sh -c apk add --no-cache --virtual=buil… 359MB acaeee72e27d 3 minutes ago /bin/sh -c #(nop) ENV GOPATH=/go 0B 96f74ddcf3b7 3 minutes ago /bin/sh -c #(nop) WORKDIR / 0B 6d1ef012b567 16 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 16 months ago /bin/sh -c #(nop) ADD file:aa17928040e31624c… 4.21MB - 각 레이어에 해당하는 이미지는 도커 파일 시스템 안에 tar.gz 파일에 저장된다.
- 레이어 자체도 이미지이며, 중간 과정에 특정한 태그를 붙일 수 있다.
$ docker image tag f84186eefbc7 ch09/echo:copy
- 레이어의 수를 최대한 줄이기
- 이미지를 구성하는 레이어 자체도 이미지 파일 형태로 디스크에 존재한다.
- 때문에 빌드 중에 필요했던 파일도 최종 결과물이 될 이미지에서 불필요한 파일이라면 결국 최종 이미지에 불필요한 파일에 들어간다.
- entrykit 이미지
- Dockerfile
FROM alpine:3.7 RUN apk add --no-cache wget RUN wget https://github.com/progrium/entrykit/releases/download/v0.4.0/entrykit_0.4.0_Linux_x86_64.tgz RUN tar -xvzf entrykit_0.4.0_Linux_x86_64.tgz RUN rm entrykit_0.4.0_Linux_x86_64.tgz RUN mv entrykit /bin/entrykit RUN chmod +x /bin/entrykit RUN entrykit --symlink - 이미지 빌드
$ docker image build -t ch09/entrykit:standard . Sending build context to Docker daemon 2.048kB Step 1/8 : FROM alpine:3.7 ---> 6d1ef012b567 Step 2/8 : RUN apk add --no-cache wget ---> Using cache ---> 9ddb2b2b256e Step 3/8 : RUN wget https://github.com/progrium/entrykit/releases/download/v0.4.0/entrykit_0.4.0_Linux_x86_64.tgz ---> Using cache ---> bbb9cf07b1a2 Step 4/8 : RUN tar -xvzf entrykit_0.4.0_Linux_x86_64.tgz ---> Using cache ---> 72ed24750e29 Step 5/8 : RUN rm entrykit_0.4.0_Linux_x86_64.tgz ---> Using cache ---> 063c338b89b0 Step 6/8 : RUN mv entrykit /bin/entrykit ---> Using cache ---> 13ae51cb1f1b Step 7/8 : RUN chmod +x /bin/entrykit ---> Using cache ---> dbfc1a91f15f Step 8/8 : RUN entrykit --symlink ---> Using cache ---> 5e6c02d2e7c0 Successfully built 5e6c02d2e7c0 Successfully tagged ch09/entrykit:standard - 이미지 레이어
$ docker image history ch09/entrykit:standard IMAGE CREATED CREATED BY SIZE COMMENT 5e6c02d2e7c0 5 minutes ago /bin/sh -c entrykit --symlink 32B dbfc1a91f15f 5 minutes ago /bin/sh -c chmod +x /bin/entrykit 9.19MB 13ae51cb1f1b 5 minutes ago /bin/sh -c mv entrykit /bin/entrykit 9.19MB 063c338b89b0 5 minutes ago /bin/sh -c rm entrykit_0.4.0_Linux_x86_64.tgz 0B 72ed24750e29 5 minutes ago /bin/sh -c tar -xvzf entrykit_0.4.0_Linux_x8… 9.19MB bbb9cf07b1a2 5 minutes ago /bin/sh -c wget https://github.com/progrium/… 2.71MB 9ddb2b2b256e 6 minutes ago /bin/sh -c apk add --no-cache wget 480kB 6d1ef012b567 16 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 16 months ago /bin/sh -c #(nop) ADD file:aa17928040e31624c… 4.21MB - 각 레이어에서 파일을 조작할 때마다 최종 이미지의 크기가 증가함을 알 수 있다.
- 이런 현상을 피하려면 Dockerfile을 빌드 해 생성되는 이미지의 레이어 수를 줄이는 것이 가장 효과적이다.
- Dockerfile.light
FROM alpine:3.7 RUN apk add --no-cache wget && \ wget https://github.com/progrium/entrykit/releases/download/v0.4.0/entrykit_0.4.0_Linux_x86_64.tgz && \ tar -xvzf entrykit_0.4.0_Linux_x86_64.tgz && \ rm entrykit_0.4.0_Linux_x86_64.tgz && \ mv entrykit /bin/entrykit && \ chmod +x /bin/entrykit && \ entrykit --symlink - 이미지 빌드
$ docker image built -f Dockerfile.light -t ch09/entrykit:light . Sending build context to Docker daemon 3.072kB Step 1/2 : FROM alpine:3.7 ---> 6d1ef012b567 Step 2/2 : RUN apk add --no-cache wget && wget https://github.com/progrium/entrykit/releases/download/v0.4.0/entrykit_0.4.0_Linux_x86_64.tgz && tar -xvzf entrykit_0.4.0_Linux_x86_64.tgz && rm entrykit_0.4.0_Linux_x86_64.tgz && mv entrykit /bin/entrykit && chmod +x /bin/entrykit && entrykit --symlink ---> Using cache ---> b504a789d31f Successfully built b504a789d31f Successfully tagged ch09/entrykit:light - 이미지 레이어
$ docker image history ch09/entrykit:light IMAGE CREATED CREATED BY SIZE COMMENT b504a789d31f About a minute ago /bin/sh -c apk add --no-cache wget && wget h… 9.67MB 6d1ef012b567 16 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 16 months ago /bin/sh -c #(nop) ADD file:aa17928040e31624c… 4.21MB
- Dockerfile
- 가독성과 이미지의 트레이드 오프
- &&나 백슬래시를 만힝 사용해야 하며 빌드 스타일에 따라 cd 명령을 자주 사용하게 되므로 Dockerfile의 가독성을 해치게 된다.
- 이미지 빌드를 시행착오와 함께 반복해야 하는 경우라면 중간 빌드를 재활용하는 이점을 누릴 수가 없다.
- 이는 불가피하게 개발 효율을 떨어뜨리므로 개발 과정에서는 각 명령을 별도로 RUN 인스트럭션으로 실행한다.
- 빌드 내용이 고정된 다음에 RUN 인스트럭션의 실행 횟수를 줄이면 된다.
- 무리하게 모든 경우에 적용하지 않는 것이 좋다.
- 이 방법이 유용한 경우는 수 MB 이상의 파일으 자주 다루는 과정이 빌드에 포함되는 경우다.
- 중간 레이어 개수 자체가 적은 경우에는 굳이 적용할 필요가 없다.
- INTRO
4. 멀티 스테이지 빌드
- 빌드 부산물을 완전히 제가할 수 없다는 한계의 대책 : 멀티 스테이지 빌드
- 빌드 컨테이너와 실행 컨테이너의 분리
- 적용 전
FROM golang:1.13 WORKDIR / ENV GOPATH /go COPY . /go/src/github.com/gihyodocker/todoapi RUN go get github.com/go-sql-driver/mysql RUN go get gopkg.in/gorp.v1 RUN cd /go/src/github.com/gihyodocker/todoapi && go build -o bin/todoapi cmd/main.go RUN cd /go/src/github.com/gihyodocker/todoapi && cp bin/todoapi /usr/local/bin/ CMD ["todoapi"] - 적용 후
FROM golang:1.13 AS build WORKDIR / ENV GOPATH /go COPY . /go/src/github.com/gihyodocker/todoapi RUN go get github.com/go-sql-driver/mysql RUN go get gopkg.in/gorp.v1 RUN cd /go/src/github.com/gihyodocker/todoapi && go build -o bin/todoapi cmd/main.go FROM apline:3.7 COPY --from=build /go/src/github.com/gihyodocker/todoapi && cp bin/todoapi /usr/local/bin/ CMD ["todoapi"] - 빌드 산출물을 다른 컨테이너로 복사할 수 있다.
- 실행 컨테이너에 불필요한 파일을 전혀 포함하지 않고 가벼운 이미지를 만들 수 있다.
- Dockerfile의 가독성을 유지하면서도 이미지의 크기를 억제할 수 있다.
- 장점
- 이미지 크기 감소뿐만 아니라 이식성에도 도움을 준다.
- 하나의 Dockerfile 안에서 컨테이너 간에 파일을 주고 받기 때문에 도커 호스트와 상관 없이 항상 같은 최종 결과물을 얻을 수 있다.
- 적용 전