땡글이LAB

[도커/쿠버네티스] Dockerfile 본문

Devops/도커, 쿠버네티스

[도커/쿠버네티스] Dockerfile

땡글이B 2022. 7. 18. 21:36

이미지 생성하기

 이미지를 생성할 때 생각나는 일반적인 프로세스는 다음과 같을 것이다.

  1. 이미지(CentOS, Ubuntu 등)으로 컨테이너를 생성
  2. 애플리케이션을 위한 환경을 설치하고 소스코드 등을 복사해 잘 동작하는 것을 확인
  3. 컨테이너를 이미지로 커밋(commit)

 하지만, 위의 방법을 이용하면 애플리케이션이 동작하는 환경을 구성하기 위해 일일이 수작업으로 패키지를 설치하고 소스코드를 깃(Git)에서 복제하거나 호스트에서 복사해야 한다. 직접 컨테이너에서 애플리케이션을 구동해보고 이미지로 커밋하기 때문에 이미지의 동작을 보장할 수 있다는 장점도 있지만 매번 일일이 수작업을 하기에는 너무 힘들다.

 

 

Dockerfile

 도커는 위와 같은 일련의 과정을 손쉽게 기록하고 수행할 수 있는 빌드(build) 명령어를 제공한다. 완성된 이미지를 생성하기 위해 컨테이너에 설치해야하는 패키지, 추가해야 하는 소스코드, 실행해야 하는 명령어와 셸 스크립트 등을 하나의 파일에 기록해두면 도커는 이 파일을 읽어 컨테이너에서 작업을 수행한 뒤 이미지로 만들어낸다.

 

 이러한 작업을 기록한 파일의 이름을 Dockerfile이라고 하며 빌드 명령어는 Dockerfile을 읽어 이미지를 생성한다. Dockerfile을 생성하면 직접 컨테이너를 생성하고 이미지로 커밋해야하는 번거로움을 덜 수 있을 뿐더러 깃과 같은 개발 도구를 통해 애플리케이션의 빌드 및 배포를 자동화할 수 있다.

 

 

Dockerfile 작성

 앞에서 설명한 것처럼 Dockerfile에는 컨테이너에서 수행해야 할 작업을 명시한다. 이 작업을 Dockerfile에 정의하기 위해서는 Dockerfile에서 쓰이는 명령어를 알아둘 필요가 있다. 

 

 간단한 예시로 웹 서버 이미지를 생성해보도록 한다. 이번 예시에서 사용할 디렉터리를 생성하고 해당 디렉터리 안에 HTML 파일을 생성한다. 

$ mkdir dockerfile && cd dockerfile

$ echo test >> test.html

 

 새롭게 생성한 디렉터리 내부에서 아래의 내용으로 Dockerfile이라는 이름의 파일을 저장한다. 아래의 Dockerfile은 이미지에 아파치 웹 서버를 설치한 뒤, 로컬에 있는 test.html 파일을 웹 서버로 접근할 수 있는 컨테이너의 디렉터리인 /var/www/html 에 복사한다.

 

# vi Dockerfile

FROM ubuntu:14.04
MAINTAINER bohub12
LABEL "purpose"="practice"
RUN apt-get update
RUN apt-get install apache2 -y
ADD test.html /var/www/html
WORKDIR /var/www/html
RUN ["/bin/bash", "-c", "echo hello >> test2.html"]
EXPOSE 80
CMD apachectl -DFOREGROUND

 Dockerfile 에서 사용되는 명령어에는 여러 가지가 있는데, 위의 파일에서는 FROM, RUN, ADD 등의 기초적인 명령어를 다루었다. Dockerfile은 한 줄이 하나의 명령어가 되고, 명령어(instruction)을 명시한 뒤 옵션을 추가하는 방식이다. 명령어를 소문자로 표기해도 상관은 없지만 일반적으로 대문자로 표기한다.

  • 도커 엔진은 Dockerfile을 읽어 들일 때 기본적으로 현재 디렉터리에 있는 Dockerfile이라는 이름을 가진 파일을 선택한다. 따라서 이 예시에서 Dockerfile이라는 이름의 파일로 저장했다. Dockerfile은 빈 디렉터리에 저장하는 것이 좋은데, 이는 이미지를 빌드할 때 사용되는 컨텍스트(Context) 때문이다. 자세한 내용은 추후에 살펴본다.

 

