IT/기초 지식

[Docker] Docker 이미지 튜토리얼

개발자 두더지 2021. 5. 10. 15:44
728x90

개요-스스로 hello-world 이미지를 빌드


 이 튜토리얼은 "hello-world" 이미지를 스스로 빌드하는 예를 통해 이미지에 대한 이해를 깊게 하기 위함이다. 또한 효율적인 Dcoker 이미지 작성이나 Dockerfile의 활용을 목표로 기초 이외에어 멀티 스테이지, 빌드도 학습할 것이다.

 포인트는 Docker이미지(image)란, Docker컨테이너의 실행에 필요한 일종의 패키지(파일이나 메타 정보의 집합체)이다. 다만 가상 머신 이미지와 같이 1개의 파일이 아니다. 

 그리고 Docker이미지를 구성하는 것은 추상적인 이미지 레이어(image layer)의 집합이다. 레이어란 "층"의 의미로, Docker은 여러 개의 레이어 상의 파일 시스템을 하나로 다룰 수 있게 한다. 일반적인 Docker 이미지는 여러 개의 이미지 레이어로 구성된다. 또한, 이미지 레이어는 읽기 전용이며, 레이어간은 부모자식 의존관계를 가지고 있다.

 보통, Docker 이미지를 자동구축할 수 있도록 설정하는 설정 파일을 Dockerfile이라고 부르며, 이 파일 내에 이미지를 구성하는 명령을 작성한다. 이 명령은 하나 하나이지만, 개념상의 이미지 레이어에 상응한다.

 또한, 여기서 실제로 이미지 작성하고 실행해 볼 환경으로는 Docker EC 19.03, 호스트 OS는 Linux(amd64환경), 그리고 스토리지 드라이버는 기본(overlayer2)을 상정하고 있다.

 

 

튜토리얼


1. Docker 이미지와 이미지 레이어

Docker 이미지란 부모관계를 가지는 여러 개의 이미지 레이어로 구성되어 있다. 이미지 레이어는 읽기 전용이다. Docker은 여러 개의 이미지 레이어를 포함한 파일이나 디렉토리의 정보를 1개로 통합하는 기술을 사용하고 있다.

 이 이미지 레이어 중에서는 Docker 컨테이너의 실행에 필요한 Linux파일 시스템과 메타 정보를 포함한다. Linux 파일 시스템이란 것은 / 디렉토리 아래의 /ect /bin /sbin /usr 등의 디렉토리 계층 밒 파일이다.

 Docker에서는 컨테이너로써 움직이고 싶은 어플리케이션이 필요로 하는, 최소한의 파일을 Docker 이미지 안에 넣는다(정확히는 이미지 레이어 내에 파일 시스템을 넣는다).

 또한 하나 하나의 이미지 레이어에는 부모 관계가 설정되어 있다. 보다 상위인 이미지 레이어부터는 부모가 되는 이미지 레이어상의 파일 시스템도 참고할 수 있다. 즉, Docker 이미지를 다운로드하면, 그 이미지가 여러 개의 이미지 레이어로 구성되어 있어도 그것을 의식하지 않고 이용할 수 있다.

 더욱이 이 어플리케이션을 움직이기 위해서 필요한 기본 커맨드나 인수를 지정, 외부에 공개하는 포트 번호의 정보, 볼륨 영역 등의 정보가 있다. 이러한 것을 메타 정보로써, 같은 Docker 이미지 레이어 안에 넣을 수 있다.

 이와 같이 Docker 이미지에는 "이미지"이라는 명칭이 붙어 있지만, 가상 머신용의 디스크 이미지이거나, OS템플릿을 의미하는 이미지와 완전히 용법이나 개념이 다르므로 주의하자.

 보통, 어떠한 Docker 이미지를 가리킬 때에는 그 이미지의 최상위에 위치한 이미지 레이어를 일컫는다(기본으로는 latest 이라는 태그가 붙은 이미지 레이어). 이 이미지 레이어에 부모관계를 가지는 레이어가 있다면, 이미지의 취득 등, 자동적으로 모아서 다운로드하거나, 업로드할 수 있다.

 이제부터는 커맨드를 실행하면서 이미지와 이미지 레이어에 대해서 확인해보자.

 

2. hello-wolrd 이미지의 다운로드(pull)

Docker 이미지를 사용하기 위해서는, Docker Hub등으로부터 docker pull 커맨드로 다운로드하거나 docker build 커맨드를 사용해서 스스로 생성(빌드)한다.

 Docker Hub란, 공식 Docker 이미지를 포함해, 다양한 Docker 이미지가 공개, 공유되어 있거나, 공동 작업(콜라보레이션)하기 위한 장소이다(누구도 이용할 수 있으므로, Docker 이미지의 "공개 레지스트리"라고 부른다). 

 이 Docker Hub으로 부터 hello-world이라는 이름의 Docker 이미지를 다운로드한다. 이 이미지는 C언어로 작성된 hello이라는 이름의 설명용문자를 표시하는 내용뿐인 바이너리가 들어 있다. 

 다운로드하기 위해서 docker pull hello-world 를 실행해보자.

$ docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest

 화면에 표시된 메시지 내용을 위에서 부터 순서대로 살펴보자

● Using default tag : latest

- Docker 이미지에는 "태그"이라는 개념이 있다. 주로 버전을 표기하기 위해 이용되는 경우가 많다. 1개의 이미지에 대해서 여러 개의 태그를 할당하는 것도 가능하다.

- docker pull <이미지명>:<tag명> 이 올바른 형식이지만, 태그명을 생략하면 자동으로 latest의 태그가 적용된다.

- 즉 docker pull hello-wolrd는 docker-pull hello-world:latest와 동일하다.

 latest : Pulling from libray/hello-world

- hello-world 앞에 library/이라는 이름공간(디렉토리명)이 자동적으로 부여되어 있다. 이 library는 Docker 공식 이미지 전용의 이름 공간이다.

- docker pull 등을 실행할 때, 이름 공간을 지정하지 않으면, 기본적으로 공식 이미지(library)의 이미지를 취득한다.

0e03bdcc26d7: Pull complete

- Docker Hub상에 있는 이미지 레이어 0e03bdcc26d7의 다운로드가 완료되었다는 의미이다.

- hello-world는 하나의 레이어이지만, 이미지에 따라 여러 레이어가 구성되어 있는 경우가 있다. 

Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9

- 이 hello-world이미지 (정확히는 hello-world:latest의 태그를 가지는 이미지 레이어)의 배시값이다.

- 중간 부분이 같다면, 태그가 변경되도, 이 배시값은 변하지 않는다.

- 다운로드는 태그 지정뿐만 아니라, docker pull sha256:배시값을 지정 할 수 도있다.

Status: Downloaded newer image for hello-world:latest

- hello-world:latest의 최신 이미지의 다운로드가 완료되었다는 상태를 표시하고 있다.

docker.io/library/hello-world:latest

- 최종적으로 다운로드가 완료된 이미지 정보이다.

- docker.io는 레지스토리 Docker Hub, 이름공간(이미지를 저장하는 레포지토리)는 library(공식 이미지), 그 안의 hello-world이미지의 태그명 latest(최신)을 다운로드(pull)하였다는 의미이다.

 이와 같이, docker pull 커맨드를 실행하는 것으로, 다양한 처리가 실시되어 그 경과가 화면상에 표시되는 것을 알 수 있다.

 

3. hello-world 이미지를 상세히 살펴보자

다운로드한 hello-world:latest 이미지를 확인하자. 로컬에 다운로드가 끝난 이미지를 확인하기 위해서는 docker images를 실행한다.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              bf756fb1ae65        5 months ago        13.3kB

실행해보면, 이와 같은 이미지의 정보가 표시된다.

● REPOSITORY

 리포지토리 이름이다. 여기서는 hello-world가 되겠다 (리포지토리먕은 사실상 이미지명이라고 생각해도 상관없지만, 정확히 보관장소로써의 리포지터리로, 그 안에 "이미지명:태그" 라는 꼬리표나 스터키가 부착된 상자가 있는 것 처럼 상상하는 편이 더 옳다). 

TAG

 이미지에 붙여진 태그이다. 여기서는 latest이다.

IMAGE ID

 이미지가 가진 고유의 이미지 ID(64자리)이다. 여기서는 쇼트 ID(12행)의 정보가 표시된다.

● CREATED

 이 이미지가 언제 생성되었는가를 표시한다.

SIZE

 이 이미지의 실체로써 디스크상에 소비되고 있는 용량이다. 

다음은 docker inspect hello-wolrd:latest실행시에 표시되는 이미지의 상세 정보에 대해서 살펴보자.

