들어가기 전
아래 링크에 해당하는 내용을 직접 해보고 정리한 내용입니다.
도커는 컨테이너를 실행하고 관리할 수 있도록 도와주는 도구입니다.
도커 이미지는 도커를 받들고 있는 중요한 기둥 중 하나입니다.
도커 이전의 컨테이너 기술들에서는 컨테이너 환경을 완전하고 효율적으로 복원한다는 게 상당히 어려웠습니다.
하지만 도커는 파일을 계층으로 나눠서 저장할 수 있는 유니온 마운트 기술과 도커 허브라는 원격 저장소를 기본적으로 제공함으로써 이 문제를 해결했습니다.
목차
- 도커 이미지는 도커 허브에서 온다.
- 도커 이미지는 어디에 저장될까요?
- 컨테이너의 레이어 계층 이해하기
- Docker diff와 Docker commit으로 이미지를 만들어보자
- Docker commit으로 이미지 만들기, Dockerfile로 이미지 만들기
- OverlayFS 직접 사용해보고, 임의의 위치에 도커 이미지 마운트 하기
도커 이미지는 도커 허브에서 온다.
도커 이미지를 pull 받아오는 경우 다음과 같은 명령어로 받아올 수 있습니다.
$ docker pull nginx:latest
latest: Pulling from library/nginx
e9995326b091: Pull complete
71689475aec2: Pull complete
f88a23025338: Pull complete
0df440342e26: Pull complete
eef26ceb3309: Pull complete
8e3ed6a9e43a: Pull complete
Digest: sha256:943c25b4b66b332184d5ba6bb18234273551593016c0e0ae906bab111548239f
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
pull 받아올 때는 분명 nginx:latest라는 이미지를 요청했는데 최하단 줄을 보시면
pull 받아진 이미지의 이름은docker.io/library/nginx:latest입니다.
이는 도커 허브를 기준으로 도커 이미지의 이름은
<NAMESPACE>/<IMAGE_NAME>:<TAG> 형식으로 구성되기 때문입니다.
이는 docker info 명령으로 확인해볼 수 있습니다.
$ docker info
...
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
hubproxy.docker.internal:5000
127.0.0.0/8
Live Restore Enabled: false
Registry에서 docker.io가 아닌 https://index.docker.io/v1/으로 구성되어 있습니다. 도커 인덱스는 도커 허브의 원래 이름이자 도메인입니다.
아까 위에서 이미지를 풀 받았을때 Digest 항목을 보시면
Digest: sha256:943c25b4b66b332184d5ba6bb18234273551593016c0e0ae906bab111548239f
다음과 같은 값이 출력되었습니다. 도커 허브에서는 이 값을 가지고도 이미지를 풀 받을 수도 있으며 아래는 이미지를 풀을 받을 때 같은 의미를 가집니다.
- nginx:latest
- nginx@sha256:943c25b4b66b332184d5ba6bb18234273551593016c0e0ae906bab111548239f
- library/nginx:latest
- docker.io/library/nginx:latest
- index.docker.io/library/nginx:latest
도커 이미지는 어디에 저장될까요?
앞서 도커 이미지를 풀 받을 때 다음과 같은 부분이 출력되었습니다.
e9995326b091: Pull complete
71689475aec2: Pull complete
f88a23025338: Pull complete
0df440342e26: Pull complete
eef26ceb3309: Pull complete
8e3ed6a9e43a: Pull complete
이는 각각의 한 줄이 레이어에 해당되는 것을 알 수 있습니다.
이미지를 풀 받으면 레이어들은 독립적으로 저장됩니다. 그리고 레이어들을 차례대로 쌓아 올려서 특정 위치에 마운트를 합니다. 기본적으로 이미지에 속하는 레이어들은 읽기 전용이므로 절대로 변하지 않습니다!
도커의 데이터는 기본적으로 시스템 상의 /var/lib/docker/에 저장되며
overlay2 드라이버로 저장된 레이어 데이터는 다시 image/overlay2/layerdb/sha256 아래에 저장됩니다
docker run -v/:/data -it ubuntu /bin/bash
chroot /data
cd /var/lib/docker/image/overlay2/layerdb/sha256
ls -l
$ docker image inspect nginx
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:a12586ed027fafddcddcc63b31671f406c25e43342479fc92a330e7e30d65f2e",
"sha256:e74d0d8d2defd5fff2f34af104d18e2512941fd9a6abb0581a6abcc95d7e90ee",
"sha256:2280b348f4d6af723032eec5a0c05f07222d6d10eafb5687eb2e86ca69de04fd",
"sha256:9e7119c28877f445e5893da11829e0aaa4e5b8112bf1521aebc4fd40219ddbae",
"sha256:4091cd312f19d65a309dd0962d374daf40f3f14b8c9e11538a6f250819c72801",
"sha256:a2e59a79fae0d350555b7143026eb0a6a55e31b0de877f6b202d5bde77b1e863"
]
},
여기서 레이어 목록을 확인해보면, 위에서 layerdb/sha256 아래에 있는 디렉터리 이름과 일치하는 것을 확인할 수 있습니다. (a12586~...)
실제 레이어 내용을 확인해보면 다음과 같습니다.
파일 목록 중에 cache-id라는 파일이 있습니다. 이 값을 출력해보면 실제 데이터가 있는 디렉터리의 다이제스트 값이 출력됩니다.
다이제스트 이름을 가진 디렉터리 아래의 diff 파일에 레이어의 콘텐츠가 들어있습니다.
$ sh-5.1# cat cache-id ; echo
924559faf4053ee66c4e7275165a8a47dd640957ca60305513706ebc499ba74e
924 ~ 는 nginx:latest 이미지의 베이스 이미지에 해당하는 debian:buster-slim 이미지입니다. 리눅스 운영체제의 기본적인 디렉터리 구성을 확인할 수 있네요. 정말 여기에 이미지의 내용이 저장되는 걸까요? 한 번 파일을 추가해서 확인해보겠습니다. debian:buster-slim 이미지로 컨테이너를 실행시켜보겠습니다.
위에서 확인한 디렉터리 구조와 완전히 같은 것을 확인할 수 있습니다.
$ pwd
/var/lib/docker/overlay2/924559faf4053ee66c4e7275165a8a47dd640957ca60305513706ebc499ba74e/diff
$ echo 'Hello, world!' > AWESOME_NEW_FILE
$ ls
AWESOME_NEW_FILE boot etc lib media opt root sbin sys usr
bin dev home lib64 mnt proc run srv tmp var
$ cat AWESOME_NEW_FILE
Hello, world!
앞서 이미지의 레이어는 절대로 변하지 않는다고 말씀드렸습니다. 하지만 관리자 권한으로 파일을 변경한다면 위와 같이 변경사항들이 반영됩니다.
따라서 도커를 사용할 때는 절대 이런 식으로 레이어를 임의로 변경해서는 안 됩니다!
컨테이너의 레이어 계층 이해하기
도커 이미지가 어떻게 만들어지는지 이해하려면 먼저 컨테이너의 레이어 구조에 대해 알아야 합니다. 그러면 실제로 컨테이너를 실행했을 때 레이어 계층이 어찌 구성되는지 알아봅시다!
$ docker history nginx:latest
IMAGE CREATED CREATED BY SIZE COMMENT
76c69feac34e 6 days ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
<missing> 6 days ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
<missing> 6 days ago /bin/sh -c #(nop) EXPOSE 80 0B
<missing> 6 days ago /bin/sh -c #(nop) ENTRYPOINT ["/docker-entr… 0B
<missing> 6 days ago /bin/sh -c #(nop) COPY file:e57eef017a414ca7… 4.62kB
<missing> 6 days ago /bin/sh -c #(nop) COPY file:abbcbf84dc17ee44… 1.27kB
<missing> 6 days ago /bin/sh -c #(nop) COPY file:5c18272734349488… 2.12kB
<missing> 6 days ago /bin/sh -c #(nop) COPY file:7b307b62e82255f0… 1.62kB
<missing> 6 days ago /bin/sh -c set -x && addgroup --system -… 61.2MB
<missing> 6 days ago /bin/sh -c #(nop) ENV PKG_RELEASE=1~bullseye 0B
<missing> 6 days ago /bin/sh -c #(nop) ENV NJS_VERSION=0.7.7 0B
<missing> 6 days ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.23.2 0B
<missing> 6 days ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
<missing> 7 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 7 days ago /bin/sh -c #(nop) ADD file:8644a8156a07a656a… 80.5MB
이 중에서 실제 레이어로 사용되는 2줄만 추출하겠습니다.
nginx-layer2 /bin/sh -c #(nop) ADD file:8644a8156a07a656a… 80.5MB
nginx-layer1 /bin/sh -c set -x && addgroup --system -… 61.2MB
레이어는 아래에서부터 위로 쌓아 올라갑니다.
$docker run -it nginx:latest bash
root@f477648881e8:/#
컨테이너와 함께 만들어진 레이어를 container-layer-f477648881e8 라고 이름을 대충 지어줍시다.
따라서 컨테이너 실행 시 마운트 되는 구조는 아래와 같습니다.
container-layer-f477648881e8(RW)
nginx-layer2(RO) = nginx:latest
nginx-layer1(RO)
container-layer-f477648881e8 레이어에는 아무것도 존재하지 않습니다. 하지만 컨테이너에서 파일 목록을 확인해보면
nginx-layer1, nginx-layer2, container-layer-f477648881e8의 파일 전체를 마운트한 작업 디렉토리가 하나 있고 그 이후 디렉토리에서 일어나는 모든 작업은 최상위 레이어인 container-layer-f477648881e8에 저장됩니다.
실제 컨테이너의 마운트 구조를 확인해보도록 하겠습니다.
$ docker inspect f477648881e8 | jq '.[].GraphDriver'
{
"Data": {
"LowerDir": "/var/lib/docker/overlay2/69b0f26e9f800067f924efe57968f16fb75bb6cdfff3ba9ac84c638e05bae4ff-init/diff:/var/lib/docker/overlay2/c46674dc436021e180a66c342face21c4f54ed8950a70b49759b67973cd35e4c/diff:/var/lib/docker/overlay2/f0178b6d8ba6f421bd1e34ac358d6b031e5c61f1e21b834d5f2bbc5dfa9c87a2/diff:/var/lib/docker/overlay2/6b68321985298fb962236568668a7b58959c8cf7c48e9e16afea674b4b8ea31b/diff:/var/lib/docker/overlay2/b5da6cd4200e88b1844cf6ed4c4d7082c055104f17636da8944ed78305a4a460/diff:/var/lib/docker/overlay2/687e4cd86cff67f2c03635e44f887e28691eabe37a7c0239a1f8e1605a35220d/diff:/var/lib/docker/overlay2/581dc6d312202c94a4876723fc3311884f9639d2e6b04da11b54b4000b0d8d9b/diff",
"MergedDir": "/var/lib/docker/overlay2/69b0f26e9f800067f924efe57968f16fb75bb6cdfff3ba9ac84c638e05bae4ff/merged",
"UpperDir": "/var/lib/docker/overlay2/69b0f26e9f800067f924efe57968f16fb75bb6cdfff3ba9ac84c638e05bae4ff/diff",
"WorkDir": "/var/lib/docker/overlay2/69b0f26e9f800067f924efe57968f16fb75bb6cdfff3ba9ac84c638e05bae4ff/work"
},
"Name": "overlay2"
}
LowerDir에는 이미지 레이어들이, UpperDir에는 컨테이너 레이어가 됩니다.
컨테이너 2개를 실행하면 다음과 같은 구조입니다.
container:f477648881e8
--------------------------------
container-layer-f477648881e8(RW)
nginx-layer2(RO) = nginx:latest
nginx-layer1(RO)
container:a437642881a8
--------------------------------
container-layer-a437642881a8(RW)
nginx-layer2(RO) = nginx:latest
nginx-layer1(RO)
따라서 아무리 많은 컨테이너를 실행시켜도 실제 쓰기가 이루어지는 레이어가 분리되어 있으므로 컨테이너들은 서로 영향을 주지 않습니다.
Docker diff와 Docker commit으로 이미지를 만들어보자
먼저 docker diff, docker commit 명령어를 사용해봅시다.
다음과 같이 터미널을 하나 더 열어서 diff 해보았을 때 아무런 변화가 없으면 정상입니다. 이유는 아직 컨테이너 레이어에 변경사항이 없기 때문입니다. 한번 실제로 디렉토리를 확인해보겠습니다.
$ docker inspect 958407847e48 | jq '.[].GraphDriver.Data.UpperDir'
/var/lib/docker/overlay2/8a524473a54148f84efcacb5215e2258686eb73be41b5adea5f0f39d1734f1fe/diff
컨테이너의 bash 셸에서 파일을 하나 만들어봅시다.
root@958407847e48:/# cd tmp
root@958407847e48:/tmp# touch THIS_IS_CONTAINER
docker diff 명령어로 변경된 내용을 확인합니다.
$ docker diff 958407847e48
C /tmp
A /tmp/THIS_IS_CONTAINER
$ tree
.
└──tmp
└──THIS_IS_CONTAINER
#C는 변경, A는 추가를 의미
컨테이너에서 변경 사항은 이미지나 같은 이미지를 사용하는 다른 컨테이너에는 아무런 영향을 끼치지 않습니다.
docker diff를 알아보았는데요 변경사항을 이미지로 저장하는 방법을 수행하는 명령어가 바로 docker commit입니다.
이미지에 저장되어있는 파일 하나를 삭제해보겠습니다.
root@958407847e48:/# rm /bin/tar
root@958407847e48:/# tar --version
bash: tar: command not found
$ docker diff 958407847e48
C /bin
D /bin/tar
C /tmp
A /tmp/THIS_IS_CONTAINER
$ tree
.
├──bin
│└──tar
└──tmp
└──THIS_IS_CONTAINER
D라는 문자열이 보이시나요? 이것은 파일이 삭제되었음을 의미합니다.
어라 근데 tar이 있네요?
사실 tar은 색이 살짝 다르게 출력될 것입니다. 이 파일은 일반 파일이 아니라 Character device 형식으로 저장되며, 이는 OverlayFS에서 파일 삭제를 나타내는 특별한 파일입니다. 이 파일은 하위 레이어의 파일이 존재하지 않는 것처럼 가려버리는 역할을 합니다.
$docker commit 958407847e48 nginx:without_tar
sha256:b49ed7f6a0034c83996a1c2d5cff6ea70ad63c9dbcf71e888a0398fb2988dec6
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx without_tar b49ed7f6a003 8 seconds ago 142MB
nginx latest 76c69feac34e 7 days ago 142MB
그리고 이 이미지로 컨테이너를 실행하면 tar를 사용할 수 없습니다.
$ docker run -it nginx:without_tar bash
root@153da22d0dc9:/# tar
bash: tar: command not found
docker history 명령어로 레이어를 확인해보겠습니다.
$ docker history nginx:without_tar
IMAGE CREATED CREATED BY SIZE COMMENT
b49ed7f6a003 2 minutes ago bash 0B
76c69feac34e 7 days ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
<missing> 7 days ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
<missing> 7 days ago /bin/sh -c #(nop) EXPOSE 80 0B
b49ed7f6a003는 bash 명령어로 방금 만든 이미지입니다. 이 이미지가 nginx:without_tar 이미지입니다.
이게 바로 도커 이미지가 만들어지는 기본적인 원리입니다.
Docker commit으로 이미지 만들기, Dockerfile로 이미지 만들기
앞에서 commit 명령어를 사용해서 컨테이너에서 직접 이미지를 만들어 보았습니다.
컨테이너와 이미지는 별개인 것이 아닌, 이미지가 실제로는 컨테이너를 기반으로 만들어집니다.
이번에 만들어볼 이미지는 git이 설치된 ubuntu이미지입니다.
먼저 우분투 이미지에 git이 없는지 확인해봅시다.
$ docker pull ubuntu:focal
$ docker run -it ubuntu:focal /bin/sh -c 'git --version'
/bin/sh: 1: git: not found
없는 것을 확인하고 이제 git을 설치해봅시다!
$ docker run ubuntu:focal /bin/sh -c 'apt-get update'
$ docker commit $(docker ps -alq) ubuntu:git-layer-1
$ docker run ubuntu:git-layer-1 /bin/sh -c 'apt-get install -y git'
$ docker commit $(docker ps -alq) ubuntu:git
총 두 단계에 걸쳐서 설치했습니다. 첫 번째는 apt-get update 명령어로 ubuntu:git-layer1 이미지로 저장했습니다.
두 번째로는 ubuntu:git-layer1 이미지에서 apt-get install -y git 명령어로 git을 설치했습니다. 그리고 이 레이어는 ubuntu:git 이미지로 저장됩니다.
$ docker run -it ubuntu:git bash -c 'git --version'
git version 2.25.1
잘 설치가 되었네요. docker history로 이미지의 계층을 살펴봅시다.
$ docker history ubuntu:git
IMAGE CREATED CREATED BY SIZE COMMENT
74963b88482b 2 minutes ago /bin/sh -c apt-get install -y git 102MB
71d0f280fe72 4 minutes ago /bin/sh -c apt-get update 38.9MB
680e5dfb52c7 7 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 7 days ago /bin/sh -c #(nop) ADD file:7633003155a105941… 72.8MB
680e5dfb52c7은 ubuntu:focal 이미지의 레이어입니다.
자 이제 dockerfile로 같은 이미지를 만들어봅시다.
FROM ubuntu:focal
RUN apt-get update
RUN apt-get install -y git
이 내용을 Dockerfile로 저장하시고 아래와 같이 ubuntu:git2 이미지를 만들어봅시다.
$ docker build -t ubuntu:git2 .
$ docker run -it ubuntu:git2 bash -c 'git --version'
git version 2.25.1
잘 설치가 된 모습을 볼 수 있습니다.
$ docker history ubuntu:git2
IMAGE CREATED CREATED BY SIZE COMMENT
e96cbf89ff1b About a minute ago RUN /bin/sh -c apt-get install -y git # buil… 102MB buildkit.dockerfile.v0
<missing> 2 minutes ago RUN /bin/sh -c apt-get update # buildkit 38.9MB buildkit.dockerfile.v0
<missing> 7 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 7 days ago /bin/sh -c #(nop) ADD file:7633003155a105941… 72.8MB
ubuntu:git와 ubuntu:git2가 거의 유사한 모습을 보여주고 있습니다.
- 베이스 이미지에서 컨테이너로 명령어 실행 (변경된 내용을 이미지로 저장)
- 위 이미지를 기반으로 다시 컨테이너로 명령어 실행
- 다시 변경된 내용을 이미지로 저장
위 과정을 도커 파일이 끝날 때까지 반복합니다.
즉, Dockerfile 한 줄마다 컨테이너로 실행되고 이미지로 만들어집니다.
OverlayFS 직접 사용해보고, 임의의 위치에 도커 이미지 마운트 하기
도커 이미지를 제대로 이해하려면 OverayFS 혹은 유니온 마운트 개념을 이해할 필요가 있습니다.
$ mkdir overlayfs; cd overlayfs
$ mkdir container image1 image2 merge work
$ touch image1/a image1/b image2/c
$ sudo mount -t overlay overlay -o lowerdir=image2:image1,upperdir=container,workdir=work merge
먼저 5개의 디렉토리를 만들어 봤습니다. 계층은 아래와 같습니다.
container(최상위) = upperdir
image2
image1
container가 최상위 디렉토리이며 OverlayFS에서 최상위 디렉토리는 upperdir으로 나타냅니다.
나머지 하위 디렉토리는 모두 lowerdir에 포합 됩니다.
$ tree . -I work
.
├── container
├── image1
│ ├── a
│ └── b
├── image2
│ └── c
└── merge
├── a
├── b
└── c
4 directories, 6 files
실제 구조를 살펴보면 위와 같습니다. 이미지들의 파일들이 merge 디렉토리에 합쳐서 보입니다.
merge 디렉토리에서 a파일을 삭제하고 d파일을 추가해봅시다.
$ rm ./merge/a
$ touch ./merge/d
$ tree . -I work
.
├── container
│ ├── a
│ └── d
├── image1
│ ├── a
│ └── b
├── image2
│ └── c
└── merge
├── b
├── c
└── d
4 directories, 8 files
처음과 비교해보면 merge 디렉토리에는 a가 삭제됐고 d가 추가됐습니다. 그리고 container에도 d가 추가되었습니다. 또한 a라는 파일도 추가되었습니다.
이는 앞에서 잠깐 설명한 것과 같이 Character device라는 특수한 형식의 파일로 삭제된 파일을 의미합니다. 그리고 image1과 image2에는 아무런 변화도 없습니다.
끝내는 말
도커를 처음 접한 것은 아니지만 사실 과거에는 그냥 코드 복붙만 하고 대충 이해만 한 상황이었지만 위와 같이 직접 실습을 통해서 도커 이미지의 구조에 대해 알아보니 정말 이해가 잘 되네요. 별거 안 한 것 같지만 오류가 많이 나고 고치고 직접 이해하는데 거의 9시간이 걸렸네요. 아무튼 여러분들도 직접 해보시길 바랍니다.