[각 명령어에 대한 설명]

  • FROM
    • 생성할 이미지의 베이스가 될 이미지를 뜻한다. FROM 명령어는 Dockerfile을 작성할 때 반드시 한 번 이상 입력해야 하며, 이미지 이름의 포맷은 docker run 명령어에서 이미지 이름을 사용했을 때와 같다. 사용하려는 이미지가 도커에 없다면 자동으로 pull한다.
  • MAINTAINER
    • 이미지를 생성한 개발자의 정보를 나타낸다. 일반적으로 Dockerfile을 작성한 사람과 연락할 수 있는 이메일 등을 입력한다. 단, 도커 1.13.0 버전 이후로 사용하지 않기에 최근에는 LABEL maintainer "bohub12 <dddd@naver.com>"과 같이 표현한다.
  • LABEL
    • 이미지에 메타데이터를 추가한다. 메타데이터는 '키:값'의 형태로 저장되며, 여러 개의 메타데이터가 저장될 수 있다. 추가된 메타데이터는 docker inspect 명령어로 확인가능하다.
  • RUN
    • 이미지를 만들기 위해 컨테이너 내부에서 명령어를 실행한다. 해당 예제에서는 apt-get update와 apt-get install apache2 명령어를 실행하기 때문에 아파치 웹 서버가 설치된 이미지가 생성된다. 단, Dockerfile을 이미지로 빌드하는 과정에서는 별도의 입력이 불가능하기 때문에 apt-get install apache2 명령어에서 설치할 것인지를 선택하는 Y/N을 Yes로 설정해야 한다. 이미지를 빌드할 때 입력을 받아야 하는 RUN이 있다면 build 명령어는 이를 오류로 간주하고 빌드를 종료한다.
  • ADD
    • 파일을 이미지에 추가한다. 추가하는 파일은 Dockerfile이 위치한 디렉터리인 컨텍스트(Context)에서 가져온다. Dockerfile이 위치한 디렉터리에서 파일을 가져온다고 생각하면 된다. 추후에 컨텍스트(Context)에 대해 자세히 다룬다. 예제에서는 Dockerfile이 위치한 디렉터리에서 test.html 파일을 이미지의 /var/www/html 디렉터리에 추가한다. ADD 명령어는 JSON 배열의 형태로 ["추가할 파일 이름", .... "컨테이너에 추가될 위치"] 와 같이 사용할 수 있다. 추가할 파일명은 여러 개를 지정할 수 있고 배열의 마지막 원소가 컨테이너에 추가할 위치이다.  
  • WORKDIR
    • 명령어를 실행할 디렉터리를 나타낸다. cd 명령어와 같은 기능을 한다. 즉, WORKDIR /var/www/html 이 실행되고 나서 RUN touch test를 실행하면 /var/www/html 디렉터리에 test 파일이 생성된다.
  • EXPOSE
    • Dockerfile 의 빌드로 생성된 이미지에서 노출할 포트를 설정한다. 그러나 EXPOSE를 설정한 이미지로 컨테이너를 생성했다고 해서 반드시 이 포트가 호스트의 포트와 바인딩되는 것은 아니며, 단지 컨테이너의 80번 포트를 사용할 것임을 나타내는 것 뿐이다. 
  • CMD
    • CMD는 컨테이너가 시작될 때마다 실행할 명령어(커맨드)를 설정하며, Dockerfile에서 한 번만 사용할 수 있다. Dockerfile에 CMD를 명시함으로써 이미지에 apachectl -DFOREGROUND라는 커맨드를 내장하면 컨테이너를 생성할 때 별도의 커맨드를 입력하지 않아도 이미지에 내장된 apachectl -DFOREGROUND 라는 커맨드가 적용되어 컨테이너가 시작될 때 자동으로 아파치 웹서버가 실행될 것이다. 그리고 아파치 웹서버는 하나의 터미널을 차지하는 포그라운드 모드로 실행되기 때문에 -d 옵션을 사용해 detached 모드로 컨테이너를 생성해야 한다. 

 

Dockerfile 빌드

 Dockerfile의 기본적인 명령어를 이해했다면 앞에서 만든 Dockerfile을 빌드해보겠다. 빌드 명령어는 다음과 같다. 

