2024. 4. 3. 15:12ㆍDocker
이 장에서는 다음을 다룹니다:
- Manual image construction and practices
- Images from a packaging perspective
- Working with flat images
- Image versioning best practices
이 장의 목표는 이미지 설계에 대한 이해를 돕고, 이미지를 빌드하는 도구를 배우며, 고급 이미지 패턴을 발견하도록 돕는 것입니다. 이를 위해 실제 예제를 통해 학습할 것입니다.
시작하기 전에, 이 책의 1부에서 다룬 개념들을 확실히 이해하고 있어야 합니다.
Docker 이미지는 컨테이너 내부에서 기존 이미지를 수정하거나, Dockerfile이라는 빌드 스크립트를 정의하고 실행하여 생성할 수 있습니다. 이 장에서는 이미지를 수동으로 변경하는 과정, 이미지 조작의 기본 메커니즘, 그리고 생성되는 아티팩트에 초점을 맞춥니다. Dockerfile과 빌드 자동화는 8장에서 다룹니다.
7.1 컨테이너에서 Docker 이미지 빌드하기
컨테이너 사용에 익숙하다면 이미지를 빌드하는 것은 간단합니다.
컨테이너의 파일 시스템은 유니온 파일 시스템(UFS) 마운트를 통해 제공된다는 점을 기억하세요. 컨테이너 내부에서 파일 시스템에 가한 모든 변경 사항은 이를 생성한 컨테이너가 소유하는 새로운 레이어로 작성됩니다.
실제 소프트웨어를 다루기 전에, 다음 섹션에서는 "Hello, World" 예제를 통해 전형적인 워크플로를 설명합니다.
7.1.1 "Hello, World" 패키징
컨테이너에서 이미지를 빌드하는 기본 워크플로는 세 가지 단계로 이루어집니다.
첫 번째로, 기존 이미지에서 컨테이너를 생성합니다. 새로 완성될 이미지에 포함하고 싶은 내용과 변경 작업에 필요한 도구를 기준으로 이미지를 선택합니다.
두 번째 단계는 컨테이너의 파일 시스템을 수정하는 것입니다. 이러한 변경 사항은 컨테이너의 유니온 파일 시스템의 새로운 레이어로 작성됩니다. 이미지, 레이어, 리포지토리 간의 관계는 이 장에서 다시 다룰 예정입니다.
변경 작업이 완료되면 마지막 단계로 해당 변경 사항을 커밋합니다. 이후에는 커밋 결과후, 새 이미지에서 새로운 컨테이너를 생성할 수 있게 됩니다. 그림 7.1은 이 워크플로를 설명합니다.
이 단계를 염두에 두고, 다음 명령어를 실행하여 hw_image라는 새 이미지를 생성하세요:
docker container run --name hw_container \
ubuntu:latest \
touch /HelloWorld <--- Added file in container
docker container commit hw_container hw_image <---Commits change to new image
docker container rm -vf hw_container <--- Removes changed container
docker container run --rm \
hw_image \
ls -l /HelloWorld <--- Examines file in new container
이것이 매우 간단해 보인다면, 생성되는 이미지가 더 복잡해질수록 약간 더 정교해진다는 것을 알아두세요. 하지만 기본적인 단계는 항상 동일합니다. 이제 워크플로에 대한 개념을 잡았으니, 실제 소프트웨어를 사용하여 새 이미지를 빌드해 보세요. 이번에는 Git이라는 프로그램을 패키징하게 될 것입니다.
7.1.2 Preparing packaging for Git
Git은 널리 사용되는 분산 버전 관리 도구입니다. 이 주제에 대해 다룬 책들이 많이 나와 있습니다. Git을 잘 모른다면 사용법을 배우는 데 시간을 투자하는 것을 추천합니다. 그러나 지금은 Git이 Ubuntu 이미지에 설치할 프로그램이라는 것만 알면 됩니다.
자신만의 이미지를 빌드하려면 먼저 적절한 베이스 이미지에서 컨테이너를 생성해야 합니다. 다음 명령어를 실행하세요:
docker container run -it --name image-dev ubuntu:latest /bin/bash
이 명령어는 bash 셸을 실행하는 새 컨테이너를 시작합니다. 이 프롬프트에서 컨테이너를 커스터마이징하기 위한 명령어를 실행할 수 있습니다.
Ubuntu는 소프트웨어 설치를 위한 apt-get이라는 Linux 도구를 제공합니다. Docker 이미지에 패키징하려는 소프트웨어를 얻는 데 유용할 것입니다. 이제 컨테이너와 함께 대화형 셸이 실행되고 있어야 합니다. 다음으로 컨테이너에 Git을 설치해야 합니다. 아래 명령어를 실행하세요:
apt-get update
apt-get -y install git
이 명령어는 APT에게 Git과 모든 종속성을 다운로드하여 컨테이너의 파일 시스템에 설치하도록 지시합니다. 설치가 완료되면 다음 명령어로 설치를 테스트할 수 있습니다:
git version
출력 예시:
git version 2.7.4
apt-get과 같은 패키지 도구는 소프트웨어 설치 및 제거를 수작업으로 하지 않고도 쉽게 처리할 수 있도록 해줍니다. 하지만 이러한 도구는 소프트웨어에 대한 격리를 제공하지 않으며, 종속성 충돌이 자주 발생합니다. 그러나 이 컨테이너 외부에서 설치한 다른 소프트웨어가 이 컨테이너에 설치된 Git 버전에 영향을 주지 않는다는 점에서 안심할 수 있습니다.
이제 Git이 Ubuntu 컨테이너에 설치되었으므로 단순히 컨테이너를 종료할 수 있습니다:
exit
컨테이너는 중지되지만 여전히 컴퓨터에 남아 있습니다. Git은 ubuntu:latest
이미지의 새로운 레이어로 설치되었습니다. 만약 지금 이 예제를 떠나 며칠 뒤에 다시 돌아온다면, 정확히 어떤 변경이 이루어졌는지 어떻게 알 수 있을까요? 소프트웨어를 패키징할 때 컨테이너에서 수정된 파일 목록을 검토하는 것이 종종 유용하며, 이를 위한 Docker 명령어가 있습니다.
7.1.3 Reviewing filesystem changes
Docker에는 컨테이너 내부에서 이루어진 모든 파일 시스템 변경 사항을 보여주는 명령어가 있습니다. 이러한 변경 사항에는 추가되거나, 변경되거나, 삭제된 파일 및 디렉터리가 포함됩니다. APT를 사용해 Git을 설치하면서 발생한 변경 사항을 검토하려면 다음과 같이 diff 서브커맨드를 실행하세요:
docker container diff image-dev <--- 파일 변경 사항의 긴 목록을 출력
A로 시작하는 라인은 추가된 파일을 나타냅니다.
C로 시작하는 라인은 변경된 파일을 나타냅니다.
마지막으로 D로 시작하는 라인은 삭제된 파일을 나타냅니다.
APT를 사용해 Git을 설치하면 여러 변경 사항이 발생합니다. 따라서 몇 가지 구체적인 예제를 통해 이를 확인하는 것이 더 나을 수 있습니다.
<--- Add new file to busybox --->
$ docker container run --name tweak-a busybox:latest touch /HelloWorld
$ docker container diff tweak-a
# Output:
# A /HelloWorld
<--- Removes existing file from busybox --->
$ docker container run --name tweak-d busybox:latest rm /bin/vi
$ docker container diff tweak-d
# Output:
# C /bin
# D /bin/vi
<--- Changes existing file in busybox --->
$ docker container run --name tweak-c busybox:latest touch /bin/vi
$ docker container diff tweak-c
# Output:
# C /bin
# C /bin/busybox
항상 작업 공간을 정리하는 것을 잊지 마세요. 예를 들어 다음과 같이 할 수 있습니다:
docker container rm -vf tweak-a
docker container rm -vf tweak-d
docker container rm -vf tweak-c
이제 파일 시스템에 적용한 변경 사항을 확인했으니, 이러한 변경 사항을 새로운 이미지로 커밋할 준비가 되었습니다. 대부분의 다른 작업과 마찬가지로, 이를 수행하는 명령은 여러 작업을 한 번에 처리합니다.
7.1.4 Committing a new image
docker container commit 명령을 사용하여 수정된 컨테이너에서 이미지를 생성합니다. -a 플래그를 사용하여 이미지에 작성자를 지정하는 것이 모범 사례입니다. 또한 항상 -m 플래그를 사용하여 커밋 메시지를 설정해야 합니다. Git을 설치한 image-dev 컨테이너에서 ubuntu-git이라는 이름의 새 이미지를 생성하고 서명하려면 다음 명령을 사용합니다:
docker container commit -a "@dockerinaction" -m "Added git" \
image-dev ubuntu-git
# 참고
docker container commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
결과로 고유한 이미지 식별자가 출력됩니다. 예:
bbf1d5d430cdf541a72ad74dfa54f6faec41d2c1e4200778e9d4302035e5d143
이미지를 커밋한 후, 해당 이미지는 컴퓨터에 설치된 이미지 목록에 나타납니다. docker images 명령을 실행하면 다음과 같은 라인이 포함되어야 합니다:
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu-git latest bbf1d5d430cd 5 seconds ago 248 MB
이 이미지를 사용해 Git을 테스트하여 작동 여부를 확인합니다:
docker container run --rm ubuntu-git git version
이제 Ubuntu 이미지를 기반으로 새 이미지를 생성하고 Git을 설치했습니다. 훌륭한 시작입니다. 그러나 명령어 재정의를 생략하면 어떻게 되는지 생각해보십시오. 다음 명령어를 실행해 보세요:
docker container run --rm ubuntu-git
이 명령을 실행해도 아무 일도 발생하지 않는 것처럼 보입니다. 그 이유는 새 이미지와 함께 커밋된 디폴트 커맨드가 원래 컨테이너를 시작했던 커맨드이기 때문입니다.
이미지를 생성한 컨테이너를 시작할 때 사용된 명령은 /bin/bash입니다. 이 이미지를 디폴트 명령으로 새 컨테이너를 생성하면 셸이 시작되고 즉시 종료됩니다. 이는 유용하지 않은 디폴트 커맨드입니다.
ubuntu-git이라는 이름의 이미지 사용자들이 매번 Git을 수동으로 호출해야 한다고 기대하지 않을 것입니다. 이미지의 Entrypoint를 Git으로 설정하는 것이 더 나을 것입니다. 엔트리포인트(Entrypoint)는 컨테이너가 시작될 때 실행될 프로그램입니다. 엔트리포인트가 설정되지 않으면 디폴트 커맨드가 직접 실행됩니다. 엔트리포인트가 설정된 경우 디폴트 커맨드와 해당 아규먼트가 엔트리포인트에 아규먼트로 전달됩니다.
엔트리포인트를 설정하려면 --entrypoint 플래그를 설정하여 새 컨테이너를 생성하고, 해당 컨테이너에서 새 이미지를 생성합니다:
docker container run --name cmd-git --entrypoint git ubuntu-git
docker container commit -m "Set CMD git" \
-a "@dockerinaction" cmd-git ubuntu-git
docker container rm -vf cmd-git
docker container run --name cmd-git ubuntu-git version
이제 엔트리포인트가 Git으로 설정[--entrypoint git] 되었으므로 사용자는 끝에 커맨드를 입력할 필요가 없습니다. 이 예에서는 약간의 절약처럼 보일 수 있지만, 많은 도구는 그렇게 간결하지 않습니다. 엔트리포인트를 설정하는 것은 이미지를 사용자가 더 쉽게 사용할 수 있고 프로젝트에 통합할 수 있도록 만드는 방법 중 하나입니다.
[참고]
7.1.5 Configuring image attributes
docker container commit 커맨드를 사용하면 이미지에 새로운 레이어를 커밋합니다. 이때 파일 시스템 스냅샷만 포함되는 것이 아니라, 각 레이어에는 실행 컨텍스트를 설명하는 메타데이터도 포함됩니다. 컨테이너가 생성될 때 설정할 수 있는 파라미터 중 다음 항목들은 컨테이너에서 생성된 이미지로 함께 전달됩니다:
- All environment variables
- The working directory
- The set of exposed ports
- All volume definitions
- The container entrypoint
- Command and arguments
이 값들이 컨테이너에 대해 명시적으로 설정되지 않은 경우, 이 값들은 원본 이미지에서 상속됩니다. 이 책의 Part 1에서 이러한 항목 각각에 대해 다루었으므로 여기서 다시 소개하지는 않겠습니다. 그러나 두 가지 상세한 예를 살펴보는 것이 유용할 수 있습니다. 첫 번째로, 두 개의 특정 환경 변수를 도입하는 컨테이너를 고려해 보십시오:
docker container run --name rich-image-example \
-e ENV_EXAMPLE1=Rich -e ENV_EXAMPLE2=Example \ <--- Creates environment variable specializaiton
busybox:latest
docker container commit rich-image-example rie <--- Commits image
docker container run --rm rie \
/bin/sh -c "echo \$ENV_EXAMPLE1 \$ENV_EXAMPLE2" <--- Outputs: Rich Example
다음으로, 이전 예제 위에 새로운 레이어로 엔트리포인트와 명령어 특수화를 도입하는 컨테이너를 고려해 보십시오:
docker container run --name rich-image-example-2 \
--entrypoint "/bin/sh" \ <--- Sets default entrypoint
rie \
-c "echo \$ENV_EXAMPLE1 \$ENV_EXAMPLE2" <--- Sets default command
docker container commit rich-image-example-2 rie <--- Commits images
docker container run --rm rie <--- Different command with same output
이 예제는 BusyBox 위에 두 개의 추가 레이어를 생성합니다. 두 경우 모두 파일은 변경되지 않았지만, 컨텍스트 메타데이터가 변경되었기 때문에 동작이 바뀌었습니다. 이러한 변경 사항에는 첫 번째 새로운 레이어에서 두 개의 새로운 환경 변수가 포함됩니다. 이 환경 변수들은 두 번째 새로운 레이어에서도 명확히 상속되며, 엔트리포인트와 기본 명령을 설정하여 해당 값을 표시합니다. 마지막 명령은 이전에 정의된 동작이 상속되었음을 보여주며, 최종 이미지를 사용하여 대체 동작을 지정하지 않고 실행합니다.
이제 이미지를 수정하는 방법을 이해했으니, 이미지와 레이어의 메커니즘을 더 깊이 탐구해 보세요. 이를 통해 실제 상황에서 고품질 이미지를 제작하는 데 도움이 될 것입니다.
7.2 Going deep on Docker images and layers
이 장의 이 시점에서, 몇 가지 이미지를 만들어 보았습니다. 이러한 예제에서는 먼저 ubuntu:latest나 busybox:latest와 같은 이미지에서 컨테이너를 생성했습니다. 그런 다음 컨테이너 내에서 파일 시스템이나 컨텍스트에 변경을 가했습니다. 마지막으로 docker container commit 명령을 사용하여 새 이미지를 생성하면 모든 것이 제대로 작동하는 것처럼 보였습니다. 컨테이너의 파일 시스템이 어떻게 작동하는지와 docker container commit 명령이 실제로 무엇을 하는지 이해하면 더 나은 이미지 작성자가 될 수 있습니다. 이 섹션에서는 이 주제를 깊이 탐구하고 이미지 작성자에게 미치는 영향을 보여줍니다.
7.2.1 Exploring union filesystems
유니온 파일 시스템의 세부 사항을 이해하는 것은 이미지 작성자에게 두 가지 이유로 중요합니다:
- 작성자는 파일 추가, 변경, 삭제가 해당 이미지에 미치는 영향을 이해해야 합니다.
- 작성자는 레이어간의 관계와 레이어가 이미지, 리포지토리, 태그와 어떻게 연결되는지에 대해 확실히 이해해야 합니다.
간단한 예를 생각해 봅시다. 기존 이미지에 단일 변경 사항을 추가하려고 한다고 가정합니다. 이 경우 대상 이미지가 ubuntu:latest
이며, 루트 디렉토리에 mychange
라는 파일을 추가하려고 합니다. 이를 수행하려면 다음 명령을 사용해야 합니다:
docker container run --name mod_ubuntu ubuntu:latest touch /mychange
위 커맨드로 생성된 컨테이너(이름: mod_ubuntu
)는 중지 상태가 되지만, 해당 파일 시스템에는 이 단일 변경 사항(mychange 파일 생성)이 기록됩니다. 3장과 4장에서 논의된 바와 같이, 루트 파일 시스템은 컨테이너가 시작된 이미지에 의해 제공됩니다. 이 파일 시스템은 유니온 파일 시스템으로 구현됩니다.
유니온 파일 시스템은 레이어로 구성됩니다. 유니온 파일 시스템에 변경 사항이 있을 때마다, 해당 변경 사항은 모든 기존 레이어 위에 새로운 레이어로 기록됩니다. 이 모든 레이어의 유니온(또는 상위 레이어에서 아래로 내려다보는 관점)이 컨테이너(및 사용자)가 파일 시스템에 접근할 때 보이는 모습입니다. 그림 7.2는 이 예제에 대한 두 가지 관점을 보여줍니다.
유니온 파일 시스템에서 파일을 읽을 때 해당 파일은, 이 파일이 존재하는 레이어들의 최상위 레이어에서부터 읽힙니다. 파일이 최상위 레이어에서 생성되거나 변경되지 않은 경우 읽기는 해당 파일이 존재하는 레이어에 도달할 때까지 레이어들을 거쳐 내려갑니다. 이는 그림 7.3에 나와 있습니다.
이 모든 레이어 기능은 유니온 파일 시스템에 의해 숨겨집니다. 컨테이너에서 실행되는 소프트웨어가 이러한 기능을 활용하기 위해 특별한 작업을 수행할 필요는 없습니다. 파일이 추가된 레이어를 이해하는 것은 파일 시스템 쓰기의 세 가지 유형 중 하나를 다룹니다. 나머지 두 가지는 삭제와 파일 변경입니다.
파일 추가와 마찬가지로, 파일 변경 및 삭제는 모두 상위 레이어를 수정함으로써 작동합니다. 파일이 삭제되면 삭제 기록이 상위 레이어에 작성되며, 이는 하위 레이어의 해당 파일 버전을 숨깁니다. 파일이 변경되면 해당 변경 사항이 상위 레이어에 작성되며, 다시 하위 레이어의 해당 파일 버전을 숨깁니다. 컨테이너 파일 시스템에 적용된 변경 사항은 이전에 장에서 사용한 docker container diff
명령으로 나열할 수 있습니다:
docker container diff mod_ubuntu
이 명령은 다음과 같은 출력을 생성합니다:
A /mychange
여기서 A
는 파일이 추가되었음을 나타냅니다. 다음 두 명령을 실행하여 파일 삭제가 기록되는 방식을 확인해 보세요:
docker container run --name mod_busybox_delete busybox:latest rm /etc/passwd
docker container diff mod_busybox_delete
이번에는 두 줄의 출력이 나타납니다:
C /etc
D /etc/passwd
D
는 삭제를 나타내며, 이번에는 파일의 상위 폴더도 포함됩니다. C
는 폴더가 변경되었음을 나타냅니다. 다음 두 명령은 파일 변경을 보여줍니다:
docker container run --name mod_busybox_change busybox:latest touch /etc/passwd
docker container diff mod_busybox_change
diff
하위 명령은 두 개의 변경 사항을 보여줍니다:
C /etc
C /etc/passwd
다시, C
는 변경을 나타내며, 두 항목은 파일과 파일이 위치한 폴더입니다. 만약 다섯 단계 깊이의 파일이 변경되었다면, 트리의 각 레벨에 대한 라인이 추가됩니다.
파일 소유권 및 권한과 같은 파일 시스템 속성 변경도 파일 변경과 동일한 방식으로 기록됩니다. 많은 파일의 파일 시스템 속성을 변경할 때는 주의해야 합니다. 이러한 파일들은 변경을 수행하는 레이어에 복사될 가능성이 큽니다. 파일 변경 메커니즘은 유니온 파일 시스템에서 이해해야 할 가장 중요한 사항이며, 이를 좀 더 자세히 살펴볼 것입니다.
대부분의 유니온 파일 시스템은 카피 온 라이트(Copy-on-Write)라는 방식을 사용합니다. 이를 카피 온 체인지(Copy-on-Change)로 생각하면 이해하기 더 쉽습니다. 읽기 전용 레이어(상위 레이어가 아닌)에 있는 파일이 수정될 때, 변경이 이루어지기 전에 해당 파일 전체가 읽기 전용 레이어에서 쓰기 가능한 레이어로 먼저 복사됩니다. 이 과정은 실행 시간 성능과 이미지 크기에 부정적인 영향을 미칠 수 있습니다. 7.2.3 절에서는 이러한 점이 이미지 설계에 어떤 영향을 미치는지 다룹니다.
이 시스템에 대한 이해를 강화하기 위해, 그림 7.4에서 보다 포괄적인 시나리오 세트를 확인해 보세요. 이 그림에서는 파일이 추가되고, 변경되고, 삭제되고, 다시 추가되는 과정이 세 개의 레이어에 걸쳐 설명되어 있습니다.
파일 시스템 변경 사항이 어떻게 기록되는지 이해하면, docker container commit 커맨드를 사용하여 새로운 이미지를 생성할 때 무슨 일이 일어나는지 이해하기 시작할 수 있습니다.
7.2.2 Reintroducing images, layers, repositories, and tags
docker container commit 명령을 사용하여 이미지를 생성했고, 이는 상위 레이어의 변경 사항을 이미지에 커밋한다는 것을 이해했습니다. 그러나 아직 "커밋"의 정의를 살펴보지 않았습니다.
유니온 파일 시스템은 레이어 스택으로 구성되어 있으며, 새로운 레이어는 스택의 맨 위에 추가됩니다. 이러한 레이어는 해당 레이어에서 이루어진 변경 사항과 레이어에 대한 메타데이터의 집합으로 별도로 저장됩니다. 컨테이너의 파일 시스템 변경 사항을 커밋할 때, 상위 레이어의 복사본을 식별 가능한 방식으로 저장하게 됩니다.
레이어를 커밋할 때, 새로운 레이어 ID(ac94, 6435, 2044...)가 생성되며, 모든 파일 변경 사항의 복사본이 저장됩니다. 이것이 정확히 어떻게 이루어지는지는 시스템에서 사용 중인 스토리지 엔진에 따라 다릅니다. 세부 사항을 이해하는 것보다 일반적인 접근 방식을 이해하는 것이 더 중요합니다. 레이어의 메타데이터에는 생성된 레이어 ID, 하위 레이어(부모)의 ID, 레이어가 생성된 컨테이너의 실행 컨텍스트가 포함됩니다. 레이어의 ID와 메타데이터는 Docker와 유니온 파일 시스템(UFS)이 이미지를 구성하는 데 사용하는 그래프를 형성합니다.
이미지는 특정 상위 레이어에서 시작하여, 각 레이어의 메타데이터에 정의된 부모 ID를 따라가면서 얻는 레이어 스택입니다. 이는 그림 7.5에 나와 있습니다.
이미지는 특정 레이어에서 시작하여 레이어 의존성 그래프를 따라 생성된 레이어들의 스택입니다. 트래버설이 시작되는 레이어는 스택의 최상위 레이어입니다. 따라서 레이어의 ID는 해당 레이어와 그 의존성으로 구성된 이미지의 ID이기도 합니다.
앞서 생성한 mod_ubuntu
컨테이너를 커밋하여 이를 실제로 확인해 보십시오:
docker container commit mod_ubuntu
이 커밋 명령은 다음과 같은 새로운 이미지 ID를 포함한 출력을 생성합니다:
6528255cda2f9774a11a6b82be46c86a66b5feff913f5bb3e09536a54b08234d
이 이미지 ID를 사용하여 새 컨테이너를 생성할 수 있습니다. 컨테이너와 마찬가지로, 레이어 ID는 사람이 직접 다루기 어려운 긴 16진수 숫자입니다. 이러한 이유로 Docker는 리포지토리를 제공합니다.
3장에서, 리포지토리는 이미지를 저장하는 이름이 붙은 버킷으로 대략적으로 정의되었습니다. 더 구체적으로, 리포지토리는 특정 레이어 ID를 가리키는 위치/이름 쌍입니다. 각 리포지토리는 특정 레이어 ID와 따라서 이미지 정의를 가리키는 하나 이상의 태그를 포함합니다. 3장에서 사용한 예제를 다시 살펴보겠습니다.
이 레포지토리는 Docker Hub 레지스트리에 위치해 있지만, 우리는 완전한 레지스트리 호스트 이름인 docker.io를 사용했습니다. 이는 사용자(dockerinaction)와 고유한 짧은 이름(ch3_hello_registry)으로 명명되었습니다. 태그를 지정하지 않고 이 레포지토리를 pull하면 Docker는 latest 태그가 있는 이미지를 pull하려고 시도합니다. --all-tags 옵션을 pull 명령어에 추가하면 레포지토리의 모든 태그 이미지를 pull할 수 있습니다. 이 예제에서는 latest라는 하나의 태그만 있습니다. 해당 태그는 그림 7.6에 나타난 것처럼 짧은 형태의 ID 4203899414c0을 가진 레이어를 가리킵니다.
레포지토리와 태그는 docker tag, docker container commit, 또는 docker build 명령어를 통해 생성됩니다. mod_ubuntu 컨테이너를 다시 살펴보고, 이를 태그와 함께 레포지토리에 넣어보세요.
docker container commit mod_ubuntu myuser/myfirstrepo:mytag
# Outputs:
# 82ec7d2c57952bf57ab1ffdf40d5374c4c68228e3e923633734e68a11f9a2b59
위에 보이는 것처럼 생성된 ID는 레이어의 또 다른 복사본이 생성되었기 때문에 다르게 표시됩니다. 이 새로운 친숙한 이름을 사용하면 이미지를 기반으로 컨테이너를 생성하는 것이 매우 간단해집니다. 이미지를 복사하려면 기존 이미지에서 새로운 태그 또는 레포지토리를 생성하기만 하면 됩니다. 이는 docker tag 명령어로 수행할 수 있습니다. 모든 레포지토리는 기본적으로 latest 태그를 포함하며, 태그를 생략하면 이전 명령어에서처럼 latest 태그가 사용됩니다:
docker tag myuser/myfirstrepo:mytag myuser/mod_ubuntu
이 시점에서 기본적인 UFS(Union File System)의 원리와 Docker가 레이어, 이미지, 레포지토리를 생성 및 관리하는 방법을 충분히 이해했을 것입니다. 이를 염두에 두고 이미지 설계에 미칠 수 있는 영향을 고려해봅시다.
컨테이너를 위해 생성된 쓰기 가능한 레이어 아래에 있는 모든 레이어는 불변(immutable)하며, 수정할 수 없습니다. 이 특성은 모든 컨테이너마다 독립적인 복사본을 생성하는 대신 이미지를 공유할 수 있도록 해줍니다. 또한 개별 레이어를 재사용 가능하게 만듭니다. 이러한 특성의 또 다른 면은 이미지를 변경할 때마다 새 레이어를 추가해야 하며, 기존 레이어는 절대 제거되지 않는다는 점입니다.
이미지가 변경될 필요가 있음을 고려할 때, 이미지의 제한 사항을 인지하고 변경이 이미지 크기에 어떤 영향을 미칠지 항상 염두에 두어야 합니다.
7.2.3 Managing image size and layer limits
만약 이미지가 대부분의 사람들이 파일 시스템을 관리하는 방식과 동일하게 진화한다면, Docker 이미지는 빠르게 사용할 수 없게 될 것입니다. 예를 들어, 이 장에서 앞서 생성한 ubuntu-git 이미지를 다른 버전으로 만들고 싶다고 가정해보세요. ubuntu-git 이미지를 수정하는 것이 자연스럽게 보일 수 있습니다. 하지만 수정하기 전에 ubuntu-git 이미지에 대한 새 태그를 생성해야 합니다. 이후 latest 태그를 재지정하게 될 것입니다.
docker image tag ubuntu-git:latest ubuntu-git:2.7 <--- Creates new tag: 2.7
새 이미지를 빌드하는 첫 번째 단계는 설치했던 Git 버전을 제거하는 것입니다.
$ docker container run --name image-dev2 \--entrypoint /bin/bash \
> ubuntu-git:latest -c "apt-get remove -y git"
$ docker container commit image-dev2 ubuntu-git:removed
$ docker image tag ubuntu-git:removed ubuntu-git:latest
$ docker image ls
이미지 목록과 크기는 다음과 비슷하게 표시됩니다:
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu-git latest 826c66145a59 10 seconds ago 226.6 MB
ubuntu-git removed 826c66145a59 10 seconds ago 226.6 MB
ubuntu-git 2.7 3e356394c14e 41 hours ago 226 MB
...
Git을 제거했음에도 불구하고 이미지 크기가 실제로 증가한 것을 확인할 수 있습니다. docker container diff 명령어로 특정 변경 사항을 확인할 수도 있지만, 크기가 증가한 이유가 유니언 파일 시스템(UFS)과 관련이 있다는 점을 빠르게 알아차려야 합니다.
기억하세요, UFS는 파일을 삭제된 것으로 표시할 때 실제로 상위 레이어에 파일을 추가합니다. 원래 파일과 다른 레이어에 존재하는 모든 복사본은 여전히 이미지에 남아있습니다. 이미지를 사용하는 사람들과 시스템을 위해 이미지 크기를 최소화하는 것이 중요합니다. 스마트한 이미지 생성으로 긴 다운로드 시간과 과도한 디스크 사용을 방지할 수 있다면 사용자들에게 큰 이점을 제공할 수 있습니다.
Docker 초기에는 이미지 저장 드라이버의 한계로 인해 이미지의 레이어 수를 최소화하려는 경우가 있었습니다. 하지만 현대 Docker 이미지 저장 드라이버는 일반 사용자들이 마주칠만한 레이어 제한이 없으므로, 크기와 캐시 가능성 같은 다른 속성을 고려해 설계하세요.
docker image history 명령어를 사용하면 이미지의 모든 레이어를 확인할 수 있습니다. 다음과 같이 표시됩니다:
- Abbreviated layer ID
- Age of the layer
- Initial command of the creating container
- Total file size of that layer
ubuntu-git:removed 이미지의 히스토리를 확인하면, 원래 ubuntu:latest 이미지 위에 이미 세 개의 레이어가 추가된 것을 볼 수 있습니다.
$ docker image history ubuntu-git:removed
IMAGE CREATED CREATED BY SIZE
826c66145a59 24 minutes ago /bin/bash -c apt-get remove 662 kB
3e356394c14e 42 hours ago git 0 B
bbf1d5d430cd 42 hours ago /bin/bash 37.68 MB
b39b81afc8ca 3 months ago /bin/sh -c #(nop) CMD [/bin 0 B
615c102e2290 3 months ago /bin/sh -c sed -i 's/^#\s*\ 1.895 kB
837339b91538 3 months ago /bin/sh -c echo '#!/bin/sh' 194.5 kB
53f858aaaf03 3 months ago /bin/sh -c #(nop) ADD file: 188.1 MB
511136ea3c5a 22 months ago 0 B
docker image save를 사용하여 이미지를 TAR 파일로 저장한 후, docker image import를 사용해 해당 파일 시스템의 내용을 Docker로 다시 가져오면 이미지를 평탄화할 수 있습니다. 하지만 이는 좋은 방법이 아닙니다. 이렇게 하면 원본 이미지의 메타데이터, 변경 이력, 그리고 동일한 하위 레벨 이미지를 다운로드할 때 사용자들이 얻을 수 있는 절감 효과를 잃게 됩니다. 이 경우 더 현명한 방법은 브랜치를 생성하는 것입니다.
레이어 시스템에 맞서 싸우는 대신, 레이어 시스템을 활용하여 브랜치를 생성함으로써 크기와 레이어 증가 문제를 모두 해결할 수 있습니다. 레이어 시스템은 이미지의 이력을 돌아가 새로운 브랜치를 생성하는 것을 간단하게 만듭니다. 동일한 이미지에서 컨테이너를 생성할 때마다 잠재적으로 새로운 브랜치를 생성하게 됩니다.
새로운 ubuntu-git 이미지를 만들 전략을 다시 생각해본다면, 단순히 ubuntu:latest에서 다시 시작하는 것이 좋습니다. ubuntu:latest에서 새 컨테이너를 시작하여 원하는 버전의 Git을 설치할 수 있습니다. 이렇게 하면 처음 생성한 ubuntu-git 이미지와 새 이미지는 동일한 부모를 공유하게 되고, 새 이미지는 관련 없는 변경 사항의 잔여물을 가지지 않게 됩니다.
브랜칭은 동료 브랜치에서 이미 완료된 단계를 반복해야 할 가능성을 높입니다. 수작업으로 이를 처리하는 것은 오류를 초래할 수 있습니다. Dockerfile을 사용하여 이미지를 자동으로 빌드하는 것이 더 나은 방법입니다.
가끔 이미지를 처음부터 완전히 빌드해야 할 필요가 생깁니다. Docker는 빌드 프로세스에서 다음 명령을 결과 이미지의 첫 번째 레이어로 만들도록 지시하는 scratch 이미지를 특별히 처리합니다. 이 방식은 이미지 크기를 줄이는 것이 목표일 때, Go나 Rust 같은 의존성이 적은 프로그래밍 언어를 다룰 때 유용할 수 있습니다. 다른 경우에는 이미지의 히스토리를 다듬기 위해 이미지를 평탄화하고 싶을 수 있습니다. 이 두 경우 모두 전체 파일 시스템을 가져오고 내보내는 방법이 필요합니다.
7.3 Exporting and importing flat filesystems
어떤 경우에는 유니언 파일 시스템 또는 컨테이너의 컨텍스트 외부에서 이미지로 전송될 파일을 작업하여 이미지를 빌드하는 것이 유리할 수 있습니다. 이를 위해 Docker는 파일 아카이브(tar 파일)를 내보내고 가져오는 두 가지 명령어를 제공합니다.
docker container export 명령어는 평탄화된 유니언 파일 시스템의 전체 내용을 stdout 또는 출력 파일로 스트리밍하여 tarball 형식으로 저장합니다. 결과적으로 컨테이너 관점에서 모든 파일을 포함하는 tarball이 생성됩니다. 이는 컨테이너의 컨텍스트 외부에서 이미지와 함께 제공된 파일 시스템을 사용할 필요가 있을 때 유용합니다. 이 목적을 위해 docker cp 명령어를 사용할 수도 있지만, 여러 파일이 필요하다면 전체 파일 시스템을 내보내는 것이 더 직접적일 수 있습니다.
새 컨테이너를 생성하고 export 서브커맨드를 사용하여 해당 파일 시스템의 평탄화된 복사본을 가져오세요:
$ docker container create --name export-test \
> intheeast0305/ch7_packed:latest ./echo For Export
$ docker container export --output contents.tar export-test
$ docker container rm export-test
$ tar -tf contents.tar
현재 디렉토리에 contents.tar
라는 이름의 파일이 생성됩니다. 이 파일에는 ch7_packed
이미지에서 가져온 두 파일인 message.txt
와 folder/message.txt
가 포함되어 있습니다. 이 시점에서 해당 파일들을 추출하거나 검사하거나 원하는 대로 변경할 수 있습니다. 아카이브에는 /etc/resolv.conf
와 같은 컨테이너 관리와 관련된 장치나 파일의 0바이트 파일도 포함되는데, 이는 무시해도 됩니다. --output
(또는 간단히 -o
) 옵션을 생략했다면 파일 시스템의 내용이 tarball 형식으로 stdout
에 스트리밍됩니다. 파일 내용을 stdout
으로 스트리밍하면 tarball을 처리하는 다른 쉘 프로그램과 연결하여 export
명령어를 유용하게 사용할 수 있습니다.
docker import
명령어는 tarball의 내용을 새 이미지로 스트리밍합니다. import
명령어는 압축된 형식과 압축되지 않은 형식의 tarball을 모두 인식합니다. 또한 파일 시스템을 가져오는 동안 선택적으로 Dockerfile 지시문을 적용할 수도 있습니다. 파일 시스템을 가져오는 것은 최소 파일 세트를 이미지에 간단히 포함시키는 방법입니다.
이 기능의 유용성을 이해하기 위해 정적으로 링크된 "Hello, World" Go 버전을 예로 들어보겠습니다. 빈 폴더를 생성하고 다음 코드를 helloworld.go
라는 새 파일에 복사합니다:
package main
import "fmt"
func main() {
fmt.Println("hello, world!")
}
컴퓨터에 Go가 설치되어 있지 않아도 Docker 사용자는 문제없습니다. 다음 명령어를 실행하면 Docker는 Go 컴파일러를 포함한 이미지를 가져와 코드를 컴파일하고 정적으로 링크(즉, 독립적으로 실행 가능하게 함)한 뒤 해당 프로그램을 다시 폴더에 저장합니다:
docker container run --rm -v "$(pwd)":/usr/src/hello \
-w /usr/src/hello golang:1.9 go build -v
모든 것이 정상적으로 작동하면 동일한 폴더에 hello
라는 실행 가능한 프로그램(바이너리 파일)이 생성됩니다. 정적으로 링크된 프로그램은 실행 시 외부 파일 종속성이 없습니다. 즉, 이 정적으로 링크된 "Hello, World" 버전은 다른 파일 없이 컨테이너에서 실행할 수 있습니다. 다음 단계는 해당 프로그램을 tarball에 포함하는 것입니다:
tar -cf static_hello.tar hello
이제 프로그램이 tarball로 패키징되었으므로, docker import 명령어를 사용하여 이를 가져올 수 있습니다.
docker import -c "ENTRYPOINT [\"/hello\"]" - \
intheeast0305/ch7_static < static_hello.tar
$ docker container run intheeast0305/ch7_static
$ docker history intheeast0305/ch7_static
IMAGE CREATED CREATED BY SIZE
edafbd4a0ac5 11 minutes ago 1.824 MB
이 경우, 생성된 이미지가 작았던 이유는 두 가지입니다. 첫째, 생성된 프로그램의 크기가 약 1.8MB에 불과하며 운영 체제 파일이나 지원 프로그램을 포함하지 않았기 때문입니다. 이는 최소화된 이미지입니다. 둘째, 단 하나의 레이어만 존재합니다. 하위 레이어에 삭제되거나 사용되지 않는 파일이 포함되어 있지 않습니다. single 레이어(또는 flat) 이미지를 사용하는 단점은 레이어 재사용의 이점을 누리지 못한다는 점입니다. 모든 이미지가 충분히 작다면 문제가 되지 않을 수 있지만, 큰 스택이나 정적 링크를 지원하지 않는 언어를 사용하는 경우 오버헤드가 상당할 수 있습니다.
이미지 설계에서 평탄화된 이미지를 사용할지 여부를 포함해 모든 결정에는 트레이드오프가 존재합니다. 어떤 메커니즘을 사용해 이미지를 빌드하든, 사용자에게는 일관적이고 예측 가능한 방식으로 다른 버전을 식별할 수 있는 방법이 필요합니다.
7.4 Versioning best practices
생략...
'Docker' 카테고리의 다른 글
9 Public and privatesoftware distribution (0) | 2024.04.07 |
---|---|
8 Building images automatically with Dockerfiles (0) | 2024.04.04 |
6 Limiting risk with resource controls (0) | 2024.04.03 |
5 Single-host networking (0) | 2024.03.29 |
4. Working with storage and volumes (0) | 2024.03.26 |