KUICS

KUICS

Docker 2

Chapter 2 도커로 응용프로그램을 패키징하기

1. 도커파일이란

자신만의 도커이미지를 패키지하기 위해서는 도커파일이라는 텍스트파일을 작성해야합니다. 도커파일에는 이미지를 만들기 위한 모든 단계가 써있습니다. 도커파일은 단순하고 제한된 영역에만 사용되는 문법을 사용합니다. 아래의 코드는 단순한 형태의 도커파일입니다.

FROM ubuntu
RUN apt-get update && apt-get install nano

이 명령어를 Dockerfile이라는 이름의 파일을 만든 뒤 그 안에 쓰면 됩니다. 이 도커파일으로 이미지를 빌드하고 그 이미지를 이용해 컨테이너를 실행시키면 nano 패키지가 깔려있는 우분투 컨테이너를 사용할 수 있게 됩니다. FROM 명령어는 기초이미지를 지정합니다. 이 기초이미지로부터 이후의 도커파일의 내용을 이용해 변화가 일어나게 됩니다. 이 경우에는 nano를 설치하기 위한 두 개의 apt명령어입니다.

이미지를 빌드하기 위해서 docker image build 명령어를 이용해야 합니다. 이 명령어를 실행시킬 때 이미지를 구분하기 위한 레포지토리 이름과 도커가 이미지를 빌드하기 위해 사용할 도커파일의 위치를 적어줘야 합니다. 또한 이미지를 라벨을 이용해 태그할 수 있습니다. 따라서 우리는 한 레포지토리에서 동시에 여러 버전의 이미지를 사용할 수 있습니다.(e.g. ubuntu:16.04ubuntu:18.04)docker image build --tag kuicsofficial/ubuntu-with-nano .명령어를 사용하면 현재 디렉토리에 있는 도커파일을 사용하여 이미지를 빌드합니다.

Result of docker image build --tag kuicsofficial/ubuntu-with-nano

Dockerfile은 따로 옵션을 주지 않았을 때의 기본 이름입니다. --file 옵션을 이용하면 임의의 이름의 파일을 이미지 빌드를 위한 도커파일로 사용할 수 있습니다. 예를 들어 server.dockerfile을 이미지 빌드를 위한 도커파일로 사용하고 싶다면 docker image build --file server.dockerfile명령어를 사용하면 됩니다.

image build명령어는 도커 서버에서 실행됩니다.(클라이언트는 단순히 정보를 전달하는 역할만을 수행합니다.) 따라서 도커파일의 위치를 명시해주어야 합니다.(여기서는 현재 폴더에 도커파일이 있으므로 .을 사용합니다.) 클라이언트는 서버에 현재 폴더의 내용을 전달하고, 서버는 이미지를 빌드할 때 사용될 폴더에 그 내용을 저장합니다. 나중에 다시 살펴보겠지만, 이 폴더를 'build context'라고 부릅니다.

이미지를 빌드할 때 레포지토리 이름을 인자로 전달할 수 있습니다. 이미지를 로컬에서 빌드할 때 원하는 이름은 무엇이든 정할 수 있지만, 일반적으로 {user}/{application}형식을 사용합니다. 여기서 {user}는 도커허브의 ID입니다.

태그는 이미지를 구분가능하게 만들어주는 구분자입니다. 즉, 도커허브의 퍼블릭이미지를 사용할 때 레포지토리 내부에는 여러 버전의 이미지가 있습니다. 만약 태그를 따로 지정하지 않는다면 도커는 자동으로 latest라는 태그를 붙입니다.

몇몇 이미지들은 {user}를 붙이지 않는 경우도 있습니다. 예를 들어, ubuntu의 경우 canonical/ubuntu가 아닌 ubuntu만으로도 작동합니다. 이러한 이미지는 공식 이미지이며 기초이미지로는 이러한 공식 이미지를 사용하는 것을 추천합니다.

이미지를 빌드하는데 성공하면, 그 이미지는 도커 서버의 로컬 캐시에 저장되며 나중에 컨테이너를 실행시키는데 사용될 수 있습니다. 또한 이미지를 도커 허브에 푸시할 수도 있습니다.

image ls명령어를 사용하면 캐시된 이미지의 목록을 보여줍니다.

Result of docker images ls

docker image ls명령어의 출력에서는 레포지토리 이름과 태그, 이미지의 ID, 만들어진 시간, 이미지의 크기를 확인할 수 있습니다. 여기서 3개의 허브에서 다운로드한 이미지가 있는 것을 볼 수 있는데, 이 이미지는 Chapter 1에서 다운받았던 이미지입니다. kuicsofficial이라는 user이름으로 만들어진 이미지는 방금 생성한 이미지입니다. 만약 더 큰 이미지를 사용한다면 로컬 캐시는 많은 용량을 차지하게 될 것입니다. 이를 관리하는 법은 Chapter 4에서 다룰 것입니다.