$ docker build -t mybuild:0.0 ./

 -t 옵션은 생성될 이미지의 이름을 설정한다. 위 명령을 실행하면 mybuild:0.0 이라는 이름의 이미지가 생성된다. -t 옵션을 사용하지 않으면 16진수 형태의 이름으로 이미지가 저장되므로 가급적이면 -t 옵션을 사용하는 것이 좋다. build 명령어의 끝에는 Dockerfile이 저장된 경로를 입력한다. 일반적으로 로컬에 저장된 Dockerfile을 사용하지만 외부 URL로부터 Dockerfile의 내용을 가져와 빌드할 수도 있다. 예시에서는 로컬에 Dockerfile을 저장했으므로 ./(현재디렉터리)를 입력했다.

 

 최종적으로 mybuild:0.0이라는 이름의 이미지가 생성된다. 해당 이미지에는 아파치 웹 서버가 설치돼있으며, 컨테이너가 시작될 때 웹 서버를 실행하도록 CMD(커맨드)를 설정했기 때문에 별다른 설정 없이도 웹 서버가 실행된다.

 

 이제 만들어진 이미지로 컨테이너를 실행해본다.

$ docker run -d -P --name myserver mybuild:0.0

 -P 옵션은 이미지에 설정된 EXPOSE의 모든 포트를 호스트에 연결하도록 설정한다. 위 예시에서는 Dockerfile에서 EXPOSE를 80번으로 설정했으며 이는 이미지에 '컨테이너의 80번 포트를 사용한다'는 것을 의미한다. 즉, 이미지를 생성하기 위한 Dockerfile을 작성하는 개발자로서는 EXPOSE를 이용해 이미지가 실제로 사용될 때 어떤 포트가 사용돼야 하는지 명시할 수 있으며, 이미지를 사용하는 입장에서는 컨테이너의 애플리케이션이 컨테이너 내부에서 어떤 포트를 사용하는지 알 수 있게 된다. -P 옵션은 EXPOSE로 노출된 포트를 호스트에서 사용가능한 포트에 차례로 연결하므로 이 컨테이너가 호스트의 어떤 포트와 연결됐는지 확인할 필요가 있다.

 

 

빌드 컨텍스트 (빌드 과정 살펴보기)

 build 명령어를 입력했을 때 많은 내용이 출력됐다. 내용 중 대부분은 Dockerfile의 RUN 을 실행해서 컨테이너 내부에서 발생한 표준 출력이지만, 이미지를 생성하는 부분은 조금 눈여겨볼 필요가 있다. 

 

 이미지 빌드를 시작하면 도커는 가장 먼저 빌드 컨텍스트를 읽어들인다. 빌드 컨텍스트는 이미지를 생성하는 데 필요한 각종 파일, 소스코드, 메타데이터 등을 담고 있는 디렉터리를 의미하며 Dockerfile이 위치한 디렉터리가 빌드 컨텍스트가 된다.

 

 빌드 컨텍스트는 Dockerfile에서 빌드될 이미지에 파일을 추가할 때 사용된다. Dockerfile 에서 이미지에 파일을 추가하는 방법은 앞에서 설명한 ADD 외에도 COPY 가 있는데, 이 명령어들은 빌드 컨텍스트의 파일을 이미지에 추가한다. 위 예제에서는 빌드 경로를 ./로 지정함으로써 test.html 파일을 빌드 컨텍스트에 추가했으며, ADD 명령어를 통해 빌드 컨텍스트에서 test.html 파일을 이미지에 추가한다. 

 

 컨텍스트에 대한 정보는 이미지를 빌드할 때 출력된 내용 중 맨 위에 위치한다. 

 컨텍스트는 build 명령어의 맨 마지막에 지정된 위치에 있는 파일을 전부 포함한다. 깃(Git)과 같은 외부 URL에서 Dockerfile을 읽어들인다면 해당 저장소(Repository)에 있는 파일과 서브 모듈을 포함한다. 따라서 Dockerfile이 위치한 곳에는 이미지 빌드에 필요한 파일만 있는 것이 바람직하며, 루트 디렉터리(/)와 같은 곳에서 이미지를 빌드하지 않도록 주의해야한다. 컨텍스트는 단순 파일 뿐 아니라 하위 디렉터리도 전부 포함하게 되므로 빌드에 불필요한 파일이 포함된다면 빌드 속도가 느려질뿐더러 호스트의 메모리를 지나치게 점유할 수 있다.

 

 이를 방지하기 위해 깃에서 사용하는 .gitignore와 유사한 기능을 사용할 수 있다. .dockerignore 라는 파일을 작성하면 빌드 시 이 파일에 명시된 이름의 파일을 컨텍스트에서 제외한다. (.dockerignore 파일은 Dockerfile이 위치한 디렉터리와 같아야 함)

 

