당신의 컨테이너 이미지가 더 작아야 하는 이유를 알고 계시나요?
컨테이너는 현대 애플리케이션 환경의 모든 곳에 있습니다. 개발자는 빌드, 레지스트리로 푸시, 일반적으로 애플리케이션이 컨테이너 내에서 작동하도록 하는 등 다양한 방식으로 이를 사용하고 있습니다.
이 모범 사례에서는 컨테이너 이미지의 세계, 특히 컨테이너 이미지를 더 작게 만드는 방법과 이것이 중요한 이유를 살펴봅니다. 그 과정에서 테스트에 사용할 수 있는 매우 작은 컨테이너 이미지를 빌드하기 위한 코드와 명령을 보여줍니다.
목차
1. 컨테이너 이미지란 무엇입니까?
2. 작은 이미지가 더 나은 이유는 무엇입니까?
3. 이미지 크기 줄이기 도구
4. 최소 이미지를 사용하여 구축
5. 이미지 없는 빌딩(Building)
6. 결론
1. 컨테이너 이미지란 무엇입니까?
제가 본 컨테이너 이미지의 가장 좋은 정의는 다음과 같습니다.
이미지는 컨테이너 없이 존재할 수 있지만 컨테이너가 존재하려면 이미지를 실행해야 합니다.
이것은 다소 원형이지만 여전히 정확합니다. 컨테이너 이미지는 애플리케이션 코드를 포함하고 컨테이너 런타임(예: Docker, rkt 및 podman)에 의해 “실행”되는 컴퓨팅 개체입니다. Kubernetes는 가장 널리 사용되는 컨테이너 오케스트레이션 시스템이지만 로컬 개발을 수행하는 경우 언급된 세 가지 다른 도구를 오케스트레이션에 사용할 수 있습니다.
이미지는 애플리케이션이 배포되는 방식을 정의합니다(예: 노출할 포트, 애플리케이션 런타임 시작(또는 진입점) 등).
2. 작은 이미지가 더 나은 이유는 무엇입니까?
더 작은 컨테이너 이미지에는 세 가지 주요 이점이 있습니다.
- 애플리케이션의 빌드 시간이 단축되었습니다. 빌드 시간에는 컨테이너 빌드뿐만 아니라 컨테이너를 레지스트리에 푸시하는 시간도 포함됩니다.
- 훨씬 작은 Memory footprint. 이미지가 작을수록 더 적은 메모리를 사용하게 됩니다. 이는 공용 클라우드 제공업체에서 운영할 때는 문제가 되지 않을 수 있지만 개발 목적으로 랩톱에서 작업할 때는 확실히 문제가 됩니다.
- 훨씬 더 작은 공격 표면과 더 적은 종속성(특히 컨테이너가 기본 이미지를 사용하지 않는 경우). 이로 인해 외부 라이브러리, 종속성 및 이미지 내부의 기타 항목과 관련하여 보안 표면이 줄어들고 설치 공간이 줄어듭니다.
더 작은 컨테이너 이미지는 일반적으로 내부에 더 적은 수의 구성 요소를 포함합니다. 즉, 이미지 내부에 비응용 코드의 양이 줄어듭니다. 일반적으로 컨테이너 이미지 내부에서 발견되는 가장 많은 양의 “비애플리케이션” 코드는 공유 라이브러리로 구성됩니다. 공유 라이브러리는 여러 응용 프로그램에서 사용하는(또는 사용할 가능성이 있는) 기능을 구현하는 소프트웨어 조각입니다. 공유 라이브러리를 사용한다는 것은 동일한 기능을 각각의 새로운 애플리케이션에서 반복해서 코딩할 필요가 없다는 것을 의미합니다. 일반적인 상황에서 공유 라이브러리는 공유 코드를 외부화하여 애플리케이션 바이너리를 더 작게 만들 수 있기 때문에 좋은 것입니다.

