Docker와 멀티스테이지 빌드를 이용한 경량 Go 웹 앱 구축
Wenhao Wang
Dev Intern · Leapcell

소스에서 컨테이너로 Go 웹 앱 배포 최적화
소개
마이크로서비스와 클라우드 네이티브 애플리케이션의 빠른 발전 속도 속에서 효율적인 배포 전략은 무엇보다 중요합니다. Go는 정적 컴파일 바이너리와 강력한 성능으로 웹 서비스를 구축하는 데 인기 있는 선택지가 되었습니다. 하지만 Go 애플리케이션을 컴파일하여 Docker 이미지에 넣는 것만이 항상 가장 효율적인 접근 방식은 아닙니다. 개발 환경에는 최종 컨테이너 이미지 크기를 부풀리는 수많은 빌드 도구, 종속성 및 불필요한 파일이 포함될 수 있으며, 이는 배포 시간 증가, 리소스 소비 증가, 공격 표면 확대로 이어집니다. 이 문서는 Docker의 강력한 기능, 특히 멀티스테이지 빌드를 활용하여 Go 웹 애플리케이션 패키징 프로세스를 간소화하고 원시 소스 코드에서 생산 준비가 된 경량 컨테이너로 변환하는 방법을 살펴봅니다. 보안과 배포 효율성을 모두 향상시키는 최소한의 이미지를 구축하는 실질적인 측면에 대해 깊이 있게 다룰 것입니다.
핵심 개념 및 구현
실제 예제를 살펴보기 전에 논의의 기반이 되는 핵심 개념에 대한 공통된 이해를 확립해 봅시다.
- Docker: Docker는 컨테이너화를 사용하여 애플리케이션의 배포, 확장 및 관리를 자동화하는 오픈 소스 플랫폼입니다. 컨테이너는 애플리케이션과 모든 종속성을 단일 유닛으로 패키징하여 다양한 환경에서 일관된 동작을 보장합니다.
- 컨테이너 이미지: 컨테이너 이미지는 코드, 런타임, 시스템 도구, 시스템 라이브러리 및 설정을 포함하여 애플리케이션을 실행하는 데 필요한 모든 것을 포함하는 가볍고 독립적인 실행 가능한 소프트웨어 패키지입니다.
- Dockerfile: Dockerfile은 사용자가 이미지를 조립하기 위해 명령줄에서 호출할 수 있는 모든 명령을 포함하는 텍스트 문서입니다. Docker는 Dockerfile의 지침을 읽음으로써 이미지를 자동으로 빌드합니다.
- 멀티스테이지 빌드: Docker의 비교적 최신 기능인 멀티스테이지 빌드를 사용하면 Dockerfile에서 여러
FROM
문을 사용할 수 있습니다. 각FROM
지침은 다른 기본 이미지를 사용할 수 있으며, 한 단계에서 다른 단계로 아티팩트를 선택적으로 복사할 수 있습니다. 이는 빌드 시간 종속성과 런타임 종속성을 분리하여 이미지 크기를 최적화하는 데 매우 강력합니다. - Go 정적 컴파일: Go 컴파일러는 기본적으로 정적으로 링크된 바이너리를 생성합니다. 즉, 필요한 모든 라이브러리가 실행 파일에 직접 번들로 포함됩니다. 이를 통해 대부분의 외부 런타임 종속성이 필요 없어 배포가 크게 단순화됩니다.
단일 스테이지 빌드의 문제점
Go 애플리케이션에 대한 일반적인 단일 스테이지 Dockerfile을 고려해 봅시다.
# Go 애플리케이션을 위한 단일 스테이지 빌드 FROM golang:1.22 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o mywebapp . EXPOSE 8080 CMD ["./mywebapp"]
이것은 작동하지만 결과 이미지의 크기가 상당히 클 것입니다. golang:1.22
이미지는 빌드 프로세스 중에만 필요하고 런타임에는 필요하지 않은 컴파일러, SDK 및 다양한 도구를 포함하는 상당한 개발 환경입니다. 최종 이미지에는 이러한 불필요한 모든 것이 포함되어 크기와 잠재적인 보안 취약성이 증가합니다.
멀티스테이지 빌드의 강력함
멀티스테이지 빌드는 실행 파일이 생성된 후 빌드 환경을 폐기할 수 있도록 하여 이 문제를 직접 해결합니다. 다음은 멀티스테이지 빌드를 사용하여 이전 Dockerfile을 리팩터링하는 방법입니다.
main.go
에 간단한 Go 웹 애플리케이션이 있다고 가정해 보겠습니다.
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from Go Web App!") }) log.Println("Server listening on port 8080") log.Fatal(http.ListenAndServe(":8080", nil)) }
그리고 go.mod
파일:
module mywebapp
go 1.22
이제 최적화된 Dockerfile
을 만들어 보겠습니다.
# 스테이지 1: Go 애플리케이션 빌드 FROM golang:1.22-alpine AS builder WORKDIR /app # Docker 레이어 캐싱 활용을 위해 먼저 go.mod 및 go.sum 복사 COPY go.mod go.sum ./ # Go 모듈 다운로드 - go.mod/go.sum이 변경되지 않으면 이 단계는 캐시됨 RUN go mod download # 나머지 애플리케이션 소스 코드 복사 COPY . . # Go 애플리케이션 빌드 # CGO_ENABLED=0: CGO를 비활성화하여 완전히 정적인 바이너리를 생성하도록 보장 # GOOS=linux: 컨테이너 내부의 Linux 운영 체제를 명시적으로 대상으로 함 # -a: 모든 패키지의 재빌드를 강제함 (C 종속성이 변경된 경우 유용) # -installsuffix cgo: CGO가 활성화된 경우 설치된 파일 뒤에 접미사를 추가함 # -ldflags "-s -w": 최종 바이너리 크기를 더욱 줄이기 위해 디버깅 정보(-s)와 심볼 테이블(-w)을 제거함. RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "-s -w" -o mywebapp . # 스테이지 2: 최소 런타임 이미지 생성 # scratch 또는 alpine과 같은 최소 기본 이미지 사용 FROM alpine:latest # 최종 이미지의 작업 디렉토리 설정 WORKDIR /app # 빌더 스테이지에서 컴파일된 바이너리만 복사 # --from=builder는 소스 스테이지를 지정함 COPY /app/mywebapp . # 애플리케이션이 수신 대기하는 포트 노출 EXPOSE 8080 # 애플리케이션을 실행할 명령 정의 CMD ["./mywebapp"]
이 멀티스테이지 Dockerfile을 자세히 살펴보겠습니다.
스테이지 1: builder
FROM golang:1.22-alpine AS builder
:golang:1.22-alpine
기본 이미지를 사용합니다.alpine
변형을 사용하면 빌드 스테이지에서도 더 작은 기본 이미지를 제공합니다. 쉽게 참조할 수 있도록 이 스테이지를builder
라고 명명합니다.WORKDIR /app
: 컨테이너 내에서/app
을 작업 디렉토리로 설정합니다.COPY go.mod go.sum ./
및RUN go mod download
: 이것은 중요한 최적화입니다. 모듈 파일만 먼저 복사하고go mod download
를 실행하면 Docker가 이 레이어를 캐시할 수 있습니다.go.mod
또는go.sum
파일이 변경되지 않으면 후속 빌드에서 이 캐시된 레이어를 재사용하여 빌드 프로세스를 가속화합니다.COPY . .
: 나머지 애플리케이션 소스 코드를 빌드 환경으로 복사합니다.RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "-s -w" -o mywebapp .
: 여기서 Go 애플리케이션이 컴파일됩니다.CGO_ENABLED=0
: 이것이 중요합니다.cgo
를 비활성화하여 Go 컴파일러가 C 라이브러리 종속성 없이 진정한 정적 바이너리를 생성하도록 보장합니다. 이는 바이너리를 매우 이식 가능하게 만듭니다.GOOS=linux
: Go 컴파일러에게 Linux(Docker 컨테이너 내부의 운영 체제)용 바이너리를 빌드하도록 명시적으로 지시합니다.-a -installsuffix cgo
: 이 플래그는CGO_ENABLED=0
과 함께 자주 사용되어 완전한 정적 빌드를 보장합니다.-ldflags "-s -w"
: 이러한 플래그는 링킹 중에 최종 실행 파일의 크기를 줄이는 데 사용됩니다.-s
: 심볼 테이블과 디버깅 정보를 생략합니다.-w
: DWARF 심볼 테이블을 생략합니다. 이러한 플래그는 바이너리 크기를 크게 줄일 수 있습니다.
-o mywebapp .
: 진입점(.
)을 컴파일하고 실행 파일을mywebapp
로 출력합니다.
스테이지 2: 최종 런타임 이미지
FROM alpine:latest
: 매우 작은 기본 이미지인alpine:latest
로 전환합니다.CGO_ENABLED=0
을 사용하는 Go 애플리케이션의 경우scratch
(빈 이미지)조차도 궁극적인 최소화를 위해 사용할 수 있습니다.alpine
은 종종 최소한의libc
와 셸을 제공하여 디버깅이나 사소한 도구에 유용할 수 있기 때문에 선택됩니다.WORKDIR /app
: 최종 이미지의 작업 디렉토리를 설정합니다.COPY --from=builder /app/mywebapp .
: 이것이 멀티스테이지 빌드의 마법입니다. 이전builder
스테이지에서 컴파일된 바이너리mywebapp
만 새롭고 최소한의 기본 이미지로 복사합니다.builder
스테이지의 모든 Go SDK, 빌드 도구 및 소스 코드는 그대로 유지됩니다.EXPOSE 8080
: 컨테이너가 런타임에 포트 8080을 수신 대기함을 Docker에 알립니다.CMD ["./mywebapp"]
: 컨테이너가 시작될 때 실행될 명령을 지정하여 컴파일된 Go 웹 애플리케이션을 실행합니다.
이미지 빌드 및 실행
이 Docker 이미지를 빌드하려면 main.go
, go.mod
및 Dockerfile
이 포함된 디렉토리로 이동한 다음 다음을 실행합니다.
docker build -t mywebapp:latest .
빌드가 완료된 후 단일 스테이지 빌드에서 생성될 것보다 최종 이미지 크기가 훨씬 작다는 것을 알게 될 것입니다. docker images
로 확인할 수 있습니다.
애플리케이션을 실행하려면:
docker run -p 8080:8080 mywebapp:latest
그런 다음 브라우저에서 http://localhost:8080
을 열어 "Hello from Go Web App!"을 볼 수 있습니다.
애플리케이션 시나리오
이 멀티스테이지 빌드 접근 방식은 다양한 시나리오에 이상적입니다.
- 프로덕션 배포: 프로덕션 환경에 완벽한 매우 최적화된 작은 이미지를 생성하여 배포 시간을 단축하고 시작 속도를 향상시킵니다.
- CI/CD 파이프라인: CI/CD 워크플로에 원활하게 통합하여 모든 커밋마다 빠르고 효율적인 이미지 생성을 보장합니다.
- 리소스 제약 환경: 모든 메가바이트가 중요한 엣지 디바이스 또는 환경에 유용합니다.
- 보안 중심: 본질적으로 작은 이미지는 잠재적으로 취약점을 가질 수 있는 라이브러리와 도구가 적기 때문에 공격 표면이 작습니다.
결론
멀티스테이지 Docker 빌드를 채택함으로써 개발자는 Go 웹 애플리케이션 소스 코드를 매우 경량화되고 안전하며 생산 준비가 된 컨테이너 이미지로 효율적으로 변환할 수 있습니다. 이 패턴은 빌드 시간 종속성과 런타임 요구 사항을 효과적으로 분리하여 이미지 크기를 크게 줄이고 배포 속도를 높이며 보안 상태를 향상시킵니다. 소스 코드에서 컴팩트한 컨테이너로의 여정은 단순히 편리함에 관한 것이 아닙니다. 이는 최신 클라우드 네이티브 Go 애플리케이션을 위한 근본적인 최적화이며, 이를 진정으로 민첩하고 성능이 뛰어나게 만듭니다. Go 서비스에 대한 멀티스테이지 빌드를 활용하는 것은 효율적인 컨테이너화의 초석입니다.