E-buildKit을 활용한 멀티 플랫폼, 캐싱 빌드 실습

개요

이 문서에서는 쿠버네티스 환경에서 buildKit을 활용해 이미지를 빌드하는 실습을 진행한다.
이미지를 빌드할 때 중요한 요소가 세 가지 정도 있다고 생각한다.

첫번째는 당연한 내용인 게, 보안적으로도 공격 표면을 줄이는데 도움이 된다.
빌드 속도를 올리는 게 중요한 이유는 결국 빌드라는 것도 자원을 소모하는 행위이기 때문이다.
클라우드 환경에서 빌드를 진행한다면, 이미지 빌드 속도를 올리는 것은 곧 비용을 절약하는 것으로 연결된다.
아키텍처의 경우에는 다양한 환경에서 컨테이너를 구동할 수 있도록 하기 위함이다.
맥에서 개발을 진행하고 리눅스 환경에서 구동을 한다면 멀티 플랫폼 빌드는 필수일 것이다.
또, 그라비톤과 같이 비용 절약적이면서 cpu 아키텍처가 다른 환경으로 서비스를 마이그레이션하는 경우에도 멀티 플랫폼 빌드가 필요하다.

실습 세팅

테라폼으로 EKS를 구축해 실습을 진행한다.
버전은 1.31로 할 것인데, 일단 1.33까지는 관련한 업데이트 사항이 없기에 더 최신 버전에서도 동작할 것으로 생각된다.
빌드 툴은 위에서도 말했듯 buildKit을 활용하는데 이유는 카니코나 다른 툴보다 빠르고 효율적이라고 평가받기 때문이다.
이미지 레지스트리는 가격 세이브를 위해 도커 허브를 이용하며, 전체 파이프라인은 아르고 워크플로우를 통해 자동화한다.

기본 워크플로우

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: ci-template
spec:
  serviceAccountName: argo-workflow
  entrypoint: main
  arguments:
    parameters:
    - name: registry
      value: zerotay
    - name: tag
      value: 0.1.5
  templates:
    - name: main
      dag:
        tasks:
          - name: build-amd64
            template: build
            arguments:
              parameters:
              - name: registry
                value: "{{workflow.parameters.registry}}"
              - name: tag
                value: "{{workflow.parameters.tag}}"
              - name: arch
                value: arm64
              artifacts:
              - name: git
                git:
                  repo: https://github.com/Zerotay/sample-web.git
                  revision: "main"
                  usernameSecret:
                    name: github-creds
                    key: username
                  passwordSecret:
                    name: github-creds
                    key: password
                  singleBranch: true
                  branch: main

    - name: build
      inputs:
        parameters:
        - name: registry
        - name: tag
        - name: arch
        artifacts:
          - name: git
            path: /mnt/git
      container:
        image: moby/buildkit:v0.9.3-rootless
        workingDir: /mnt/git
        env:
          - name: BUILDKITD_FLAGS
            value: --oci-worker-no-process-sandbox
          - name: DOCKER_CONFIG
            value: /.docker
        command:
          - buildctl-daemonless.sh
        args:
          - build
          - --frontend
          - dockerfile.v0
          - --local
          - context=.
          - --local
          - dockerfile=.
          - --output
          - type=image,name={{inputs.parameters.registry}}/zero-web:{{inputs.parameters.tag}},push=true
        volumeMounts:
          - name: docker-config
            mountPath: /.docker
            readOnly: true
      volumes:
        - name: docker-config
          secret:
            secretName: dockerhub-creds
            items:
            - key: .dockerconfigjson
              path: config.json

가장 기본적인 워크플로우는 이렇게 만들었다.
image.png
기본적으로 속도가 생각보다 빨랐는데, 이전과 달라서 조금 당황했다..
예전에 분명 4분 걸렸던 거 같은데..?
image.png
이미지는 이렇게 들어왔는데, 이전보다 용량도 아주 조금 작아졌다.
어째서 용량이 작아진 것인지는 잘 모르겠다.
다만 이전에 빌드를 하던 시점에는 docker buildx를 로컬에서 활용했는데, 이 과정에서 추가적인 메타데이터들이 포함되기라도 했던 건 아닐까?
(아니 이미지 크기에 메타데이터가 포함된다는 것도 말이 이상한데)

캐싱

인라인 캐싱

image.png
흠. 내 생각과 다르게, 캐싱의 결과가 조금도 보이지 않는다.
image.png
멀티 스테이지 빌드를 하다보니, 중간 스테이지는 제대로 캐싱이 진행되지 않는다.
그래서 결과적으로 각 스테이지는 다시 빌드가 진행되고 새로 빌드가 됐으니 최종 스테이지에서도 다시금 데이터를 받게 된다.
결국 멀티 스테이지 빌드에서는 인라인 캐싱이 그다지 실효성이 없게 되는 것이다..

사실 인라인 캐싱을 통해 볼 것은 그 캐싱 데이터가 구체적으로 어디에 저장되는가에 대한 것이었는데, 이 부분은 다른 것을 하다가 제대로 실습하지 못했다.
이미지 용량에 차이가 발생하지 않는다는 것은 애초에 알고 있었지만, 그럼 구체적으로 어디에 캐싱을 한다는 말인가?
이미지 메타데이터?

