Docker&K8s

CH_03_02_컨테이너의 이식성(Portability)

dding-shark 2025. 7. 23. 14:03
728x90

3.2 컨테이너의 이식성(Portability)

컨테이너 기술의 가장 강력한 장점 중 하나는 바로 이식성(Portability)입니다. 이식성이란, Docker와 같은 컨테이너 런타임 환경이 설치된 곳이라면 온프레미스, 로컬 개발 환경, 클라우드 등 어디에서든 컨테이너가 동일하게 동작할 것을 기대할 수 있는 재현성을 의미합니다.

하지만 여기서 한 가지 의문이 생길 수 있습니다. 컨테이너는 하드웨어를 가상화하는 가상 머신(VM)과 달리, 호스트(Host) OS의 커널을 공유합니다. 그렇다면 어떻게 호스트 환경과 무관하게 동일한 동작을 보장할 수 있을까요?

올바른 지적입니다. 이러한 구조적 특징 때문에 컨테이너의 이식성은 완벽하지 않으며, 몇 가지 중요한 예외 상황이 존재합니다.


3.2.1 한계 1: 커널 및 CPU 아키텍처의 차이

컨테이너가 호스트 OS의 커널 리소스를 공유하므로, 컨테이너는 특정 CPU 아키텍처(e.g., x86-64, ARM64)와 OS(e.g., Linux, Windows)를 전제로 빌드됩니다.
가장 대표적인 예로, x86-64(Intel/AMD) 아키텍처를 기반으로 빌드된 컨테이너 이미지는 ARM64 아키텍처를 사용하는 Apple Silicon(M1/M2/M3) CPU에서 기본적으로 실행될 수 없습니다. CPU가 이해할 수 있는 명령어 세트가 다르기 때문입니다.
이러한 문제를 해결하기 위해 Docker는 BuildKit이라는 차세대 빌드 엔진을 도입했습니다. BuildKit을 사용하면 docker buildx와 같은 확장된 명령어를 통해 여러 CPU 아키텍처에서 동시에 실행 가능한 멀티 플랫폼 이미지를 이전보다 손쉽게 생성할 수 있습니다. (BuildKit에 대한 자세한 내용은 10.5장에서 다룹니다.)


3.2.2 한계 2: 동적 라이브러리 의존성

애플리케이션이 사용하는 라이브러리 또한 이식성을 저해하는 주요 원인이 될 수 있습니다. 특히 C/C++, Go, Rust, Java 등이 네이티브 라이브러리를 동적 링크(Dynamic Linking) 방식으로 사용할 때 문제가 발생합니다.

구분 정적 링크 (Static Linking) 동적 링크 (Dynamic Linking)
연결 시점 빌드 시점에 라이브러리 코드를 바이너리에 포함 실행 시점에 OS에 존재하는 라이브러리를 호출하여 연결
장점 의존성 문제에서 자유로워 이식성이 높음 바이너리 크기가 작고, 메모리를 효율적으로 사용
단점 바이너리 크기가 커지고, 라이선스 문제 발생 가능 실행 환경에 해당 라이브러리가 없으면 실행 불가
동적 링크를 사용하는 경우, 빌드 환경(예: CI 서버)에는 존재하던 라이브러리가 컨테이너 이미지의 OS에는 존재하지 않아 애플리케이션 실행에 실패하는 상황이 발생할 수 있습니다.

예를 들어, glibc 라이브러리 기반의 CI 환경에서 빌드한 바이너리를 musl libc 기반의 경량화된 알파인(Alpine) 리눅스 컨테이너에서 실행하면 호환성 문제로 동작하지 않습니다.

이 문제를 피하기 위해서는, 모든 의존 라이브러리를 정적 링크하여 실행 파일 하나에 포함시키거나, 실행 환경 컨테이너에 필요한 라이브러리(glibc 등)를 직접 설치해야 합니다. 매번 실행 환경에 맞춰 빌드 설정을 바꾸는 것은 비효율적이므로, 약간의 단점(파일 크기 증가 등)을 감수하더라도 정적 링크를 우선적으로 고려하는 것이 안정적인 이식성 확보에 유리합니다.


3.2.3 멀티스테이지 빌드를 통한 이식성 및 효율성 확보

도커는 위에서 언급한 문제들, 특히 빌드 환경과 실행 환경의 차이에서 오는 문제를 해결하기 위해 멀티스테이지 빌드(Multi-stage Builds) 라는 강력한 해법을 제시합니다.
멀티스테이지 빌드는 빌드용 컨테이너와 실행용 컨테이너를 하나의 Dockerfile 내에서 분리하여, 최종 이미지의 크기를 최적화하고 이식성과 보안을 크게 향상시킵니다.

멀티스테이지 빌드가 필요한 이유

  • 불필요하게 큰 이미지: 과거에는 JDK, Maven, Node.js 등 빌드에만 필요한 무거운 도구들이 최종 이미지에 그대로 남아 용량을 차지했습니다.
  • 보안 취약점: 실제 서비스 운영에 필요 없는 빌드 도구들이 이미지에 포함되면서 잠재적인 보안 공격의 대상이 되었습니다.

작동 원리

  1. 여러 빌드 단계 정의: Dockerfile에 여러 개의 FROM 명령어를 사용하여 각기 다른 빌드 단계를 만듭니다. 각 단계는 독립적이며, AS <이름>으로 별칭을 부여할 수 있습니다.
  2. 결과물만 선택적 복사: COPY --from=<이전 단계 이름> 명령어를 통해, 이전 빌드 단계에서 생성된 결과물(컴파일된 바이너리, .jar 파일 등)만을 다음 단계로 가져옵니다.
  3. 최종 단계로 이미지 생성: Docker는 가장 마지막 FROM 단계만을 사용하여 최종 이미지를 생성합니다. 중간 빌드 단계의 도구와 파일들은 최종 이미지에 포함되지 않습니다.

멀티스테이지 빌드 예시 (Java Spring Boot)

# =================================================================
# 1단계: 'builder' 스테이지 - 애플리케이션 빌드
# Maven과 JDK를 사용하여 소스 코드를 .jar 파일로 컴파일합니다.
# =================================================================
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests


# =================================================================
# 2단계: 최종 스테이지 - 애플리케이션 실행
# JRE만 포함된 경량 이미지를 기반으로 합니다.
# =================================================================
FROM openjdk:17-slim
WORKDIR /app

# 'builder' 단계에서 생성된 .jar 파일만 복사해옵니다.
COPY --from=builder /app/target/*.jar app.jar

# 컨테이너 시작 시 애플리케이션을 실행합니다.
ENTRYPOINT ["java", "-jar", "app.jar"]

장점 요약

  • 최적화된 이미지 크기: 빌드 도구를 제거하고 실행에 필요한 최소한의 파일만 포함하여 이미지 크기를 획기적으로 줄입니다.
  • 향상된 보안: 공격에 노출될 수 있는 불필요한 도구들을 제거하여 보안을 강화합니다.
  • 간결한 Dockerfile: 별도의 스크립트 없이 하나의 파일에서 빌드와 배포 이미지를 모두 관리하여 유지보수가 용이합니다.
728x90