$ docker inspect hello-world:latest
[
    {
        "Id": "sha256:bf756fb1ae65adf866bd8c456593cd24beb6a0a061dedf42b26a993176745f6b",
        "RepoTags": [
            "hello-world:latest"
        ],
        "RepoDigests": [
            "hello-world@sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9"
        ],
        "Parent": "",

여기서는 아래와 같은 정보를 알 수 있다.

● ID

이 이미지는 ID(64자리)이다.

RepoTags

이 이미지에 할당되어 있는 태그가 hello-world:latest라는 것을 알 수 있다.

RepoDigests

이 이미지 내용에 대한 배시값이다. 태그는 변경 가능하지만, 이 값은 내용이 변경되지 않는한 동일하다.

Parent

부모 이미지의 정보이다. "" 는 의존관계를 가진 부모 이미지가 없다는 것이다. 즉 hello-world:latest이미지는 이 1개의 이미지 레이어으로만 구성되어 있다는 것이다.

화면을 조금 더 스크롤해보면 "Cmd" 세션이 보인다. 이것은 컨테이너 실행시에 인수가 없으면, 컨테이너내에서 어떤 커맨드를 실행할지를 지정한다.

            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/hello\"]"
            ],

이 CDM["/hello"]이라는 기재로부터 이 컨테이너 살행하면 컨테이너내의 경로 /hello를 실행하는 것을 알 수 있다.

(참고로, "Cmd"를 포함한 ContainerConfig 세션은 컨테이너의 내용을 이미지에 커밋할 때의 정보이다. "Cmd" 섹션에 /bin/sh 의 기재가 있지만, 어디까지나 이미지의 커밋(작성시)의 내용적이나 부분이므로, CMD명령으로 /bin/sh를 실행하는 의도는 없다 )

 화면의 마지막 부분까지 스크롤해보면, 다음과 같은 섹션이 보인다.

        "GraphDriver": {
            "Data": {
                "MergedDir": "/var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/merged",
                "UpperDir": "/var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/diff",
                "WorkDir": "/var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/work"
            },
            "Name": "overlay2"
        },

UpperDir이라고 적혀있는 경로가 이 Docker을 실행하고 있는 호스트상에 hello-world이미지의 실체를 보존하고 있는 디렉토리이다. 

이 화면상에서는 94f...로 시작하는 문자열이지만, 환경에 따라 랜덤한 문자열로 변경된다.

 그렇다면, 컨테이너 안을 ls -l <디렉토리명> 커맨드로 조사해보자. 디렉토리명은 변수에 따라 다르므로 주의할 필요가 있다. 또한 커맨드의 실행에는 root권한이 필요하다. 따라서 이 경우 sudo ls -l ... 로 실행하자. 

# ls -al /var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/diff
합계 24
drwxr-xr-x 2 root root  4096  6월 13 12:39 .
drwx------ 3 root root  4096  6월 13 12:39 ..
-rwxrwxr-x 1 root root 13336  1월  3 10:21 hello

 hello이라는 이름의 파일이 보인다. 이것으로 부터, hello-world:latest이미지의 파일 시스템은 hello이라는 바이너리뿐이라는 것을 알 수 있다.

(참고로, 이번의 예에서는 1개의 파일만 존재하지만, 예를 들어 ubuntu나 centos등 Linux디스트리뷰션의 이미지를 다운로드하면, 각 이미지용의 디렉토리내에는 ./bin/ ./sbin/ ./var/ 등 각 디스트리뷰션용의 / 이하 파일 시스템이 전개된다. 그리고 그러한 곳에서 Docker 컨테이너를 실행하면, 호스트상과 컨테이너 내에 다른 Linux 디스트리뷰션가 동작하고 있는 것 처럼 "보이지만", 실제로는 호스트상의 Linux Kernel상에 Docker은 지정한 Docker 이미지의 디스트리뷰션, 예를 덜어 centos인 경우 centos의 파일 시스템을 마운트하고, 디폴트로는 그 중에 포함된 /bin/bash 를 PID 1로 하는 이름 공간내에 실행하고 있다. 컨테이너 실행시, 1개의 Linux상에 여러 개의 Linux가 동작하고 있는 것은 아니다)

 이번에 실행한 hello-world는 이미지가 1개 밖에 없으므로, 호스트상에는 이 1개의 디렉토리내에 hello-world Docker 이미지의 내용물 전체가 포함된다. 따라서, 여러 개의 이미지 레이어로 구성되어 있는 Docker 이미지가 있다면, 호스트상에 여러 개의 디렉토리가 존재할 것이다.

 더욱이 이미지에는 메타 정보가 포함되어 있다. hello-world에서는 CMD 명령으로 /hello를 실행하는 명령이 아니다. 이 메타 정보에도 이미지 정보가 필요하다 (또한, 메타 정보에는 개념적 이미지 레이어가 있으므로, 호스트 상에서는 실체로써의 파일이나 디렉토리가 없다).

 이렇게 하여, Docker 엔진은 Docker의 이미지 레이어를 추상적인 Docker 이미지라는 단위로 다뤄지고 있다. Docker 컨테이너 실행시, 호스트상에는 이곳 저곳 흩어져 있는 파일이나 디렉토리를 1개의 파일 시스템으로 조작하여 가능하도록 하고 있다.

 여기까지 Docker이미지는 호스트상에 1개의 실체로써 파일이 존재하지 않는다는 것을 알 수 있다.

 그렇다면, 이제 이 hello를 컨테이너가 아닌, 실행해보자. 파일경로로는 /var/lib/docker/overlay2/<디렉토리명>/diff/hello을 실행한다. 

# /var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/diff/hello

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

 실행해보면 "Hello from Docker!" 이후에 문자열이 표시된다. 이 문자열은 소스 코드 hello.c에 적혀 있는 대로이다.

 지금까지의 hello-world:latest 이미지에 대한 내용을 정리하자면, 이 hello이라는 바이너리를 실행하는 것을 알 수 있다. 그리고 이 바이너리는 amd64/x86_64용으로 컴파일되어있으므로, Linux상에 그대로 실행될 수 있음을 확인하였다.

(또한, CPU amd64/x86_64용으로 컴파일한 바이너리는 실행할 수 있지만, 다른 CPU 구조용의 바이너리는 amd64/x86_64에서 실행할 수 없다. 예를 들어 Raspberry Pi는 ARM이라는 구조이므로, 이 hello 바이너리는 라즈베리 파이에서 실행되지 않는다. 바이너리로써 실행되지 않는 이상,  Docker 이미지에 넣으려고 해도 다음 섹션에서 설명하겠지만 Docker 컨테이너로 실행할 수 없다. Docker은 하드웨어 emulation을 실행하지 않으면 안되고, 하드웨어를 가상화하는 것과 같은 기술도 아니다 )

 

4. hello-world Docker컨테이너의 실행

다음은 hello-world 이미지와 달리, Docker 컨테이너(아래, 컨테이너와 생략)을 실행한다. 실행 전 컨테이너란 무엇인가에 대해 간단히 복습하자.  한마디로 간단하게 말하자면, 특별한 상태에서 Linux의 프로세스를 실행하는 것이 컨테이너이다. Docker은 Linux 커넬이 가진 이름 공간(namespace)의 분리 기술이나 cgroup에 의한 리소스 제한, 그 외에 Docker Engine의 구현에 의해, Docker이미지내에 있는 파일 시스템 내에 프로그램을 특별한 상태로써 실행한다.

즉, hello-world를 Docker컨테이너에서 실행시키면

- hello-world 이라는 이름의 Docker이미지를 준비한다.

- hello-world의 이미지안에 있는, 어떠한 프로그램을 실행한다.

- 이미지내의 정의(CMD명령)에서는 /hello을 자동적으로 실행한다.

- 그러나, /hello를 컨테이너로써(이름공간 등의 제약을 받아) 실행

이상의 동작으로 실행된다.

또한, 컨테이너는 컨테이너용의 읽기를 할 수 있는 Docker 이미지 레이어가 자동으로 작성된다. 이 이미지 레이어는 보통의 이미지용과 동일하게, 부모관계를 가지고 있다. 따라서, 동일한 호스트상에 여러 개의 컨테이너를 실행해도, 원래 존재하는 Docker이미지 이상의 용량을 필요하지 않는다는 장점이 존재한다.

그럼 hello-world컨테이너를 실행해보자. docker run hello-world를 실행하자.

$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.
...생략...
For more examples and ideas, visit:
 https://docs.docker.com/get-started/

이와 같이, 직접 hello의 바이너를 실행한 것과 동일한 처리(문자열의 표시)을 하는 것을 알 수 있다.

중요한 것은 이 hello 바이너리는 호스트상에 존재하고 있다는 것이다. 아까 봤던, Docker이미지의 실체로써의 hello가 위치한 경로가 있다. 그러나, 컨테이너로써 실행하고 있으므로, hello는 PID 이름공간에 분리되어 있는 hello만 존재하는 프로세스 공간이다. 더욱이 mount이름공간의 분리에 의해 hello가 존재하는 디렉토리가 컨테이너를 실행하는 프로세스 공간에서 /로 마운트한다.

 즉, /hello만 존재하고 있는 파일 시스템상, Linux의 유저 공간내에 hello의 프로세스가 실행되지 않는 것처럼 보이는 특별한 상태가 존재하고 있다. 이것이 Docker컨테이너이다. 그리고 컨테이너는 이름 공간내에 PID1로써 hello를 실행한다. hello가 화면에 문자열을 출력한 뒤에 exit 상태가 되어 컨테이거 그 자체가 실행 종료(exited)가 된다.

이와 같이,

- Docker 이미지를 준비한다.

- Docker컨테이너용의 이름공간(PID, mount, .... 등)을 분리(isolate)한 환경을 작성한다.

- Docker은 Docker이미지의 안에 어떤 파일(바이너리 등의 프로그램)을 그 이름공간에서 실행한다.

- 실행한 프로세스가 처리완료되면, Docker컨테이너도 종료(정지)된다.

이 컨테이너 실행으로부터 종료까지의 흐름을, Docker의 라이프사이클이라고 부른다.

컨테이너의 상태를 알아보기 위해서는 docker ps에 -a (all) 의 옵션을 붙이면 된다.

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
e8c0bab26796        hello-world         "/hello"            4 minutes ago       Exited (0) 4 minutes ago                       frosty_bhabha

- CONTAINER ID : 컨테이너에 대해서 랜덤으로 할당되어있는 64문자의 컨테이너 ID이다. 이 컨테이너 ID 혹은 컨테이너로 컨테이너를 조작한다.

- IMAGE : 컨테이너 실행원이되는 이미지명이다.

- COMMAND : 컨테이너내에 실행하고 있는 코맨드이다.

- CREATED : 컨테이너가 언제 생성되었는가를 나타낸다.

- STATUS : 컨테이너의 상태

- PORT : 컨테이너가 포트에 공개되어 있는 경우에, 여기에 표시가 나타난다.

- NAMES : 실행시에 지정하지 않으면 자동적으로 랜덤한 문자열이 된다.

 이 hello-world컨테이너는 이 컨테이너 전용 이름 공간내에서 /hello프로그램을 실행하고, 종료했다. docker ps에서 표시되어있는 것은 컨테이너라기보다는 컨테이너용의 이미지 레이어를 목록을 표시하고 있다고 생각하는 편이 맞다고 할 수 있다.

 hello-world컨테이너는 굉장히 심플하므로, 이 외의 컨테이너 내부관련 조작을 하지 않을 것이다. Linux 디스트리뷰션에 포함되어 있는 컨테이너나 쉘이, 이 이미지 안에 포함되어 있지 않기 때문이다.

 마지막으로 이 컨테이너를 삭제해보자. docker rm <컨테이너ID 혹은 컨테이너명>을 실행한다.

$ docker rm e8
e8
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

 참고로, 이와 같이 컨테이너 ID는 전방 일치로 지정할 수 있다. 긴 컨테이너 ID는 복사하지 않아도, 보통은 2행 혹은 3행의 지정하여도 조작이 가능하므로 편리하다. 그리고 삭제 후 docker ps -a를 실행하면 컨테이너(용의 이미지 레이어)가 남아있지 않다는 것을 알 수 있다.

 

5. 개인용 hello-world Docker 이미지를 Dockerfile로 빌드

다음은 스스로 Docker이미지를 구축(빌드)해보자. 이미지를 생성하기 위해서는 docker build커맨드를 사용한다. 이 때에 -t로 만든 이미지명(과 태그)과, Dockerfile이라는 이미지 구축 명령을 기재한 파일의 경로("컨텍스트"라고 부른다)를 지정한다.

일반적으로 Dockerfile으로 빌드하는 흐름은 아래와 같은 이미지와 같다.

여기서 부터는 개인 전용의 hello-world 이미지를 생성하고 있다.

 먼저, 작업용 디렉토리를 작성한 후, 이동한다. 여기서는 myhello이라는 이름으로 한다. 

$ mkdir myhello
$ cd myhello

 또한, 아까 발견한 호스트상의 hello파일을 이 디렉토리에 복사한다. 아래 ...의 부분은 본인의 환경에 맞춰 바꿔 작성하면 된다.

cp /var/lib/docker/overlay2/......./hello .

 먼저, 커맨드라인에 다음의 커맨드를 실행한다. 어떠한 결과가 되는가?

docker build -t myhello -<<EOF
FROM scratch
EOF

FROM scratch가 Dockerfile내에 적용되는 명령의 하나이다. 보통 여기서는 원본이 되는 Docker이미지를 지정할 수 있다.  scratch를 지정하면 아무것도 아닌 비어있는 듯한 이미지를 생성하는 명령이다 (초기의 Docker은 Docker Hub상에 있던 scratch 이미지로 /만이 존재하는 것을 다운로드하였지만, 현재의 scratch 이미지 지정은 실체가 없는 특별한 지정이다 ).

 그리고 여기서 Dockerfile가 종료되어 있기 때문에, 실행해도 다음과 같이 무엇도 내부에 없다는 표시가 나온다.

$ docker build -t myhello -<<EOF
> FROM scratch
> EOF
Sending build context to Docker daemon  2.048kB
Step 1/1 : FROM scratch
 --->
No image was generated. Is your Dockerfile empty?

 다음은 Dockerfile이라는 명칭의 파일을 동일한 디렉토리 내에 생성해보자. 다시 COPY 명령을 사용해서 hello를 컨테이너내에 위치시키자. COPY ./hello / 로 호스트상의 ./hello를 컨테이너 안의 /에 복사하는 명령이다. 

cat << EOF > Dockerfile
FROM scratch
COPY hello /
EOF

Dockerfile의 안을 확인해보자.

$ cat Dockerfile
FROM scratch
COPY hello /

이미지를 빌드하자. -t 로 태그는 myhello로 한다.

$ docker build -t myhello .
Sending build context to Docker daemon  16.38kB
Step 1/2 : FROM scratch
 --->
Step 2/2 : COPY hello /
 ---> Using cache
 ---> d1a7687418b6
Successfully built d1a7687418b6
Successfully tagged myhello:latest

이것으로 myhello 이미지가 생성되었다. 이미지 목록을 확인해보자.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
myhello             latest              d1a7687418b6        4 minutes ago       13.3kB
hello-world         latest              bf756fb1ae65        5 months ago        13.3kB

이제 이 이미지로 컨테이너를 실행해보자.

$ docker run --rm myhello
docker: Error response from daemon: No command specified.
See 'docker run --help'.

그런데 에러가 발생한다. 왜 에러가 발생하는 것일까?

 No command specified 문구가 있다. myhello이라는 명칭의 이미지가 생성되어 컨테이너로써 실행할 후 있는 준비가 되어있다. 하지만, 이것은 컨테이너로써 어떤 것을 실행할 것인지 지정하지 않으면, 이러한 에러가 출력된다.

 다음은 컨테이너 내에 /hello를 실행하도록, docker run --rm myhello /hello 커맨드를 입력하자.

$ docker run --rm myhello /hello

Hello from Docker!
This message shows that your installation appears to be working correctly.
...생략...

 이번에는 정상적으로 실행되었다!

 그러나, 매번 커맨드를 지정하는 것은 귀찮다. 디폴트로 /hello 를 실행하도록 CMD 명령을 추가한 Dockerfile를 준비하자.

cat << EOF > Dockerfile
FROM scratch
COPY hello /
CMD ["/hello"]
EOF

 다시 Dockerfile을 확인하자.

$ cat Dockerfile
FROM scratch
COPY hello /
CMD ["/hello"]

 이 상태에서, 이미지 레이어는 개념적으로 아래와 같이 중첩되어 있다.

 여기서 다시 build한다. 이번에는 v2이라는 태그를 붙인다.

$ docker build -t myhello:v2 .
Sending build context to Docker daemon  16.38kB
Step 1/3 : FROM scratch
 --->
Step 2/3 : COPY hello /
 ---> Using cache
 ---> d1a7687418b6
Step 3/3 : CMD ["/hello"]
 ---> Running in 98bb18184d82
Removing intermediate container 98bb18184d82
 ---> 5e11c5479c11
Successfully built 5e11c5479c11
Successfully tagged myhello:v2

[보충 설명1] Using cache이란, Docker가 이미지를 빌드할 때에, 기존의 Dockerfile에 적혀있는 이미지 내용과 일치하는 경우에, 기본으로 캐시를 이용한다 (build시에 --no-cache 옵션으로 캐시를 사용하지 않도록 지정할 수 있다).

[보충 설명2] Running in 98bb18184d82 이란 빌드중 이 CMD ["/hello"]를 쓰기위한 컨테이너 (중간 컨테이너라고 부르겠다)를 자동으로 실행한다. 그리고 이 컨테이너용의 이미지 레이어에 적혀진 내용을 이미지 레이어로 변환하는 처리, 커밋(docker commit)을 실행하고, 중간 컨테이너가 자동삭제 Removing intermediate container 98bb18184d82 돼있다.

[보충설명3] 또한 docker commit으로 이미지 레이어를 커밋(변환) 하는 것은 파일 시스템과 메타 정보뿐이다. 컨테이너 실행시의 로그(Docker용어의 로그란, 커맨드등을 실행한 표준 출력을 의미한다)는 레이어와 다른 장소에 저장되어, 로그의 정보는 커밋되지 않는다.

 아래의 커맨드으로 생성된 myhello:v2 이미지의 컨테이너를 실행한다.

$ docker run --rm myhello:v2

Hello from Docker!
This message shows that your installation appears to be working correctly.
...생략...

 이렇게 아까와 달리, 자동적으로 /hello를 실행하는 이미지를 작성할 수 있다.

 또한, docker images 커맨드를 실행하면, 신버전과 구버전 2개의 버전의 myhello가 동일 호스트상에 함께 존재할 수 있다는 것을 알수 있다.

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
myhello             v2                  5e11c5479c11        About a minute ago   13.3kB
myhello             latest              d1a7687418b6        14 minutes ago       13.3kB

 여기서 docker history myhello:latestdocker history myhello:v2를 실행해, 비교해보자.

$ docker history myhello:latest
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
d1a7687418b6        15 minutes ago      /bin/sh -c #(nop) COPY file:801a928f8ba2b08b…   13.3kB
$ docker history myhello:v2
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
5e11c5479c11        2 minutes ago       /bin/sh -c #(nop)  CMD ["/hello"]               0B          
d1a7687418b6        15 minutes ago      /bin/sh -c #(nop) COPY file:801a928f8ba2b08b…   13.3kB     

 이와 같이, 이미지(레이어를 표시) ID d1a7687418b6의 부분이 중복되어 있다는 것을 알 수 있다. 흘긋 보면 2개의 이미지가 따로 따로 존재하는 것처럼 보이지만, latest와 v2의 차이로는 5e11c5479c11 (CMD ["/hello"]의 명령 유무) 와 d1a7687418b6 (hello 를 복사한 이미지 레이어)가 공유되고 있다는 것을 알 수 있다.

 이와 같이 이미지가 부모관계를 가지고 공유하는 이미지 레이어는 이미지간에 공유할 수 있다는 것을 알 수 있다. 복수의 파생 버전이 있는 경우에도 잘 사용한다면 호스트상에 용량을 그다지 소비하지 않고 이용할 수 있다.

 

6. my-hello-world 이미지를 소스 코드로부터 빌드하기 (개발자용)

아래의 경우는 일본어 환경을 상정하고 작성한 것이다.

공식 이미지 hello-world에 포함되어 있는 바이너리 hello는 C언어로 작성되어 있다. Github의 소스 코드를 참고해, 일본어로 메시지가 표시하는 my-hello-world를 작성해보자.

이번에는 호스트상에 C 언어의 개발환경을 구축하는 것이 아닌, Docker이미지내에 GCC와 소스코드를 넣어, 컴파일한다. 그리고 성과물의 바이너리 파일만을 컨테이너내에 저장하고, 그것을 실행하기 위한 Docker 이미지를 구축한다. 이러한 일련의 흐름으로 멀티 스테이지 빌드를 사용한다. 

 먼저, 작업용 디렉토리를 생성하고 이동한다.

$ mkdir my-hello-world
$ cd my-hello-world

 다음은 아래와 같이 hello.c를 에디터등으로 작성한다.

#include <sys/syscall.h>
#include <unistd.h>

#ifndef DOCKER_IMAGE
        #define DOCKER_IMAGE "my-hello-world"
#endif

#ifndef DOCKER_GREETING
        #define DOCKER_GREETING "Dockerから、こんにちは!"
#endif

#ifndef DOCKER_ARCH
        #define DOCKER_ARCH "amd64"
#endif

const char message[] =
        "\n"
        DOCKER_GREETING "\n"
        "このメッセージが表示されていれば、インストールは正常終了しました。\n"
       (이 메시지가 표시되어 있다면, 인스톨은 정상으로 종료되었음을 의미한다.)
        "\n"
        "メッセージを表示するために、Dockerは以下の手順を処理しました:\n"
        (메시지를 표시하기 위해서 Docker은 아래의 순서대로 처리한다.)
        " 1. DockerクライアントはDockerデーモンに接続。\n"
        (1. Docker클라이언트는 Docker데몬에 접속한다.)
        " 2. DockerデーモンはDocker Hubから\"" DOCKER_IMAGE "\" イメージをダウンロード。\n"
        (2. Docker데몬은 Docker Hub로부터 \"" DOCKER_IMAGE "\" 이미지를 다운로드한다.)
        "    (" DOCKER_ARCH ")\n"
        " 3. Dockerデーモンはダウンロードしたイメージから、実行可能な新しいコンテナを作成し、\n"
        "    今あなたが読んでいるこのメッセージを表示します。\n"
        (3. Docker 데몬은 다운로드한 이미지로부터 실행가능한 새로운 컨테이너를 생성하고,
         지금 당신이 읽고 있는 이 이미지를 표시한다.)
        " 4. Dockerデーモンは出力結果をDockerクライアントに流し、あなたのターミナルに出力します。\n"
        "\n"
        "さらにチャレンジするには、Ubuntu コンテナを次のコマンドで動かしましょう:\n"
        (4. 더 챌린지하기 위해서는 ubuntu컨테이너를 다음의 커맨트로 실행해보자.)
        " $ docker run -it ubuntu bash\n"
        "\n"
        "イメージの共有、自動ワークフローなどの機能は、フリーなDocker IDで行えます:\n"
        (이미지의 공유, 자동 워크 플로우 등의 기능은 프리 Docker ID로 실행할 수 있다.)
        " https://hub.docker.com/\n"
        "\n"
        "更なる例や考え方は、ドキュメントをご覧ください:\n"
        (다른 예나 작성법을 참고하고 싶은 경우, 아래의 문서를 확인하길 바란다.)
        " https://docs.docker.com/get-started/\n"
        "\n";

int main() {
        //write(1, message, sizeof(message) - 1);
        syscall(SYS_write, STDOUT_FILENO, message, sizeof(message) - 1);

        //_exit(0);
        //syscall(SYS_exit, 0);
        return 0;
}

(괄호부분은 번역한 부분입니다.)

지금부터는 아래의 Dockerfile을 작성한다.

FROM alpine:latest AS build
RUN apk -U add gcc libc-dev
COPY hello.c /
RUN gcc -static -o hello hello.c

FROM scratch AS release
COPY --from=build /hello /
CMD ["/hello"]

 이것은 멀티 스테이지 빌드 기능을 사용하고 있다. build 스테이지에서는 alpine:latest의 Alpine Linux 환경 위에 C언어의 컴파일 환경을 만들어, hello.c를 컴파일한다.

 다음은 release 스테이지로, build스테이지로 컴파일한 hello를 복사한다. 그리고 CMD 명령으로 컨테이너 실행시에 이 hello 바이너리를 실행하도록 지정한다.

 이 멀티 스테이지 빌드를 사용한 최종 성과물 my-hello-world이미지에는 /hello 바이너리만 존재하는 최소한의 Docker이미지가 생성된다.

 그다음 실제로 빌드해보자.

$ docker build -t my-hello-world:latest .
...생략...
Step 7/7 : CMD ["/hello"]
 ---> Running in d4a997c55ad7
Removing intermediate container d4a997c55ad7
 ---> 248e4fe57d26
Successfully built 248e4fe57d26
Successfully tagged my-hello-world:latest

 docker images 커맨드로 이미지가 생성된 것을 알 수 있다.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
my-hello-world      latest              248e4fe57d26        35 seconds ago      51.2kB
<none>              <none>              aaf8ce6a03ec        36 seconds ago      146MB

 여기서 <none>이라는 이름의 이미지가 있다. 이것은 build 스테이지의 빌드 과정에서 작성된 중간이미지이다.

 주목해야할 것은 2개의 이미지 용량 차이이다. 중간 이미지의 용량(SIZE)는 146MB인 반면에, my-hello-world는 51.2kB밖에 되지 않는다. 멀티 스테이지 빌드를 활용하면 실제 이용에 필요한 이미지의 용량을 최저로 다룰 수 있다.

 여기서 다시 <none> 이미지의 상세히 살펴보자. docker history <이미지ID> 를 실행하면 이미지의 생성 과정을 알 수 있다. 이 예에서는 aaf8ce6a03ec가 이미지ID이지만, 유저의 환경에 따라 다를 수 있다는 것을 주의하자.

$ docker history aaf8ce6a03ec
IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
aaf8ce6a03ec        About a minute ago   /bin/sh -c gcc -static -o hello hello.c         51.2kB     
d31995be0993        About a minute ago   /bin/sh -c #(nop) COPY file:a4bbbb3d6b55d6c8…   1.88kB    
bd28c3199374        About a minute ago   /bin/sh -c apk -U add gcc libc-dev              140MB      
a24bb4013296        2 weeks ago          /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B         
<missing>           2 weeks ago          /bin/sh -c #(nop) ADD file:c92c248239f8c7b9b…   5.57MB

 또한, my-hello-world:latest 이미지의 docker history도 확인해보자.

$ docker history my-hello-world
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
248e4fe57d26        2 minutes ago       /bin/sh -c #(nop)  CMD ["/hello"]               0B          
8b646124473a        2 minutes ago       /bin/sh -c #(nop) COPY file:667e96ac9d2f55ef…   51.2kB     

 여기서는 scratch이라는 비어있는 듯한 Docker 이미지로부터 시작했기 때문에, build스테이지로부터 파일을 복사하는 COPY 명령을, CMD명령으로 /hello를 실행하는 지정만 되어있음을 알 수 있다.

 그 이후에, 기대한대로에 표시되는가를 확인해보자.

$ docker run my-hello-world

Dockerから、こんにちは!!
このメッセージが表示されていれば、インストールは正常終了しました。

メッセージを表示するために、Dockerは以下の手順を処理しました:
 1. DockerクライアントはDockerデーモンに接続。
 2. DockerデーモンはDocker Hubから"my-hello-world" イメージをダウンロード。
    (amd64)
 3. Dockerデーモンはダウンロードしたイメージから、実行可能な新しいコンテナを作成し、
    今あなたが読んでいるこのメッセージを表示します。
 4. Dockerデーモンは出力結果をDockerクライアントに流し、あなたのターミナルに出力します。

さらにチャレンジするには、Ubuntu コンテナを次のコマンドで動かしましょう:
 $ docker run -it ubuntu bash

イメージの共有、自動ワークフローなどの機能は、フリーなDocker IDで行えます:
 https://hub.docker.com/

更なる例や考え方は、ドキュメントをご覧ください:
 https://docs.docker.com/get-started/

 그리고, hello.c를 다시 변경하여 빌드하거나 조건을 바꿔서 해보거나 이런 저런 변경을 해보자.

 여기서는 C 언어를 예를 들어, 호스트상에 언어 개발 환경을 갖추지 않아도 Docker 컨테이너로 컴파일하거나, 실행할 수 있는 바이너리를 컨테이너화하는 것을 확인할 수 있었다.

 물론, C언어뿐만 아니라 임의의 언어로 다양한 버전이 혼재되어 있는 개발 환경에서도 활용할 수 있다. 1개의 호스트상에 있으면서, 호스트 상의 의존관계에 일체 영향을 미치지 않고, 그리고 컨테이너간의 환경 차이를 신경쓰지 않고, 스무스하게 업무에 활용할 수 있음을 기대할 수 있다. 

 

 

마무리


Docker 이미지, 이미지 레이어, 컨테이너의 차이에 대해서 알아보았다.

- 추상적인 Docker 이미지이란, 이미지 레이어(층)을 겹치는 형식으로 되어 있다.

- Docker 컨테이너 실행이란, 호스트상에 있는 여러 개의 디렉토리나 파일을 1개 파일 시스템 내에 마운트하는 것과 같이 보이는 것처럼, 이 파일 시스템내에 어떤 프로그램을 특별한 상태(이름 공간의 분리등)에 실행하는 것이다. 

- Docker 컨테이너에서는 추가한 이미지 레이어를 커밋하고 새로운 이미지 레이어를 작성할 수 있다. 보통 Dockerfile과 달리, docker build 커맨드로 Docker 이미지를 자동 구축한다. 

더욱 상세한 정보나 Docker에 대해서 그리고 Dockerfile의 자세한 작성법은 아래의 슬라이드 자료를 보는 것이 좋을 것 같다. (일본어 자료)

- 컨테이너의 생성법 <Docker뒤에서는 무엇이 이루어지고 있는가?>

- Dockerfile를 쓰기 위한 최적의 방법 해설편

최신 문서의 일본어 번역이나, 아래의 섹션을 참고할 수 있다.

- Dockerfile의 최적의 방법 - Docker docs-ja 19.03버전 문서

- Docker로 개발 - Docker-docs-ja 19.03 문서


참고자료

qiita.com/zembutsu/items/24558f9d0d254e33088f

728x90