90DaysOfDevOps (Day 19)

고태규·2025년 9월 27일
0

DevOps

목록 보기
18/21
post-thumbnail

해당 스터디는 90DaysOfDevOps
https://github.com/MichaelCade/90DaysOfDevOps
를 기반으로 진행한 내용입니다.

Day 19 - Building Efficient and Secure Docker Images with Multi-Stage Builds


1. 기존 이미지 빌드 방식의 문제점


기존 방식으로 빌드된 도커 이미지는 불필요한 요소들을 너무 많이 포함하고 있다.

이상적인 이미지는 실제 서비스에 필요한 어플리케이션과 런타임만 포함되는 것이다.

하지만, 최적화되지 않은 일반적인 이미지는, 어플리케이션, 런타임 외에도 컴파일러, 소스 코드, 테스트 코드, 각종 의존성 라이브러리, 빌드 로그 등 실행에 필요 없는 요소들이 포함된다.

이러한 불필요한 요소들은 다음과 같은 문제들을 야기시킨다.

1-1. 보안

  • 이미지에 포함된 요소가 많을수록, 공격자가 파고들 수 있는 지점 (공격 표면적) 이 넓어짐.
  • 사용되지 않는 라이브러리의 취약점이 전체 시스템의 위협이 될 수 있음.

1-2. 성능

  • 이미지의 크기가 크면, 다운로드, 업로드, 배포에 더 많은 시간이 소요됨.
  • 개발속도와 CI/CD 파이프라인의 효율성을 저하시키는 원인

1-3. 효율성

  • 불필요하게 큰 이미지는 저장공간을 낭비하고, 전체적인 개발 및 운영 프로세스를 비효율적으로 만듦.


2. 멀티스테이지 빌드


이러한 문제점을 해결하기 위해 제시된 방법이 멀티스테이지 빌드이다.

멀티스테이지 빌드 (Multi-Stage Build)는 하나의 Dockerfile 안에서 이미지 빌드 과정을 여러 단계 (Stage)로 나누어, 최종 이미지를 최적화하는 기법이다.

핵심은 코드를 컴파일하는 빌드 환경과 어플리케이션을 실행하는 런타임 환경을 분리하는 것으로, 컨테이너 이미지를 만들면서, 최종 컨테이너에는 필요 없는 환경 (빌드, 테스트 등) 을 제거한다.

멀티스테이지 빌드를 사용하면 해당 문제들을 해결할 수 있다.

  • 이미지 크기 감소

    • 빌드에만 사용된 도구와 소스코드를 최종 이미지에서 제거하기 때문에 이미지의 크기를 약 90% 줄일 수 있음.
  • 보안 강화

    • 최종 이미지에는 실행에 필수적인 파일만 남기므로, 공격 표면적이 줄어들어 보안성이 향상됨.
  • 편리한 관리

    • 여러 개의 Dockerfile이나 셸 스크립트를 관리할 필요 없이, 하나의 Dockerfile에서 모든 빌드 과정을 깔끔하게 관리 가능함.
  • 동일한 문법 사용

    • FROM, COPY, RUN 등 기존 Dockerfile 문법을 그대로 사용하므로 적용에 문제가 없음.

멀티스테이지 빌드 Dockerfile 예시

# Base Image
FROM alpine:latest AS base

# First Image
FROM ubuntu:latest AS first
RUN echo "Hello" > /hello

# Second Image
FROM debian:latest AS second
RUN echo "conference" > /conference

# Final Image
FROM base
COPY --from=first /hello /hello
COPY --from=second /conference /conference
CMD cat /hello && cat /conference

1. 베이스 이미지 (base) 스테이지

# Base Image
FROM alpine:latest AS base
  • 가벼운 리눅스 배포판인 alpine:latest 이미지를 가져와 AS base 라는 이름으로 지정
  • 해당 스테이지는 다른 빌드 과정에 참여하기보다, 최종 이미지가 사용할 기반(base)가 됨.

2. 첫 번째 이미지 (first) 스테이지

# First Image
FROM ubuntu:latest AS first
RUN echo "Hello" > /hello
  • 두번째 스테이지는 ubuntu:latest 이미지를 기반으로 하며 AS first 라는 이름으로 지정
  • 해당 스테이지는 RUN 명령어를 사용하여 'Hello"라는 텍스트가 담긴 /hello 파일을 생성