Dockerfile을 이용한 컨테이너 생성과 커밋

 build 명령어는 Dockerfile에 기록된 대로 컨테이너를 실행한 뒤 완성된 이미지를 만들어낸다. 그렇지만 이미지로 만드는 과정이 하나의 컨테이너에서 일어나는 것은 아니다. 이미지를 빌드할 때 나오는 다음과 같은 출력 결과를 통해 이를 어느정도 짐작할 수 있다.

 

 위의 각 Step은 Dockerfile에 기록된 명령어에 해당한다. 각 명령어가 실행될 때마다 새로운 컨테이너가 하나씩 생성되며 이를 이미지로 커밋한다. 즉, Dockerfile에서 명려어 한 줄이 실행될 때마다 이전 step 에서 생성된 이미지에 의해 새로운 컨테이너가 생성되며, Dockerfile에 적힌 명령어를 수행하고 다시 새로운 이미지 레이어로 저장한다. 

 

 따라서 이미지의 빌드가 완료되면 Dockerfile 명령어 줄 수 만큼 레이어가 존재하게 되며 중간에 컨테이너도 같은 수만큼 생성되고 삭제된다. 위 출력 결과에서 Removing intermediate container...는 중간에 이미지 레이어를 생성하기 위해 임시로 생성된 컨테이너를 삭제하는 것이고 삭제되기 전 출력되는 ID 커밋된 이미지 레이어를 의미한다. 

 

캐시를 이용한 이미지 빌드

 한 번 이미지 빌드를 마치고 난 뒤 다시 같은 빌드를 진행하면 이전의 이미지 빌드에서 사용했던 캐시를 사용한다. 다음 내용을 파일로 저장하고 다시 이미지를 빌드해본다. 예제 2.3에서 사용한 Dockerfile 의 내용에서 일부를 지웠다. 

 

$ vi Dockerfile2

FROM ubuntu:14.04
MAINTAINER bohub12
LABEL "purpose"="practice"
RUN apt-get update
$ docker build -f Dockerfile2 -t mycache:0.0 ./

 위의 명령어를 입력하면, 아래와 같이 출력결과가 나온다. 

 즉 Using cache 라는 출력 내용과 함께 별도의 빌드 과정이 진행되지 않고 바로 이미지가 생성됐다. 이전에 빌드했던 Dockerfile 에 같은 내용이 있다면 build 명령어는 이를 새로 빌드하지 않고 같은 명령어 줄까지 이전에 사용한 이미지 레이어를 활용해 이미지를 생성한다. 이는 같은 명령어를 여러 번 실행해야 하는 여러 개의 이미지를 빌드하거나 빌드 도중 Dockerfile의 문법과 기타 오류가 발생했을 때 불필요하게 다시 명령어를 실행하지 않게 한다.

  • 이미지 빌드 중 오류가 발생하면, build 명령어가 중지되면 이미지 레이어 생성을 위해 마지막으로 생성된 임시 컨테이너가 삭제되지 않은 채로 남게 된다. 또한 이미지의 빌드가 완전하지 않기 때문에 -t 옵션의 값으로 지정된 이미지의 이름이 아닌 <none>:<none>으로 이미지가 생성됨. 

 

 하지만 캐시가 필요없는 경우도 있다. 예로 Dockerfile에 "RUN git clond '..'" 이라는 명령어가 있는데, 깃허브 저장소에서 변경이 있었는데도 캐시를 사용하게 되면 변경사항이 Dockerfile로 생성된 이미지에 적용되지 않는다. 캐시를 사용하지 않으려면 "--no-cache" 옵션을 추가한다. 아래의 명령어 참고.

$ docker build --no-cache -t mybuild:0.0 .

 또는 캐시로 사용할 이미지를 직접 지정할 수도 있다. 특정 Dockerfile을 확장해서 사용한다면 기존의 Dockerfile로 빌드한 이미지를 빌드 캐시로 사용할 수 있다. 예를 들어, 도커 허브의 nginx 공식 저장소에서 nginx:latest 이미지를 빌드하는 Dockerfile 에 일부 내용을 추가해 사용한다면 nginx:latest 이미지를 캐시로 사용할 수 있다.