레지스트리 캐싱

image.png
레지스트리 캐싱은 정상적으로 이뤄진다.
중간 스테이지들이 전부 캐시로 퉁쳐져버려 빌드 시간은 거의 소모되지 않고 다만 푸시를 하는 시간만 거의 그대로이다.
image.png
시간이 확실히 단축된 것이 보인다.
image.png
허브에서 확인해보면 아예 캐싱용 태그가 따로 표시되는 것을 볼 수 있다.
이 친구는 이미지가 아니기 때문에, 이미지 다이제스트는 존재하지 않는다.

멀티 플랫폼 빌드

아키텍처 간 파이프라인 분리 - 문제 발생

  templates:
    - name: main
      dag:
        tasks:
          - name: build-amd64
            template: build
            arguments:
              parameters:
              - name: registry
                value: "{{workflow.parameters.registry}}"
              - name: tag
                value: "{{workflow.parameters.tag}}"
              - name: arch
                value: amd64
              artifacts:
              - name: git
                git:
                  repo: https://github.com/Zerotay/sample-web.git
                  revision: "main"
                  usernameSecret:
                    name: github-creds
                    key: username
                  passwordSecret:
                    name: github-creds
                    key: password
                  singleBranch: true
                  branch: main

          - name: build-arm64
            template: build
            arguments:
              parameters:
              - name: registry
                value: "{{workflow.parameters.registry}}"
              - name: tag
                value: "{{workflow.parameters.tag}}"
              - name: arch
                value: arm64
              artifacts:
              - name: git
                git:
                  repo: https://github.com/Zerotay/sample-web.git
                  revision: "main"
                  usernameSecret:
                    name: github-creds
                    key: username
                  passwordSecret:
                    name: github-creds
                    key: password
                  singleBranch: true
                  branch: main

    - name: build
      inputs:
        parameters:
        - name: registry
        - name: tag
        - name: arch
        artifacts:
          - name: git
            path: /mnt/git
      nodeSelector:
        kubernetes.io/arch: "{{inputs.parameters.arch}}"
      container:
        image: moby/buildkit:v0.9.3-rootless
        workingDir: /mnt/git
        env:
          - name: BUILDKITD_FLAGS
            value: --oci-worker-no-process-sandbox
          - name: DOCKER_CONFIG
            value: /.docker
        command: [sh,-c]
        args:
          - >
            buildctl-daemonless.sh
            build --frontend dockerfile.v0 --local context=. --local dockerfile=. 
            --output type=image,name={{inputs.parameters.registry}}/zero-web:{{inputs.parameters.tag}},push=true 
            --export-cache type=registry,ref={{inputs.parameters.registry}}/zero-web:{{inputs.parameters.arch}}-cache 
            --import-cache type=registry,ref={{inputs.parameters.registry}}/zero-web:{{inputs.parameters.arch}}-cache 
        volumeMounts:
          - name: docker-config
            mountPath: /.docker
            readOnly: true
            # --opt platform=linux/{{inputs.parameters.arch}} 
      volumes:
        - name: docker-config
          secret:
            secretName: dockerhub-creds
            items:
            - key: .dockerconfigjson
              path: config.json

멀티 플랫폼을 위해서 이런 식으로 세팅해보았다.
한번의 빌드를 통해 여러 플랫폼 빌드를 수행하면 빌드에 시간이 오래 걸리게 되고 가상 환경을 구동하는 만큼 자원의 낭비가 생기게 된다.
그래서 아예 각 플랫폼에 대한 빌드를 각 노드에 맞춰 빌드하게 만든 것이다.
노드 셀렉터에 대해서는 카펜터가 파드의 언스케줄 상태를 인식하고 알아서 적절한 노드를 배치할 것이다.
image.png
실제로 빌드 자체는 제대로 진행됐다.
arm 노드가 없었기 때문에 노드가 프로비저닝되는 시간이 있어 상대적으로 arm 빌드는 펜딩이 조금 더 길긴 했으나, 어차피 이 작업이 끝나고 arm 노드는 카펜터가 알아서 잘 없애주었다.
image.png
그러나 이렇게 각각의 빌드를 푸시하는 방식을 택하자 이전에 푸시된 정보가 없어지면서 결국 뒤늦게 푸시된 아키텍쳐로 이미지가 고정되어 버리는 이슈가 발생했다.
여러 방면으로 시도를 해봤으나, 두번의 빌드가 통합되어 멀티 플랫폼이 지원되는 일은 발생하지 않았다.
결국 푸시를 하는 그 순간에 한번에 무조건 멀티 플랫폼 이미지를 전달해야 한다는 말이 된다.
이 부분은 언젠가 업데이트가 되지 않을까 싶긴 하다.
같은 태그에 대해서 다른 플랫폼으로 빌드된 이미지를 푸시할 때 겹치지 않도록 설정하는 것은 설정에 따라서 충분히 지원되지 않을까?
그도 그럴 게, buildx 의 경우 빌드 클라우드를 활용하면 각 노드에서 빌드가 진행된 후 이미지가 푸시된다고 한다.
이때는 잘 되는 것으로 보아, 당장의 빌드 방식이 이런 사용케이스를 그저 고려하지 못한 채 개발됐다고 생각해볼 수 있다.
image.png
buildx의 코드를 잠깐 살펴보는데, 메타데이터에 데이터가 잘 들어가 있으면 해결되는 것이 아닐까 하는 생각이 들었다.[1]
지속적으로 찾아봤지만, 메타데이터를 직접적으로 도커파일이나 환경 변수, 인자 등으로 세팅하는 옵션은 지원되지 않는 것 같았다.
image.png
어쩔 수 없이 그냥 멀티 플랫폼 빌드를 시도했는데, 여기에서도 문제가 발생했다.
내부적으로 qemu는 분명 돌아가고 있는 것으로 보인다.
캐싱에서 문제가 발생하는 게 아닐까 하는 추측도 했지만, 그보다는 루트 권한 없이 실행하는 부분에서 생기는 이슈가 아닐까 싶기도 하다.