3. 두 번째 이미지 (second) 스테이지

# Second Image
FROM debian:latest AS second
RUN echo "conference" > /conference
  • 세 번째 스테이지는 debian:latest 이미지를 사용하며 AS second 로 이름을 지정
  • 해당 스테이지는 RUN을 통해 "conference" 텍스트가 담긴 /conference 파일을 생성

4. 최종 이미지 (Final Image) 스테이지

# Final Image
FROM base
COPY --from=first /hello /hello
COPY --from=second /conference /conference
CMD cat /hello && cat /conference

해당 스테이지가 멀티스테이지 빌드의 핵심 포인트

  • FROM base :
    빌드를 ubuntu나 debian이 아닌 처음에 정의했던 가벼운 alpine 이미지, 즉 base에서 새로 시작

  • COPY --from=first /hello /hello :
    COPY --from 구문을 사용하여 first 스테이지인 Ubuntu 환경에서 만들었던 /hello 파일만 가져옴. (Ubuntu OS 전체가 아닌 파일 하나만 복사)

  • COPY --from=second /conference /conference :
    COPY --from 구문을 사용하여 second 스테이지인 Debian 환경에서 만들었던 /conference 파일만 가져옴. (Debian OS 전체가 아닌 파일 하나만 복사)

  • CMD cat /hello && cat /conference :
    cat 명령어로 두 파일의 내용을 모두 출력하도록 설정하여, 다른 환겨에서 만들어진 결과물들이 최종 이미지에 성공적으로 합쳐졌음을 보여주는 과정

결론적으로, 해당 Dockerfile은 각기 다른 운영체제 환경에서 필요한 파일을 생성한 뒤, 최종적으로는 아주 가벼운 베이스 이미지 (Alpine)에 해당 결과물들만 선택적으로 합쳐 최적화된 최종 이미지를 만드는 과정을 보여주는 Dockerfile이다.


3. 일반 빌드와 멀티스테이지 빌드


3-1. 일반 Dockerfile

FROM golang:1.19.5-alpine3.17
EXPOSE 9001
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/api .
CMD ["/app/api"]
  • golang 이미지를 기반으로 시작해서, 소스코드를 복사하고, 의존성을 다운로드, 코드 컴파일, 마지막으로 해당 이미지에서 어플리케이션을 실행하는 Dockerfile
  • 해당 방식을 사용시, 최종 실행 파일 (api) 외에도 Go 컴파일러 및 관련 도구, 전체 소스 코드, 관련 의존성 모듈 들이 모두 포함되어 이미지 크기가 매우 커짐.

3-2. 멀티스테이지 Dockerfile

# 스테이지 1: 빌드 환경 
FROM golang:1.19.5-alpine3.17 AS builder
WORKDIR /build
EXPOSE 9001
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app .

# 스테이지 2: 런타임 환경 
FROM alpine:3.17
COPY --from=builder /app /bin/app
CMD ["/bin/app"]
  • 스테이지 1: 빌드 환경

    • FROM golang... AS builder: 첫 번째 스테이지를 builder라는 이름으로 지정
    • 해당 스테이지를 사용하여 코드를 컴파일하여 실행파일인 /app을 만듦 (일반적인 Dockerfile과 동일한 작업 수행)
  • 스테이지 2: 최종 이미지 (런타임 환경)

    • FROM alpine:3.17: Go 컴파일러가 없는, 매우 가벼운 alpine 이미지에서 완전히 새로 시작
    • COPY --from=builder /app /bin/app: 이전 builder 스테이지에서 만들었던 결과물 (실행 파일 /app)만 정확히 가져와 최종 이미지에 복사

해당 방식을 사용하게 되면, builder 스테이지에 있던 Go 컴파일러, 소스 코드, 의존성 모듈은 모두 버려지게 되고, 최종 이미지는 alpine 베이스와 실행파일만 구성되게 된다.

따라서, 이미지 크기는 일반 Dockerfile보다 훨씬 가벼워지고, 실행에 필요한 최소 요소만 포함되므로 보안성이 강화되게 된다.


0개의 댓글