2. 도커파일의 명령어

필수적인 도커파일의 명령어는 FROM뿐입니다. FROM은 기초이미지를 지정하는 명령어이며, 도커파일의 최상단에 위치해야 합니다. 물론 FROM만으로는 많은 일을 할 수 없습니다. 아래는 다른 명령어들의 예시입니다.

  • RUN: 커맨드를 실행시킵니다.

  • ENV: 환경변수를 설정합니다.

  • COPY: 파일을 이미지로 복사합니다.

  • EXPOSE: 포트를 호스트에 연결할 수 있도록 노출시킵니다.

  • VOLUME: 외부 저장장치에 연결될 수 있는 폴더를 이미지 내부에 만듭니다.

  • CMD: 컨테이너를 실행시킬 때 실행시킬 명령어를 설정합니다.

    이 명령어를 모두 사용한 도커파일을 만들어 보겠습니다. 이 이미지는 특정 포트로부터 입력을 받아 그 정보를 파일로 출력합니다.

FROM ubuntu
RUN apt-get update && apt-get install -y netcat-openbsd
ENV LOG_FILE echo.out
COPY ./echoserver.sh /echoserver.sh
RUN chmod +x /echoserver.sh
EXPOSE 8082
VOLUME /server-logs
CMD /echoserver.sh

명령어의 순서는 매우 중요합니다.

echoserver.sh파일

#!/bin/bash

netcat -k -l  -p 8082 > /server-logs/$LOG_FILE

도커는 이미지를 빌드하며 아래의 명령을 실행시킵니다.

  • 로컬 캐시에 없다면 가장 최근의 우분투 이미지를 다운로드 받습니다.
  • 패키지 목록을 업데이트하고 netcat 패키지를 다운로드 받습니다.
  • LOG_FILE이라는 환경변수를 echo.out으로 설정합니다.
  • echoserver.sh라는 파일을 컨테이너의 루트 폴더에 복사합니다.
  • echoserver.sh 파일을 실행가능하도록 권한을 수정합니다.
  • 컨테이너의 8082포트를 노출시킵니다.
  • server-logs라는 파일시스템을 마운트 시킵니다.

이러한 명령은 컨테이너 내부에서 이미지를 빌드하며 수행됩니다. 로컬 머신(호스트)가 아닌 이미지 내부에 netcat 패키지와 LOG_FILE 환경변수가 설정되어 있을 것 입니다.

아래는 빌드할 때 명령어를 실행시킨 디렉토리의 파일목록과 빌드 명령어입니다.

Result of ll and docker image build --tag kuicsofficial/echoserver .

CMD명령어를 제외한 나머지 명령어는 이미지를 빌드하며 수행됩니다. 컨테이너를 실행시킬 때 도커는 CMD명령어에 지정된 명령을 실행시킵니다. 위의 이미지는 echoserver.sh라는 스크립트를 실행시킬 것 입니다. echoserver.sh는 netcat을 8082포트로 압력을 받도록 실행시키고 그 출력을 파일로 전달합니다. 파일이 저장되는 위치는 도커파일에 지정된 VOLUME과 환경변수를 이용합니다. docker container run --detach --publish 8082:8082 --name echo-server kuicsofficial/echoserver 명령어는 8082포트를 호스트와 연결하여 echoserver이미지를 실행시킵니다.

아래와 같이 명령어를 실행시키면 호스트에서 8082포트로 문자열을 보냅니다. Result of nc localhost 8082

윈도우에는 netcat이 없습니다. netcat에서 다운로드 받을 수 있습니다.

^C는 Ctrl-C입니다.

docker container exec명령어를 이용하면 컨테이너 내부에서 명령을 실행한 뒤 출력을 호스트에 보여줍니다. docker container exec echo-server cat /server-logs/echo.out명령어를 사용하여 올바르게 작동했는지 확인해보겠습니다.

Result of docker container exec echo-server cat /server-logs/echo.out

이 예시는 단순한 형태의 도커파일의 예시입니다. 일반적인 도커파일은 베이스 이미지에 응용프로그램을 설치하는 등의 작업을 일을 합니다. 예를 들어 사용하려는 플랫폼이 공식 이미지가 있다면 그 이미지를 기초 이미지로 활용하여 자신이 원하는 작업을 통해 원하는 이미지를 만들어낼 수 있습니다.

3. 도커가 이미지를 빌드하는 방식

도커는 이미지를 빌드하기 위해 레이어화된 파일시스템을 사용합니다. 기초 이미지를 이용해서 도커는 각각의 명령어를 실행시킬 임시 컨테이너를 실행시키고 명령어를 실행시킨 후에 임시 컨테이너들 새로운 레이어로 저장하고, 이를 로컬 캐시에 저장합니다. 도커는 빌드하며 캐시를 사용하므로 현재 명령어에 적절한 레이어를 찾아 재사용할 수 있습니다.

