💫효율성 관점에서 잘 만든 컨테이너 이미지란 무엇일까?
지난 글에서 보안 관점에서 잘 만든 컨테이너 이미지에 대해 살펴봤는데요.
이번엔 효율적인 컨테이너 이미지를 만드는 방법에 대해 알아보겠습니다.
먼저 효율성 관점에서 요구되는 사항을 정리해보겠습니다.
- 컨테이너 이미지 크기 경량화
- 컨테이너 이미지 레이어 최소화
- 컨테이너 이미지 레이어 최적화
이미지 크기 경량화는 그 뜻이 금방 이해가 되는데… 컨테이너 이미지 레이어는 뭘까요?
지금 낯설게 느껴져도 괜찮습니다. 제가 이제부터 컴팩트하게 짚어드리는 핵심 내용만 확인하시면 금방 감을 잡으실 테니까요.
📚컨테이너 이미지 레이어란?
컨테이너 이미지는 여러 개의 파일 시스템 상태가 하나씩 쌓여서 만들어집니다. 빠른 이해를 위해 아래와 같은 Dockerfile 예시 코드를 살펴볼게요.
FROM golang:1.23
WORKDIR /src
COPY <<EOF /src/main.go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go
기본적인 Go 애플리케이션을 실행하는 이미지가 정의된 Dockerfile인데요. 이렇게 빌드된 이미지는 아래와 같은 상태가 쌓이게 됩니다.
- 베이스 이미지의 초기 상태
- 1번 상태 위에 Go 애플리케이션 코드(/src/main.go)를 복사한 상태
- 2번 상태 위에 Go 애플리케이션을 빌드해서 결과물이 생성된 상태
이렇게 컨테이너 이미지를 구성하는 각각의 상태를 컨테이너 이미지 레이어라고 합니다. 정확히는 읽기 전용 파일 시스템 레이어가 쌓이는 건데, 이건 참고만 하시면 됩니다.
위에서 살펴본 상태들을 자세히 보면 한 가지 공통점이 있는데요. 바로 FROM
, COPY
, RUN
과 같은 Dockerfile의 지시어가 있는 곳에서 상태 변화, 즉 레이어가 생성된다는 것입니다.
컨테이너 이미지가 레이어로 구성된 이유는 아래와 같은 두 가지 이점이 있기 때문입니다.
-
컨테이너 이미지 빌드 속도 향상
- 컨테이너 이미지의 레이어는 재사용이 가능합니다.
- 그래서 만약 위에서 살펴본 예제 코드에서 다른 애플리케이션을 빌드하도록 수정되더라도, 기존 베이스 이미지의 레이어가 그대로 사용되어 컨테이너 이미지를 더 빨리 빌드할 수 있는 것입니다.
-
컨테이너 이미지 저장 공간 감소
- 이미 만들어진 컨테이너 이미지의 레이어는 재사용이 가능하기 때문에 컨테이너 이미지를 효율적으로 저장할 수 있습니다.
- 우리가 컨테이너 이미지를 빌드하거나 가져올 때(Pull), 해당 이미지를 구성하는 레이어 중에 이미 로컬에 존재하는 레이어는 재사용하고 새로 필요한 레이어만 로컬에 저장하면 되기 때문입니다.
좀 더 직관적으로 이해할 수 있도록 아까 살펴본 Dockefile 예제 코드를 Docker build 명령어로 이미지 빌드해봤습니다. 빌드가 끝나면 아래와 같은 메시지가 표시되는데요.
스크린샷에 빨간 상자로 하이라이트한 부분을 보면, 실제 예제 코드에서 명시된 지시어들이 실행되면서 각각의 레이어로 생성되는 것을 알 수 있습니다.
지금까지 컨테이너 이미지의 레이어에 대해 핵심 내용만 살펴봤는데요. 이제 컨테이너 이미지를 빌드할 때 효율성을 챙길 수 있는 팁을 알아보겠습니다.
바로 적용할 수 있는 실질적인 내용으로만 구성했으니 꼭 확인해보세요.
✨컨테이너 이미지의 효율성을 챙기는 팁
1️⃣ 멀티 스테이지 빌드 기법 활용하기
하나의 Dockerfile에서 여러 빌드 단계를 정의하여 최종 이미지 크기를 획기적으로 줄이는 기법을 멀티 스테이지 빌드 기법이라고 합니다.
가장 많이 활용되는 패턴은, 첫 번째 빌드 단계에서 애플리케이션 빌드나 의존성 설치를 완료하고, 두 번째 최종 단계에서는 최소한의 베이스 이미지 위에 애플리케이션 구동에 필요한 결과물만 복사하는 방식입니다.
즉, 빌드에만 필요한 대용량 패키지나 파일이 최종 이미지에 포함되지 않아 이미지의 크기가 대폭 감소하는 것이죠.
좀 더 쉽게 이해할 수 있도록 Dockerfile 예시 코드를 살펴보겠습니다. 아래 예시 코드는 Go 애플리케이션을 빌드하는 이미지에 멀티 스테이지 빌드 기법을 적용한 것인데요.
첫 번째 스테이지에서 Go 언어로 개발한 애플리케이션을 빌드하고, 두 번째(최종) 스테이지에서 또다른 베이스 이미지에 이전 스테이지에서 빌드된 바이너리만 복사하여 최종 이미지를 구성하고 있습니다.
# 첫 번째 스테이지: Go 바이너리 빌드 (이미지에 Builder라는 이름을 지정)
FROM golang:1.23 AS builder
WORKDIR /src
COPY <<EOF /src/main.go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go
# 최종 스테이지: 최소 이미지에 이전 스테이지에서 빌드된 바이너리만 복사
# (이전 스테이지에서 지정한 Builder 이미지의 /bin/hello 경로 내 데이터 복사)
FROM alpine:latest
COPY --from=builder /bin/hello /bin/hello
CMD ["/bin/hello"]
COPY --from
라는 Dockerfile 지시어가 낯설 수도 있는데요. 제가 알기 쉽게 차근차근 설명해보겠습니다.
이전 스테이지의 이미지(예제에서는 golang:1.23
베이스 이미지 위에 구성된 builder
라는 이름의 이미지)에서 Go 바이너리 빌드를 완료했죠?
builder
내 Go 바이너리 빌드의 결과물이 들어있는 경로(위 예제에서는 /bin/hello
)의 내용을 최종 스테이지의 alpine:latest
이미지 내 동일한 위치(/bin/hello
)에 복사(COPY
)한다는 의미입니다.
그러면 최종 이미지에는 이전 스테이지에서 빌드한 결과물만 받아오는 거죠.
이렇게 하면 최종 이미지 크기를 줄일 수 있으면서, 애플리케이션 동작에 불필요한 데이터는 제외하기 때문에 보안 공격 표면도 줄이는 효과도 같이 얻게 됩니다.
이렇게 효율과 보안성을 모두 챙길 수 있으니, 컨테이너 이미지를 만들 땐 멀티 스테이지 빌드 기법을 꼭 고려해봐야겠죠?
2️⃣ .dockerignore 파일 활용하기
.dockerignore
파일은 Docker 빌드 시 Docker 데몬으로 전송되는 빌드 컨텍스트(컨테이너 이미지가 빌드되는 환경)에서 제외할 파일과 디렉토리를 지정할 수 있습니다. 우리에게 익숙한 .gitignore
파일과 비슷한 양식으로 동작하는데요.
.dockerignore
파일로 . git
폴더나 개발 환경 전용 파일 등 이미지 빌드에 불필요한 데이터를 빌드 컨텍스트에서 제외시키면 더 빠르게 이미지를 빌드할 수 있죠.
또한 최종 이미지에 최소한의 필수 데이터만 포함된다는 것은 이미지의 보안성도 높일 수 있다는 뜻이므로, .dockerignore
파일 역시 적절히 활용하는 것이 중요합니다.
3️⃣ 컨테이너 이미지 레이어 최소화
컨테이너 이미지 레이어 최소화는 말 그대로 불필요한 레이어를 줄이는 것입니다. 컨테이너 이미지의 레이어가 많아질수록 빌드 속도는 느려지고 저장 공간은 늘어나기 때문입니다.
우리가 실제로 컨테이너 이미지를 정의하다보면 빌드 과정에서 다양한 명령어를 실행해야 합니다. 사용 가능한 패키지를 업데이트하는 apt-get update
명령어나 필요한 패키지를 설치하는 apt-get install
명령어, 불필요한 임시 데이터를 제거하는 rm
명령어까지 모두 Dockerfile의 RUN
지시어를 사용해야 하는데요.
이때 각 명령어를 별도의 RUN
지시어로 실행하면 그만큼 새로운 컨테이너 이미지 레이어를 생성하게 되는 것입니다. 컨테이너 이미지 최적화는 여기서부터 시작됩니다.
패키지 설치 과정에 필요한 명령어들이 아래와 같이 각각의 RUN
지시어로 정의되어있다고 가정해보겠습니다.
RUN apt-get update
RUN apt-get install -y mysql-client
RUN rm -rf /var/lib/apt
그러면 패키지 설치 과정에서 이미지 레이어가 총 2개 생성되는데요.
이때 이미지 레이어 최소화를 위해 순서대로 &&
로 묶어 하나의 RUN
지시어 안에서 정의할 수 있습니다.
RUN apt-get update && apt-get install -y mysql-client && rm -rf /var/lib/apt
이렇게 수정하면 패키지 설치 과정에 필요한 이미지 레이어를 1개로 줄일 수 있어 빌드 속도가 향상됩니다.
4️⃣ 자주 변경될 것 같은 지시어는 Dockerfile 아래에 정의하기
지금까지 알아본 이미지 레이어의 성질을 이용해서 컨테이너 이미지의 빌드 속도를 높이는 방법이 한 가지 더 있습니다.
바로 Dockerfile에 정의하는 지시어의 순서를 이미지 빌드에 유리하게 지정하는 것입니다.
Dockerfile의 지시어 순서와 컨테이너 이미지 빌드와 어떤 관계가 있냐고요? 방금까지 살펴봤던 이미지 레이어의 개념에 대해 약간 다른 관점에서 바라보면 금방 이해하실 수 있습니다.
이미지 레이어는 이미지 빌드 과정에서 Dockerfile의 지시어들이 실행될 때마다 새로 생성된다고 했죠? 이걸 바꿔서 이야기하면, 이미지를 구성하는 각 레이어는 이전 레이어 위에 새로운 파일 변경이 있을 때 이를 저장하는 역할을 수행하는 것입니다.
A-B-C-D로 이어지는 이미지 레이어를 가진 컨테이너 이미지가 있다고 가정해볼게요.
만약 D 레이어에 새로운 변경이 있어 이미지를 새로 빌드한다면, 기존에 가지고 있던 A, B, C 레이어를 그대로 재사용하고 D’라는 새로운 레이어만 생성하게 됩니다.
하지만 만약 C 레이어에 새로운 변경이 있다면, 기존의 A, B 레이어만 재사용하고 C’와 D’라는 새로운 레이어를 생성해야 하는 것입니다.
그래서 우리는 자주 변경될 것으로 예상되는 레이어의 지시어를 최대한 Dockerfile의 하단에 둬야 이미지 빌드 시간을 절약할 수 있습니다.
아까 ‘📚컨테이너 이미지 레이어란?‘에서 살펴봤던 예시를 다시 가져와보겠습니다.
- 베이스 이미지의 초기 상태 (
FROM
) - Go 애플리케이션 코드를 복사 (
COPY
) - Go 애플리케이션을 빌드해서 결과물이 생성 (
RUN
)
이렇게 3가지 레이어가 존재하는 이미지의 Dockerfile에 ‘Go 애플리케이션 구동에 필요한 패키지 설치’하는 지시어(RUN
)를 추가해야 한다고 가정해볼게요.
새로운 지시어는 위 지시어들 중 어느 위치에 두어야 할까요?
아마 설치해야 하는 패키지보다는 애플리케이션 코드가 더 자주 변경될 수 있기 때문에, 아래와 같은 순서로 Dockerfile 지시어를 정의해야 할 것입니다.
- 베이스 이미지의 초기 상태 (
FROM
) - Go 애플리케이션 구동에 필요한 패키지 설치 (
RUN
) - Go 애플리케이션 코드를 복사 (
COPY
) - Go 애플리케이션을 빌드해서 결과물이 생성 (
RUN
)
위 순서라면 Go 애플리케이션 코드가 변경되어 컨테이너 이미지를 새로 빌드하더라도 기존 1번과 2번 레이어는 재사용되기 때문에 더 빨리 이미지를 빌드할 수 있겠죠.
👍컨테이너 이미지의 보안과 효율성을 챙기면 얻을 수 있는 이점들
지금까지 컨테이너 이미지의 보안성과 효율성에 대해 살펴봤는데요. 이렇게 컨테이너 이미지를 ‘잘’ 만들면 어떤 이점이 있을까요? 아래와 같이 4가지로 정리할 수 있습니다.
-
보안 사고 발생 시 피해 감소
- 만약 보안 사고가 발생하더라도, 컨테이너 이미지 내에서의 권한이 높지 않고 공격 표면도 적기 때문에, 보안 사고의 피해를 줄일 수 있고 빠른 대응이 가능해집니다.
-
비용 절감 및 배포 효율성 증가
- 컨테이너 이미지가 가벼워져 배포 파이프라인 속도가 향상되고, 스토리지와 네트워크 비용도 절감할 수 있습니다.
- Auto Scaling이 될 때 인스턴스 부팅과 서비스 준비 시간이 감소하여 배포 효율성이 증가합니다.
-
개발팀과 운영팀의 업무 효율성 개선
- 개발팀은 로컬에서 빠르고 안정적으로 이미지를 빌드/테스트 가능합니다.
- 운영팀은 작고 예측 가능한 이미지 덕분에 배포와 관리가 용이해집니다.
-
클라우드 환경에서 효율적인 운영 가능
- Serverless 환경에서의 콜드 스타트 시간 단축에 도움이 되기 때문에 사용자 경험을 개선하고 비용 효율성을 높일 수 있습니다.
🔭마무리
컨테이너 이미지의 베이스 이미지 업데이트 등을 반영한 정기적인 재빌드와 Dockerfile 변경에 대한 리뷰는 이미지 품질을 꾸준히 높이는 데에 필수입니다.
이미 클라우드와 마이크로서비스 아키텍처가 대중화된 요즘, 컨테이너 이미지의 중요성은 아무리 강조해도 부족할 텐데요.
진행 중인 프로젝트에 컨테이너 이미지를 사용한다면, 효율성 관점에서 개선할 점은 없는지 한번 살펴보는 건 어떨까요?
IT 업계에서 효율성은 필수인 만큼, 개인의 성장뿐만 아니라 팀의 개발 효율성을 높일 수 있는 기회가 될 테니까요!