사실 이 상태에서 문제를 해결하는 방법은 단순하다.
한 태그에 대한 멀티 플랫폼 이미지는 포기하는 것이다.
그저 이미지 버저닝이나 태그 부분에 아키텍처를 넣는 방식으로 각 이미지 간 아키텍쳐 분리를 시켜 올리면 당장 발생하는 문제는 해결할 수 있기는 하다.
그렇지만.. 도망쳐 나온 곳에 낙원은 없는 거 아니냐..

당장 적당한 대안이 떠오르지 않아, 실습은 여기에서 종료했다.

결론

캐싱을 통해 매우 효율적으로 빌드를 하는 데에는 성공했지만, 멀티 플랫폼 빌드에서 문제가 생긴 것이 역시 퍽 아쉽다.
더 딥 다이브를 하면 방법을 찾을 수 있을 것 같은데, 단순하게 세팅하는 방법이 없다는 것을 안 것만으로 일단 의의는 있다고 하겠다.

아예 다른 방식으로 시도하는 것도 가능은 할 것 같다.
빌드킷 자체는 데몬으로 정말 띄워둔 상태에서 빌드를 진행하는 식으로 말이다.
하지만 데몬 프로세스가 노드에 떠 있어야 하기에 이것도 사실 마음에 들지는 않는다.

더 좋은 방법은, buildx의 도움을 받는 것이 아닐까 한다.
드라이버를 쿠버네티스로 하고, 여기에서 멀티 플랫폼으로 설정을 한다면 멀티 노드로 돌리는 동시에 같은 태그로 이미지를 올리는 게 가능할 것으로 생각된다.
이 경우 워크플로우로 정의를 했으나 막상 워크플로우는 추적되지 않는 다른 파드들이 띄워지는 식으로 동작을 하게 될 거라 이것도 썩 마음에 든다고는 못하겠다.
그래도 현재의 문제는 확실하게 해결할 수 있을 것으로 보이긴 한다.

다음에 추가 실습을 한다면, buildx를 아예 활용하는 방향으로 시도해보지 않을까 한다.
이때 docker cli까지도 설치를 해야 한다는 것이 리소스 낭비처럼 느껴지기도 하지만, 멀티 플랫폼 빌드가 제대로 이뤄질 수 있도록 하는 기회비용이라고 생각하면 그렇게 크게 아깝지도 않은 것 같다.
어차피 cli 자체의 크기로만 치면 스토리지 용량을 먹을 뿐이고, docker buildx라는 명령 방식 자체가 buildx 클라이언트만 이용하게 되는 방식이기에 그렇다.

관련 문서

이름 noteType created
레지스트리 knowledge 2024-07-04
Amazon Elastic Container Registry knowledge 2024-11-03
Argo Workflows knowledge 2025-03-24
컨테이너 이미지 knowledge 2025-03-24
buildKit knowledge 2025-03-30
buildKit knowledge 2025-05-04
Harbor knowledge 2025-06-16
8W - 아르고 워크플로우 published 2025-03-30
k8s air-gap install topic 2025-06-09
폐쇄망 k8s 설치 개요 topic 2025-06-09
에어갭 kubespray 단일 노드 설치 with Vagrant topic 2025-06-09
kubespray 삽질 topic 2025-06-11
kubesphere topic 2025-06-12
kubespray - 에어갭 최소 이미지, HA, cillium 기본 topic 2025-06-12
minio 케이스 topic 2025-06-14
스터디 1 topic 2025-06-15
kubespray cillium 특화 세팅 topic 2025-06-16
kubespray 심화 topic 2025-06-17
큐브스프레이 한정 설명 topic 2025-06-18
E-buildKit을 활용한 멀티 플랫폼, 캐싱 빌드 실습 topic/explain 2025-03-30
T-초기화 컨테이너의 이미지 바꾸기 topic/temp 2024-08-22

참고


  1. https://github.com/docker/buildx/blob/master/build/build.go#L417 ↩︎