도커파일이 정확히 만들어져있고, 다른 도커파일이 비슷한 구조를 갖는다면 최대한 캐시된 레이어를 많이 활용할 수 있습니다. 이상적으로는 비슷한 의존성을 가진 여러 도커파일을 빌드한다면 정확히 그 이미지에서만 필요한 레이어만을 새로 생성하고 명령어를 실행시킬 수 있습니다. 예를 들어 아래의 두 도커파일은 마지막 COPY명령어를 제외하고 완전히 같습니다.

도커파일 A

FROM ubuntu
RUN touch /setup.txt
RUN echo init > /setup.txt
COPY file.txt /a.txt

도커파일 B

FROM ubuntu
RUN touch /setup.txt
RUN echo init > /setup.txt
COPY file.txt /b.txt

첫번째 도커파일을 빌드하며 도커는 RUNCOPY를 할 레이어를 만듭니다. docker image build -t kuicsofficial/a -f a.dockerfile . 명령어는 a.dockerfile이라는 도커파일을 사용하겠다고 지정하여 도커파일을 빌드합니다.

file.txt라는 이름의 적당한 파일을 빌드하는 디렉토리에 생성해야 오류없이 동작할 것 입니다.

Result of docker image build -t kuicsofficial/a -f a.dockefile .

이 결과에는 몇가지 주목할만한 점이 있습니다. Step 1을 실행할 때 우리가 이전에 이미 우분투 이미지를 사용한 적이 있었기 때문에 도커는 캐시에 있는 이미지를 사용합니다. 따라서 도커는 사용할 이미지의 ID만 보여줍니다.

하지만, Step 2를 실행할 때는 이전에 실행한 적이 없기 때문에 도커는 임시 컨테이너를 만들어 실행시킵니다. 도커는 이 컨테이너를 실행시킨 이후에 7ab로 시작하는 ID를 가진 레이어로 저장합니다. 마찬가지로 Step 3와 Step 4 역시 캐시에 적절한 이미지가 없으므로 도커는 임시 컨테이너를 만든 뒤에 그 레이어를 저장합니다.

docker image history명령어는 이미지의 모든 레이어를 보여줍니다. 아래는 kuicsofficial/a이미지의 레이어 기록입니다.

Result of docker image history kuicsofficial/a

이 결과에는 많은 정보가 포함되어 있습니다. missing이라고 써있는 레이어는 캐시에 중간 레이어가 남아있지 않음을 의미합니다. 여기에 있는 missing레이어는 우분투 이미지에 포함되어 있는 동작을 수행하는 레이어이기 때문에 로컬 캐시에 저장되어 있지 않습니다. 도커는 이미지를 도커 허브에서 다운로드 받아 사용합니다. 따라서 모든 레이어가 로컬 캐시에 존재하지는 않습니다. 하지만 일부 기록은 볼 수 있습니다(여기서는 2ca로 시작하는 것입니다.). 그 위의 세 레이어는 우리가 도커파일에 지정한 동작입니다.

그럼 두번째 도커파일을 빌드해보겠습니다. 이제 로컬 캐시에 적절한 레이어가 있으므로 도커는 이 레이어를 사용할 것 입니다.

Result of docker image build -t kuicsofficial/b -f b.dockerfile .

도커가 이미지를 빌드할 때 COPY명령어를 수행하기 위한 중간 레이어를 새로 만들어 사용하는 것을 볼 수 있습니다. file.txt를 b.txt로 복사하는 명령어는 이전에 실행된 적이 없기 때문입니다. 나머지 명령어는 로컬 캐시에 저장된 것을 재사용할 수 있습니다. kuicsofficial/b이미지의 레이어를 보면 아래 7개의 레이어를 ID가 위와 같고, 마지막 레이어의 ID만 다른 것을 볼 수 있습니다.

Result of docker image history kuicsofficial/b

이러한 이미지 캐시는 빌드 타임을 매우 많이 단축할 수 있게 해줍니다. 다만 이미지 캐시를 사용할 때 모든 명령어가 빌드될 때의 상태로 이미지 캐시에 캐시된다는 것을 기억해야 합니다.

도커는 로컬 캐시에서 적절한 레이어를 찾으므로 나중에는 최신상태를 유지하지 못하게 될 수도 있습니다. 예를 들어 apt-get update와 같은 명령어는 실행될 때 가장 최신의 패키지 목록을 가져오지만, 나중에 빌드할 때는 최신의 패키지 목록이 아니게 됩니다.

이러한 문제를 방지하기 위한 방법이 Best practices for writing Dockerfile에 나와있습니다. 여기에 제시되어 있는 의도치않은 캐시된 이미지 사용을 막을 수 있는 방법 중 하나는 여러 명령어를 RUN명령어 하나에 묶어서 사용하는 것입니다.


이 내용은 Docker_Succintly 책을 기반으로 쓰여졌습니다. 이 책은 Syncfusion 에서 다운로드 받을 수 있습니다.