$ docker build --cache-from nginx -t my_extend_nginx:0.0 .

 

멀티 스테이지를 이용한 Dockerfile 빌드 

 일반적으로 애플리케이션을 빌드할 때 많은 의존성 패키지와 라이브러리를 필요로 한다. 예를 들어, Go로 작성된 소스코드를 빌드하기 위해서는 Go와 관련된 빌드 툴과 라이브러리가 미리 설치되어 있어야 한다. Dockerfile에서 Go 소스코드를 빌드하기 위해서 가장 먼저 생각나는 방법은 아래의 Dockerfile 처럼 1) Go와 관련된 도구들이 미리 설치된 이미지를 FROM에 명시한 뒤, 2) RUN 명령어로 소스코드를 컴파일 하는 것이다. 이 방법을 통해 Hello World를 출력한다.

 

$ vi main.go
package main
import "fmt"

func main() {
	fmt.Println("hello world!")
}
$ vi Dockerfile

FROM golang
ADD main.go /root
WORKDIR /root
RUN go build -o /root/mainApp ./main.go
CMD ["./mainApp"]
$ docker build . -t go_helloworld

 위의 프로그램을 돌리기 위해, 이미지의 크기는 무려 960MB에 달한다. 실제 실행 파일의 크기는 매우 작지만 소스코드 빌드에 사용된 각종 패키지 및 라이브러리가 불필요하게 이미지의 크기를 차지하고 있는 것이다. 

 

 17.05 버전 이상을 사용하는 도커 엔진이라면 이미지의 크기를 줄이기 위해 멀티 스테이지(Multi-stage) 빌드 방법을 사용할 수 있다. 멀티 스테이지 빌드는 하나의 Dockerfile 안에 여러 개의 FROM 이미지를 정의함으로써 빌드 완료 시 최종적으로 생성될 이미지의 크기를 줄이는 역할을 한다.

 

 

$ vi Dockerfile

FROM golang
ADD main.go /root
WORKDIR /root
RUN go build -o /root/mainApp ./main.go

FROM alpine:latest
WORKDIR /root
COPY --from=0 /root/mainApp .
CMD ["./mainApp"]

 일반적인 Dockerfile과는 달리 FROM 명령어를 통해 2개의 이미지가 명시되었다. 첫 번째 FROM 에 명시된 golang 이미지는 이전과 동일하게 main.go 파일을 /root/mainApp으로 빌드했다.

 

 그러나 두 번째 FROM 아래에서 사용된 COPY 명령어는 첫 번째 FROM 에서 사용된 이미지의 최종상태에 존재하는 /root/mainApp 파일을 두 번째 이미지인 alpine:latest에 복사한다. 이 때 --from=0은 첫 번째 FROM에서 빌드된 이미지의 최종 상태를 의미한다. 즉, 첫 번째 FROM 이미지에서 빌드한 /root/mainApp 파일을 두 번째의 FROM 에 명시된 이미지인 alpine:latest 이미지에 복사한다.

  • alpine이나 busybox와 같은 이미지는 우분투나 CentOS에 비해 이미지 크기가 매우 작지만 기본적인 프로그램 실행에 필요한 필수적인 런타임 요소가 포함되어 있는 리눅스 배포판 이미지이다. 이러한 이미지를 활용하면 경량화된 애플리케이션 이미지를 간단히 생성할 수 있다는 장점이 있다.

 위에서 만든 Dockerfile을 빌드해본다.

$ docker build . -t go_helloworld:multi-stage

 이렇게하면, 이전에 만든 이미지와 동일한 역할을 하는 이미지임에도 이미지의 최종 크기가 크게 줄은 것을 확인할 수 있다. 이와 같이 멀티 스테이지 빌드는 반드시 필요한 실행 파일만 최종 이미지 결과물에 포함시킴으로써 이미지 크기를 줄일 때 유용하게 사용할 수 있다.

 

 

References

 

시작하세요! 도커/쿠버네티스 - 교보문고

친절한 설명으로 쉽게 이해하는 컨테이너 관리 | ★ 이 책의 구성 ★◎ 도커의 기본 사용 방법을 익힘으로써 컨테이너의 기본 개념을 학습합니다. (1부 1장, 1부 2장)◎ 도커 스웜 모드를 통해 서

 

 

 

Comments