컨테이너에서 단일 애플리케이션을 실행할 때는 공유 라이브러리가 필요하지 않습니다. 결국 코드를 공유할 다른 응용 프로그램이 없습니다!
공유 라이브러리는 공간을 차지하며 해당 수명 주기는 빌드 프로세스의 일부로 독립적으로 관리되어야 합니다. 공유 라이브러리는 일반적으로 운영 체제와 함께 제공되며 이를 사용하는 응용 프로그램과 별도로 유지 관리됩니다.
컨테이너 내에서 작동할 때 이러한 일반적인 분리가 필요하지 않습니다. 공유 라이브러리를 분리하지 않는다는 것은 개발자가 애플리케이션을 실행하는 데 필요한 구성 요소만 포함하고 그 이상은 포함하지 않는다는 것을 의미합니다. 컨테이너 컨텍스트에서 이는 공유 라이브러리가 없음을 의미합니다.
3. 이미지 크기 줄이기 도구
컨테이너 이미지를 구축하는 전통적인 접근 방식은 사전 구축된 운영체제를 포함하고 사용하는 것입니다. 더 일반적으로 사용되는 기본 이미지 중 하나를 사용하는 경우 Ubuntu에 대해 다음과 같은 목록이 표시됩니다(여기에서는 가독성을 위해 여러 줄로 나뉩니다).
# podman images
REPOSITORY TAG IMAGE ID ...
docker.io/library/ubuntu latest 9873176a8ff5 ...
docker.io/library/fedora latest 055b2e5ebc94 ...
registry.fedoraproject.org/f33/fedora-toolbox latest af1f279fed20 ...
... CREATED SIZE
... 3 weeks ago 75.1 MB
... 7 weeks ago 184 MB
... 6 months ago 351 MB
보시다시피, Ubuntu의 경우 75MB에서 완전한 fedora-toolbox 이미지의 경우 무려 351MB에 이르기까지 이미지 크기가 매우 다양합니다. 이러한 이미지 중 하나가 실행될 때마다 애플리케이션을 다시 컴파일하고 이미지를 레지스트리에 푸시하는 경우 빌드 시간을 계산하지 않고 시작 및 로드하는 데 시간이 걸립니다.
이미지 크기를 줄이기 위한 두 가지 일반적인 선택이 있습니다. Alpine Linux와 Red Hat UBI(Universal Base Image)입니다.
Alpine Linux는 C 표준 라이브러리(libc)의 musl 구현과 과다한 도구를 포함하는 최소 커널인 BusyBox를 기반으로 합니다. musl libc를 사용한다는 것은 Alpine Linux에서 작동하도록 애플리케이션 코드를 다시 컴파일해야 한다는 것을 의미하며, 이는 애플리케이션의 소스 코드에 액세스할 수 없는 경우 문제가 됩니다.
Red Hat의 UBI는 여기서 또 다른 각도입니다. UBI는 개발자가 사용할 런타임 세트가 있는 표준화된 컨테이너 이미지 세트입니다.
Alpine Linux를 사용하면 일반적으로 이 목록에서 5.87MB로 훨씬 작은 이미지가 생성됩니다.
# podman images
REPOSITORY TAG IMAGE ID ...
registry.access.redhat.com/ubi8/ubi latest 8215cb84fa58 ...
registry.access.redhat.com/ubi8/ubi-minimal latest 3f32499d4f3a ...
docker.io/library/alpine latest d4ff818577bc ...
... CREATED SIZE
... 2 weeks ago 234 MB
... 2 weeks ago 105 MB
... 3 weeks ago 5.87 MB
Alpine과 UBI는 같은 질문(더 작은 이미지를 만드는 방법)을 다루지만 다른 출발점에서 접근합니다. Alpine은 매우 작은 코드베이스로 시작하여 필요한 도구만 추가합니다. 반면에 UBI는 더 큰 운영 체제에서 시작하여 가장 기본적인 부분으로 축소합니다.
4. 최소 이미지를 사용하여 구축
최소한의 이미지를 구축하려면 먼저 애플리케이션이 필요합니다. 이 블로그의 목적을 위해 C로 다음과 같은 매우 간단한 응용 프로그램을 작성했습니다. (사실 저는 Istio 테스트를 위해 수천 개의 컨테이너를 가동하고 싶어 작은 컨테이너 이미지가 필요한 동료를 위해 이 앱을 처음 작성했습니다.)
아마도 가장 주목할만한 것은 실제로 아무 것도하지 않는다는 것입니다! pause() 함수를 호출하고 신호를 기다립니다.
# more pausle.c
#include <unistd.h>
int main(void) {return pause(); }
예제로 사용하기에는 이상한 응용 프로그램처럼 보일 수 있지만 이미지 크기에 대한 제 요점을 설명하는 데 좋습니다. 매우 작기 때문에 애플리케이션은 컨테이너 크기에 거의 영향을 미치지 않습니다.
정상적인 상황에서 이 gcc 명령을 실행하여 몇 가지 최적화로 앱을 컴파일합니다.
# gcc -Os -fdata-sections -ffunction-sections -fipa-pta -W1,--gc-sections -W1,-O1 -W1,--as-needed -W1,--strip-all pausle.c -o pausle-dynamic
결과는 매우 작은 바이너리(단 15KB)입니다.
# ls -lh pausle-dynamic
-rwxr-xr-x. 1 root root 15K Jul 22 22:00 pausle-dynamic
이 응용 프로그램은 동적으로 연결되어 있습니다. 즉, 실행하려면 운영 체제에 공유 라이브러리가 필요합니다. ldd 명령을 실행하여 이를 확인할 수 있습니다.
# ldd pausle-dynamic
linux-vdso.so.1 (0x00007fffafbe3000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb193983000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb193b5d000)
이것을 컨테이너에 빌드하려면 공유 라이브러리가 포함된 기본 이미지를 사용해야 합니다. 저는 podman을 사용하고 있으며 빌드에는 Dockerfile이 어느 정도 보편적이기 때문에 사용하고 있습니다.
# more Dockerfile
FROM registry.access.redhat.com/ubi8/ubi-minimal
ADD pausle-dynamic /
CMD ["/pausle-dynamic"]
저는 RedHat UBI 최소 이미지(단계 1)를 사용하고 컨테이너가 시작될 때 이를 실행하는 명령과 함께 사전 컴파일된 애플리케이션을 추가합니다(단계 3).
# podman build --tag=pausle-dynamic .
STEP 1: FROM registry.access.redhat.com/ubi8/ubi-minimal
STEP 2: ADD pausle-dynamic /
--> 344589591c7
STEP 3: CMD ["/pausle-dynamic"]
STEP 4: COMMIT pausle-dynamic
--> 1f72538cf84
1f72538cf84c10ae525e545fb5596840f09d277eccaffae46f6b6a3815339c8b
podman 이미지 명령은 내 애플리케이션이 15KB만 추가하기 때문에 새 이미지가 유비 최소 기본 이미지보다 크지 않음(105MB)을 보여줍니다.
# podman images
REPOSITORY TAG IMAGE ID ...
localhost/pausle-dynamic latest 1f72538cf84c ...
registry.access.redhat.com/ubi8/ubi-minimal latest 3f32499d4f3a ...
docker.io/library/alpine latest d4ff818577bc ...
... CREATED SIZE
... About a minute ago 105 MB
... 4 weeks ago 105 MB
... 5 weeks ago 5.87 MB
이 이미지를 실행하면 사용 가능하고 실행 중임을 알 수 있습니다.
# podman run -d pausle-dynamic
5905d1ae4dc00b37f47ae4dlef4c7d99d8d5e1bd781da3b1decc436b10f5663b
# podman ps -a
CONTAINER ID IMAGE COMMAND ...
5905d1ae4dc0 localhost/pausle-dynamic:latest /pausle-dynamic ...
... CREATED STATUS
... 4 seconds ago Up 4 seconds ago
새 컨테이너 내에서 셸을 실행하고 내 바이너리를 볼 수 있습니다.
# podman exec -it 5905d1ae4dc0 /bin/bash
# ldd pausle-dynamic
linux-vdso.so.1 (0x00007ffd89762000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa65a658000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa65aa1d000)
애플리케이션은 내가 컴파일하고 컨테이너에 구축한 것과 동일합니다. 동적으로 연결되어 있기 때문에 운영 체제의 공유 라이브러리를 실행해야 합니다. 즉, 내 애플리케이션을 실행하려면 이러한 라이브러리가 포함된 기본 이미지가 필요합니다. 이것은 내 컨테이너의 크기를 증가시키므로 여전히 내가 원하는 만큼 최소화되지 않습니다.
5. 이미지 없는 빌딩(Building)
이 컨테이너 이미지를 훨씬 작게 만들기 위해 두 가지 작업을 수행할 수 있습니다. 내 코드를 정적으로 연결할 수 있습니다. 즉, 더 나은 설명을 위해 내 애플리케이션이 공유 라이브러리를 바이너리로 “bundles in”합니다.
이 명령을 실행하여 정적으로 연결합니다.
# gcc -Os -s static -ffunction-sections -fipa-pta -W1,--gc-sections pausle.c -o pausle-dynamic
# strip pausle-static
# ls-lh pausle-static
-rwxr-xr-x. 1 root root 697K Jul 22 22:31 pausle-static
ls 명령은 라이브러리가 응용 프로그램에 번들로 제공되기 때문에 결과 응용 프로그램 바이너리가 697KB(동적으로 연결된 응용 프로그램보다 상당히 큼)를 보여줍니다.
이제 ldd 명령을 실행하여 공유 라이브러리를 표시하면 실행 파일이 동적이지 않다는 메시지가 나타납니다.
# ldd pausle-static
not a dynamic executable
Dockerfile에서 기본 이미지를 사용하지 않음을 나타내기 위해 특수 no‑op 키워드 scratch를 사용하고 있습니다.
# more Dockerfile
FROM scratch
ADD pausle-static /
CMD ["/pausle-static"]
이제 podman build 명령을 실행하여 이전과 같은 방식으로 이미지를 빌드합니다.
# podman build --tag=pausle-static .
STEP 1: FROM scratch
STEP 2: ADD pausle-static /
--> 7fb16e85314
STEP 3: CMD ["/pausle-static"]
STEP 4: COMMIT pausle-static
--> f7f7c833975
f7f7c83397545aef51e0ad665def03040d5a06adf50651133184864bd1adaed4
컨테이너는 동적 실행 파일과 똑같은 방식으로 빌드되지만 결과 이미지는 동적 이미지보다 훨씬 작습니다. 716KB로 정적으로 컴파일된 바이너리 자체(697KB)보다 약간 큽니다.
# podman images
REPOSITORY TAG IMAGE ID ...
localhost/pausle-static latest f7f7c8339754 ...
localhost/pausle-dynamic latest 1f72538cf84c ...
... CREATED SIZE
... 2 minutes ago 716 kB
... 20 minutes ago 105 MB
컨테이너를 초기화하고 실행 중인지 확인합니다.
# podman run -d pausle-static
1748d59be199a797aa01f569b10445787d3f40439fb0b404b22e4226f4f44e09
# podman ps -a
CONTAINER ID IMAGE COMMAND ...
1748d59be199 localhost/pausle-static:latest /pausle-static ...
... CREATED STATUS
... 5 seconds ago Up 6 seconds ago
셸을 실행하거나 컨테이너 내에서 ls 명령을 실행하려고 하면 컨테이너에 다른 응용 프로그램이 없다는 오류 메시지가 나타납니다. 컨테이너에 운영 체제나 기본 이미지가 포함되어 있지 않기 때문입니다.
# podman exec -it 1748d59be199 /bin/bash
Error: executable file `/bin/bash` not found in $PATH: No such file or directory: OCI not found
# podman exec -it 1748d59be199 /bin/ls
Error: executable file `/bin/ls` not found in $PATH: No such file or directory: OCI not found
6. 결론
작은 컨테이너 이미지를 빌드하는 것은 모든 종류의 시나리오(예: 개발 및 테스트)에서 유용합니다. 이미지를 원격 레지스트리로 푸시하는 데 걸리는 시간을 포함하여 새 이미지의 빌드 시간을 크게 줄입니다.
앞서 언급했듯이 더 작은 컨테이너 이미지(특히 기본 이미지를 사용하지 않는 경우)도 훨씬 더 작은 공격 표면과 더 적은 종속성을 가지므로 외부 라이브러리, 종속성 및 이미지 내부의 기타 항목 측면에서 차지하는 공간이 줄어듭니다. 대부분의 경우 작은 이미지를 만드는 것은 깔끔함과 대칭적인 느낌을 전달하기 때문에 매우 멋진 일입니다!
댓글을 달려면 로그인해야 합니다.