Docker Buildx Bake 마스터하기: 멀티-플랫폼 빌드, 테스트 등
Docker Buildx Bake 마스터하기: 멀티-플랫폼 빌드, 테스트 등 (Mastering multi-platform builds, testing, and more with Docker Buildx Bake)
Buildx Bake로 선언적 구성을 사용해 Docker 빌드와 테스트를 자동화하는 방법을 설명하는 가이드예요.
출처: 문서
본문
이 가이드는 Docker Buildx Bake를 사용해 이미지 빌드, 테스트, 빌드 아티팩트 생성을 단순화하고 자동화하는 방법을 보여줘요. 선언적 docker-bake.hcl 파일에 빌드 구성을 정의하면 수동 스크립트를 없애고 복잡한 빌드, 테스트, 아티팩트 생성을 위한 효율적인 워크플로우를 가능하게 해요.
가정 (Assumptions)
이 가이드는 다음에 익숙하다고 가정해요:
사전 요구사항 (Prerequisites)
- 머신에 최신 버전의 Docker가 설치되어 있어요.
- 저장소 클론을 위해 Git이 설치되어 있어요.
- containerd 이미지 저장소를 사용하고 있어요.
소개 (Introduction)
이 가이드는 Docker Buildx Bake가 빌드 및 테스트 워크플로우를 간소화할 수 있는 방법을 보여주는 예시 프로젝트를 사용해요. 저장소는 Dockerfile과 docker-bake.hcl 파일을 모두 포함해 Bake 명령을 시도할 수 있는 바로 사용 가능한 설정을 제공해요.
예시 저장소를 클론해 시작하세요:
git clone https://github.com/dvdksn/bakeme.git
cd bakeme
Bake 파일 docker-bake.hcl은 targets와 groups를 사용해 빌드 타깃을 선언적 구문으로 정의하며, 이를 통해 복잡한 빌드를 효율적으로 관리할 수 있어요.
기본적으로 제공되는 Bake 파일은 다음과 같아요:
target "default" {
target = "image"
tags = [
"bakeme:latest",
]
attest = [
"type=provenance,mode=max",
"type=sbom",
]
platforms = [
"linux/amd64",
"linux/arm64",
"linux/riscv64",
]
}
target 키워드는 Bake의 빌드 타깃을 정의해요. default 타깃은 명령줄에서 특정 타깃이 지정되지 않았을 때 빌드할 타깃을 정의해요. default 타깃의 옵션에 대한 간단한 요약:
target: Dockerfile의 타깃 빌드 스테이지.tags: 이미지에 할당할 태그.attest: 이미지에 첨부할 Attestations.-
팁: attestation은 이미지 빌드의 출처를 추적하는 build provenance와 보안 감사 및 규정 준수에 유용한 SBOM(Software Bill of Materials) 같은 메타데이터를 제공해요.
-
platforms: 빌드할 플랫폼 변형.
이 빌드를 실행하려면 저장소 루트에서 다음 명령을 실행하세요:
$ docker buildx bake
Bake를 사용하면 길고 외우기 어려운 명령줄 주문을 피하고, 수동이고 오류가 발생하기 쉬운 스크립트를 구조화된 구성 파일로 대체해 빌드 구성 관리를 단순화해요.
대조적으로, Bake 없이 이 빌드 명령은 다음과 같을 거예요:
$ docker buildx build \
--target=image \
--tag=bakeme:latest \
--provenance=true \
--sbom=true \
--platform=linux/amd64,linux/arm64,linux/riscv64 \
.
테스트 및 린팅 (Testing and linting)
Bake는 빌드 구성을 정의하고 빌드를 실행하는 것만을 위한 것이 아니에요. Bake를 사용해 테스트를 실행할 수도 있으며, 효과적으로 BuildKit을 작업 러너로 사용할 수 있어요. 컨테이너에서 테스트를 실행하는 것은 재현 가능한 결과를 보장하는 데 좋아요.
이 섹션은 두 가지 유형의 테스트를 추가하는 방법을 보여줘요:
go test를 사용한 단위 테스트.golangci-lint를 사용한 스타일 위반 린팅.
Test-Driven Development (TDD) 방식으로 Bake 파일에 새 test 타깃을 추가하는 것부터 시작하세요:
target "test" {
target = "test"
output = ["type=cacheonly"]
}
팁:
type=cacheonly를 사용하면 빌드 출력이 사실상 폐기됩니다; 레이어는 BuildKit의 캐시에 저장되지만 Buildx는 결과를 Docker Engine의 이미지 저장소에 로드하려고 하지 않아요. 테스트 실행의 경우 빌드 출력을 내보낼 필요가 없어요 — 오직 테스트 실행만 중요해요.
이 Bake 타깃을 실행하려면 docker buildx bake test를 실행하세요. 이 시점에 Dockerfile에 test 스테이지가 없음을 나타내는 오류가 표시될 거예요.
$ docker buildx bake test
[+] Building 1.2s (6/6) FINISHED
=> [internal] load local bake definitions
...
ERROR: failed to solve: target stage "test" could not be found
이 타깃을 충족하려면 해당 Dockerfile 타깃을 추가하세요. 여기서 test 스테이지는 빌드 스테이지와 같은 기본 스테이지에 기반해요.
FROM base AS test
RUN --mount=target=. \
--mount=type=cache,target=/go/pkg/mod \
go test .
팁:
--mount=type=cache지시문은 빌드 사이에 Go 모듈을 캐시해 의존성을 다시 다운로드할 필요를 없애 빌드 성능을 개선해요. 이 공유 캐시는 동일한 의존성 세트가 빌드, 테스트 및 기타 스테이지에서 사용 가능함을 보장해요.
이제 Bake로 test 타깃을 실행하면 이 프로젝트의 단위 테스트가 평가될 거예요. 동작하는지 확인하려면 main_test.go에 임의의 변경을 해 테스트가 실패하게 만들 수 있어요.
다음으로, 린팅을 활성화하기 위해 Bake 파일에 lint라는 또 다른 타깃을 추가하세요:
target "lint" {
target = "lint"
output = ["type=cacheonly"]
}
그리고 Dockerfile에 빌드 스테이지를 추가하세요. 이 스테이지는 Docker Hub의 공식 golangci-lint 이미지를 사용할 거예요.
팁: 이 스테이지는 외부 의존성을 실행하는 데 의존하므로, 사용할 버전을 빌드 인자로 정의하는 것이 일반적으로 좋은 생각이에요. 이렇게 하면 의존성 버전을 Dockerfile의 시작 부분에 함께 배치해 향후 버전 업그레이드를 관리할 수 있어요.
ARG GO_VERSION="1.23"
ARG GOLANGCI_LINT_VERSION="1.61"
#...
FROM golangci/golangci-lint:v${GOLANGCI_LINT_VERSION}-alpine AS lint
RUN --mount=target=.,rw \
golangci-lint run
마지막으로, 두 테스트를 동시에 실행하려면 Bake 파일에서 groups 구성을 사용할 수 있어요. 그룹은 단일 호출로 실행할 여러 타깃을 지정할 수 있어요.
group "validate" {
targets = ["test", "lint"]
}
이제 두 테스트 모두를 다음처럼 간단하게 실행할 수 있어요:
$ docker buildx bake validate
변형 빌드 (Building variants)
때로는 프로그램의 두 개 이상의 버전을 빌드해야 할 때가 있어요. 다음 예시는 matrices를 사용해 프로그램의 별도 "release" 및 "debug" 변형을 빌드하기 위해 Bake를 사용해요. 행렬을 사용하면 서로 다른 구성으로 병렬 빌드를 실행할 수 있어 시간을 절약하고 일관성을 보장해요.
행렬은 단일 빌드를 각각 고유한 행렬 파라미터 조합을 나타내는 여러 빌드로 확장해요. 이는 최소한의 구성 변경으로 프로그램의 프로덕션 빌드와 개발 빌드를 병렬로 빌드하도록 Bake를 오케스트레이션할 수 있다는 뜻이에요.
이 가이드의 예시 프로젝트는 조건부로 디버그 로깅 및 추적 기능을 활성화하는 빌드 시간 옵션을 사용하도록 설정되어 있어요.
go build -tags="debug"로 프로그램을 컴파일하면 추가 로깅 및 추적 기능이 활성화됩니다(개발 모드).debug태그 없이 빌드하면 프로그램이 기본 로거로 컴파일됩니다(프로덕션 모드).
빌드할 변수 조합을 정의하는 matrix 속성을 추가해 Bake 파일을 업데이트하세요:
target "default" {
+ matrix = {
+ mode = ["release", "debug"]
+ }
+ name = "image-${mode}"
target = "image"
matrix 속성은 빌드할 변형("release" 및 "debug")을 정의해요. name 속성은 행렬이 여러 개의 개별 빌드 타깃으로 확장되는 방식을 정의해요. 이 경우 행렬 속성은 서로 다른 구성 파라미터를 사용하는 두 개의 워크플로우인 image-release와 image-debug로 빌드를 확장해요.
다음으로 행렬 변수의 값을 취하는 BUILD_TAGS라는 빌드 인자를 정의하세요.
target = "image"
+ args = {
+ BUILD_TAGS = mode
+ }
tags = [
이미지 태그가 이 빌드들에 할당되는 방식도 변경하고 싶을 거예요. 작성된 대로 두 행렬 경로는 동일한 이미지 태그 이름을 생성해 서로 덮어쓸 거예요. 행렬 변수 값에 따라 태그를 설정하려면 tags 속성을 조건 연산자를 사용하도록 업데이트하세요.
tags = [
- "bakeme:latest",
+ mode == "release" ? "bakeme:latest" : "bakeme:dev"
]
mode가release이면 태그 이름은bakeme:latestmode가debug이면 태그 이름은bakeme:dev
마지막으로 컴파일 단계 동안 BUILD_TAGS 인자를 소비하도록 Dockerfile을 업데이트하세요. -tags="${BUILD_TAGS}" 옵션이 -tags="debug"로 평가되면 컴파일러는 debug.go 파일의 configureLogging 함수를 사용해요.
# build compiles the program
FROM base AS build
-ARG TARGETOS TARGETARCH
+ARG TARGETOS TARGETARCH BUILD_TAGS
ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH
RUN --mount=target=. \
--mount=type=cache,target=/go/pkg/mod \
- go build -o "/usr/bin/bakeme" .
+ go build -tags="${BUILD_TAGS}" -o "/usr/bin/bakeme" .
그게 전부예요. 이 변경으로 docker buildx bake 명령은 이제 두 개의 multi-platform 이미지 변형을 빌드해요. docker buildx bake --print 명령으로 Bake가 생성하는 정식 빌드 구성을 검사할 수 있어요. 이 명령을 실행하면 Bake가 서로 다른 빌드 인자와 이미지 태그를 가진 두 타깃의 default 그룹을 실행함을 보여줘요.
{
"group": {
"default": {
"targets": ["image-release", "image-debug"]
}
},
"target": {
"image-debug": {
"attest": ["type=provenance,mode=max", "type=sbom"],
"context": ".",
"dockerfile": "Dockerfile",
"args": {
"BUILD_TAGS": "debug"
},
"tags": ["bakeme:dev"],
"target": "image",
"platforms": ["linux/amd64", "linux/arm64", "linux/riscv64"]
},
"image-release": {
"attest": ["type=provenance,mode=max", "type=sbom"],
"context": ".",
"dockerfile": "Dockerfile",
"args": {
"BUILD_TAGS": "release"
},
"tags": ["bakeme:latest"],
"target": "image",
"platforms": ["linux/amd64", "linux/arm64", "linux/riscv64"]
}
}
}
모든 플랫폼 변형을 포함하면 이 빌드 구성이 6개의 서로 다른 이미지를 생성한다는 뜻이에요.
$ docker buildx bake
$ docker image ls --tree
IMAGE ID DISK USAGE CONTENT SIZE USED
bakeme:dev f7cb5c08beac 49.3MB 28.9MB
├─ linux/riscv64 0eae8ba0367a 9.18MB 9.18MB
├─ linux/arm64 56561051c49a 30MB 9.89MB
└─ linux/amd64 e8ca65079c1f 9.8MB 9.8MB
bakeme:latest 20065d2c4d22 44.4MB 25.9MB
├─ linux/riscv64 7cc82872695f 8.21MB 8.21MB
├─ linux/arm64 e42220c2b7a3 27.1MB 8.93MB
└─ linux/amd64 af5b2dd64fde 8.78MB 8.78MB
빌드 아티팩트 내보내기 (Exporting build artifacts)
바이너리 같은 빌드 아티팩트를 내보내는 것은 Docker나 Kubernetes 없이 환경에 배포할 때 유용할 수 있어요. 예를 들어 프로그램이 사용자의 로컬 머신에서 실행되어야 한다면요.
로컬 exporter를 사용해 다음 타깃으로 로컬 플랫폼의 바이너리 버전을 내보낼 수 있어요.
target "bin" {
target = "bin"
output = ["build/bin"]
platforms = ["local"]
}
이 스테이지는 local 플랫폼을 지정한다는 점에 유의하세요. 기본적으로 platforms를 지정하지 않으면 빌드는 BuildKit 호스트의 OS와 아키텍처를 타깃으로 해요. Docker Desktop을 사용한다면 Docker가 Linux VM에서 실행되므로 이는 로컬 머신이 macOS나 Windows라도 빌드가 linux/amd64 또는 linux/arm64를 타깃으로 한다는 뜻인 경우가 많아요. local 플랫폼을 사용하면 타깃 플랫폼이 로컬 환경과 일치하도록 강제돼요.
다음으로 빌드 스테이지에서 컴파일된 바이너리를 복사하는 bin 스테이지를 Dockerfile에 추가하세요.
FROM scratch AS bin
COPY --from=build "/usr/bin/bakeme" /
이제 docker buildx bake bin으로 로컬 플랫폼 버전의 바이너리를 내보낼 수 있어요. 예를 들어 macOS에서는 이 빌드 타깃이 macOS의 표준 실행 파일 형식인 Mach-O 형식의 실행 파일을 생성해요.
$ docker buildx bake bin
$ file ./build/bin/bakeme
./build/bin/bakeme: Mach-O 64-bit executable arm64
다음으로 프로그램의 모든 플랫폼 변형을 빌드하는 타깃을 추가해보죠. 이렇게 하려면 방금 만든 bin 타깃을 상속하고 원하는 플랫폼을 추가해 확장할 수 있어요.
target "bin-cross" {
inherits = ["bin"]
platforms = [
"linux/amd64",
"linux/arm64",
"linux/riscv64",
]
}
이제 bin-cross 타깃을 빌드하면 모든 플랫폼에 대한 바이너리가 생성돼요. 각 변형에 대해 하위 디렉터리가 자동으로 생성돼요.
$ docker buildx bake bin-cross
$ tree build/
build/
└── bin
├── bakeme
├── linux_amd64
│ └── bakeme
├── linux_arm64
│ └── bakeme
└── linux_riscv64
└── bakeme
5 directories, 4 files
"release"와 "debug" 변형도 생성하려면 기본 타깃에서 했던 것처럼 행렬을 사용할 수 있어요. 행렬을 사용할 때는 행렬 값에 따라 출력 디렉터리도 구분해야 해요. 그렇지 않으면 각 행렬 실행에 대해 바이너리가 같은 위치에 작성돼요.
target "bin-all" {
inherits = ["bin-cross"]
matrix = {
mode = ["release", "debug"]
}
name = "bin-${mode}"
args = {
BUILD_TAGS = mode
}
output = ["build/bin/${mode}"]
}
$ rm -r ./build/
$ docker buildx bake bin-all
$ tree build/
build/
└── bin
├── debug
│ ├── linux_amd64
│ │ └── bakeme
│ ├── linux_arm64
│ │ └── bakeme
│ └── linux_riscv64
│ └── bakeme
└── release
├── linux_amd64
│ └── bakeme
├── linux_arm64
│ └── bakeme
└── linux_riscv64
└── bakeme
10 directories, 6 files
결론 (Conclusion)
Docker Buildx Bake는 복잡한 빌드 워크플로우를 간소화해 효율적인 multi-platform 빌드, 테스트, 아티팩트 내보내기를 가능하게 해요. 프로젝트에 Buildx Bake를 통합하면 Docker 빌드를 단순화하고, 빌드 구성을 이식 가능하게 만들며, 복잡한 구성을 다룰 수 있어요.
다양한 구성을 실험하고 프로젝트의 필요에 맞게 Bake 파일을 확장하세요. 빌드, 테스트, 아티팩트 전개를 자동화하기 위해 CI/CD 파이프라인에 Bake를 통합하는 것을 고려할 수도 있어요. Buildx Bake의 유연성과 강력함은 개발 및 배포 과정을 크게 개선할 수 있어요.
더 읽을거리 (Further reading)
Bake 사용 방법에 대한 자세한 내용은 다음 리소스를 확인하세요: