지난 번에 log4j2 Rolling Policy Property를 추가하는 PR을 올리면서 Spring Boot 코드 읽는게 재밌었고 자신이 어느정도 생겨서 심심할 때 Spring Boot 레포를 확인해보다가 흥미로운 이슈를 발견해서 PR을 올려보았다.
issue 46674, 46665에 문제가 상세하게 작성되어있다.
https://github.com/spring-projects/spring-boot/issues/46674
https://github.com/spring-projects/spring-boot/issues/46665
46674 issue를 확인하고 작업을 시작했다. 해당 issue에 background로 46665 이슈가 있길래 확인해 보았는데, 비슷한 내용을 서술하는 것 같지만 다루는 이슈가 살짝 다르다.
간단 정리
#46665: inspect시 플랫폼 정보 누락되어 platform mismatch 오류
#46674: export시 플랫폼 정보 누락되어 Invalid buildpack reference '~' 오류 (왜 이 오류가 터지는지 찾는데 참 오래 걸렸다... 여러 레이어들이 맞물려서 에러들이 wrapping되니 결국 Invalid buildpack reference 오류가 되어버리는 마법)
현재 M1 맥북을 사용하고 있는데, 이슈에서 제보자는 M4 프로세서에서 문제가 확인되었으며 다른 프로세서에서는 재현하지 못했다고 해서 재현 방법을 찾아보던 중

이 옵션을 켜면 내 환경에서도 완벽하진 않지만 비슷한 문제를 재현할 수 있다는 것을 확인했다. 사실 이건 기존 이슈와는 다르다고 생각해서 이슈 댓글로 해당 이슈를 추가로 설명해서 달아두었다.
해당 옵션을 키면 오류가 나는 이유에 대해서 간단히 찾아봤는데, 정리해보자면 다음과 같다.
- 클래식 스토어(옵션 OFF)
- 태그(repo:tag)가 단일 manifest만 가리키도록 flat해짐
- pull 때 지정한 플랫폼(예: amd64)만 로컬에 그 태그로 존재 -> 이후 inspect가 플랫폼을 못 고르더라도 볼 게 하나뿐이라서 문제가 안 터졌던 것.
- containerd 스토어(옵션 ON)
- 태그가 멀티아키 인덱스(Manifest List) 로 그대로 저장됨
- inspect가 v1.41로 호출되면 ?platform 쿼리가 무시되고, 호스트 기본 플랫폼 variant가 보이게 됨 -> inspect에서 본 플랫폼(arm64) ≠ 요구한 플랫폼(amd64) -> platform mismatch Exception
이슈를 다루기 앞서 코드를 분석하며 찾은 문제가 되는 코드 베이스의 위치는 buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform 내부의 Docker/DockerApi, Build/Builder 클래스이다.
issue #46665

간략하게 요약해보자면 arm64 기반의 맥북 환경에서 Spring Boot Gradle 플러그인의 bootBuildImage 작업을 사용하여 크로스 플랫폼 이미지(arm64 호스트에서 amd64 이미지)를 빌드할 때 "Invalid buildpack reference"오류가 발생하는 이슈이다.
댓글에서 maintainer가 다양한 시나리오를 테스트하면서 문제 해결에 대한 실마리를 찾을 수 있었다.
시나리오 1: imagePlatform 설정 시 오류
bootBuildImage {
imagePlatform = "linux/amd64"
}
Image platform mismatch detected. The configured platform 'linux/amd64' is not supported by the image 'docker.io/paketobuildpacks/builder-noble-java-tiny:latest'. Requested platform 'linux/amd64' but got 'linux/arm64'
이에 대해 maintainer는 원인은 이전 빌드에서 호스트 플랫폼(arm64)으로 pull한 이미지가 Docker 데몬에 남아있어 발생하는 것 같다고 작성해주었다. -> 해당 건은 이슈 46674로 이어진다.
시나리오 2: 클린 상태에서 다른 플랫폼으로 빌드 시 #46665 오류 메세지
클린 상태에서 플랫폼을 지정해서 다시 빌드했을 때 issue 제보자가 말한 오류를 재현할 수 있었다고 한다.
ERROR: failed to export: saving image: failed to fetch base layers: saving image with ID "sha256:084c81f50c9619c5da73bd7310febf44437db8621c7563601989ef8839882bce" from the docker daemon: Error response from daemon: unable to create manifests file: NotFound: content digest sha256:27a8cdfd06282af14980e596bdcf79c9281a69cea83021b95e686f33603f5300: not found
Execution failed for task ':bootBuildImage'.
> Invalid buildpack reference 'paketobuildpacks/opentelemetry:2'
이슈에서 토론한 내용들을 읽어보며 코드에서 동일한 오류가 어디서 발생하는지 확인해보았다.


추가적으로 failed to export:~ 에러는 코드에서 찾지 못해서 인터넷을 검색해본 결과 도커의 이미지 내보내기(GET /images/{ref}/get나 docker save) 단계에서 잘못된 변형을 내보내려 할 때 나는 엔진 오류로, export 단계 문제를 가리킨다고 한다.
* 왜 여기서 Invalid buildpack reference 오류가 터질까?
열심히 지피티와 구글링해본 결과, buildpack이 잘못 선언되었다! 라고 한다. 근데 이상한게 Containerd store 옵션을 끄면 동일한 buildpack에 대한 빌드가 되는데, 왜 이 옵션을 키면 오류가 터지는가? 여기서 삽질을 좀 했는데, demo 프로젝트의 빌드팩은 빌드팩 이미지는 빌더 내장 빌드팩이 아닌 커스텀 빌드팩을 지정하고 있다. 이러면 기존 빌더에 없기 때문에 ephemeral builder로 커스텀 빌드팩을 export하여 레이어를 쌓게 되는데, 여기서 플랫폼 정보가 누락되어 예외가 wrapping 되고 resolver에서 null로 처리되어 결국 Invalid buildpack reference 오류가 터지게 되는 것이다....
그럼 platform을 명시했는데 왜 platform mismatch 오류가 나는 것일까? bootBuildImage의 흐름을 살펴보면 Spring Boot의 bootBuildImage는 pull과 save를 한다. 도커 레지스트리에서 이미지를 받아오고 빌드하여 저장하는데, 위의 오류 메세지를 확인해보면 export, 즉 save 단계에서 오류가 발생한다는 것을 알 수 있다. 또한 로그를 확인해보면 pull 단계에서는 지정한 platform으로 잘 받아오는 것을 확인할 수 있다.

그럼 이제 Spring Boot에서 buildpack에 대해 export하는 코드를 살펴보자.




플랫폼 정보를 어디에서도 찾아볼 수 없다. 이러면 Docker는 호스트 플랫폼(arm64)로 요청을 처리하기 때문에 pull한 amd64와 export하는 arm64가 일치하지 않는 문제가 생기게 된다.
요청한 플랫폼 정보를 넘겨주기 위해 플랫폼 정보를 어디에서 받아서 들고다니는지 열심히 코드를 뒤적여보았다.

요청 진입점을 Builder 클래스에서 찾을 수 있었다. 5번째 줄에서 ImageFetcher로 요청된 플랫폼을 추적하는 것을 확인할 수 있다. 그렇다면 이 ImageFetcher를 활용해서 exportLayers에 요청된 플랫폼 정보를 넘겨줄 수 있다!


docker 공식 문서를 확인하여 export 단계에서 플랫폼 정보를 포함한 url을 만들어 플랫폼 불일치 오류가 생기지 않도록 해주었다.
문제를 다시 정리해보자면 pull할 땐 amd64로 가져왔지만 export할땐 platform이 전달되지 않아 docker가 호스트 아키텍쳐(arm64)를 선택하고이 둘이 일치하지 않아 오류가 발생하는 문제였다.
pull 단계에서 platform=linux/amd64로 올바른 Variant를 받아왔지만 export 단계로 플랫폼을 전달하지 않아 Docker가 호스트 아키텍처(arm64)를 선택하는 문제이다.
* /images/{ref}/get URL에서 platform 쿼리를 지원하는 버전은 1.48버전 이후라고 명시되어있기 때문에 EXPORT_PLATFORM_API_VERSION 상수를 정의하여 api 버전에 따른 분기를 해주었다.
참고) https://docs.docker.com/reference/api/engine/version-history
Engine API version history
Documentation of changes that have been made to Engine API.
docs.docker.com
요약: pull 플랫폼과 export 플랫폼이 불일치 -> 빌드팩 레이어 추출 실패
issue #46674

46674 이슈는 46665 이슈와 비슷해보여서 어떻게 접근해야할지 고민이 있었다. 나오는 오류도 platform mismatch라서 코드분석과 리서치를 꽤 오랜시간 했던 것 같다. 하지만 도커 데몬의 캐시를 지우면 빌드가 된다는 점이 실마리였다.
"왜 캐시를 지우면 오류가 해결될까?"에 대한 고민해보고 찾아본 결과 도커의 동작과 연관이 있다는 것을 알 수 있었다. 캐시를 지우면 해결된다는 것은 기존 빌드 결과와 충돌이 있다는 말이다. 그 이유는 Docker는 로컬 이미지 스토어(캐시)를 먼저 조회하기 때문이다. inspect(Docker daemon에 이미지의 상세 메타데이터를 조회하는 동작)는 레지스트리를 보지 않고 “로컬 이미지 스토어의 태그 -> 이미지ID” 매핑을 먼저 사용한다. 따라서 요청이 태그(name:tag)면 로컬에 있는 그 태그의 변형을 그대로 돌려주게 된다. 첫 빌드(기본 플랫폼)로 로컬에 arm64 변형이 태그로 남아있는데, 다음에 imagePlatform=linux/amd64로 빌드하게되면 코드가 태그 기준 inspect를 호출하면 로컬의 arm64가 반환되어 플랫폼 불일치가 나는 것이다.
실제로 코드를 확인해보자.


bootBuildImage의 진입점은 pull 메서드이다. 기본적으로 digest를 포함해서 요청하는 것이 아니라면 ImageReference엔 이름:태그만 포함된다. 그리고 inspect 메서드는 플랫폼 파라미터가 없고, 로컬 태그가 가리키는 변형을 반환한다. 따라서 같은 태그 아래에 여러 플랫폼이 존재한다면 docker 입장에서는 모르는 것이다. 심지어 로컬에 먼저 풀린 동일한 태그가 있다면 바로 그걸 사용하게 된다.
해당 문제를 해결하기 위해서는 pull받은 이미지를 정확하게 가르켜야 한다. inspect에서도 플랫폼 정보를 전파하면 해결될 것이라고 생각하여 inspect에서 사용하는 URL인 /images/{ref}/json에 플랫폼 정보를 넘겨주어 정확한 variant를 선택하도록 해주었다.
* /images/{ref}/json의 플랫폼 쿼리는 1.49버전 이상에서 지원하므로 INSPECT_PLATFORM_API_VERSION 상수를 추가적으로 지정하여 분기해주었다.



추가적으로 여기서 폴백 로직으로 digest를 사용했다. api 버전을 지원하지 않는 경우엔 기존 로직을 사용하기 이전에 digest로 먼저 지정해주었다.
digest란 이미지(또는 레이어/매니페스트)의 내용에 대해 계산한 SHA‑256 해시값으로 "이름@sha256:..."의 형태로 사용한다. 해시값이기 때문에 각 이미지마다 고유하게 식별할 수 있어 해당 문제를 예방할 수 있다고 생각했다.
다행히도 기존 클래스에 digest를 캡쳐하는 변수가 있어서 해당 클래스에 getDigest를 만들어주어 digest를 확보해주었다. 그리고 확보한 digest를 기반으로 inspect를 수행하여 다른 이미지는 다루지 않게 했다.
참고
당연하지만 ImagePlatform의 equals는 모든 플랫폼 변형에 대한 검증을 수행한다. 하나라도 다르면 platform mismatch가 나게 된다.

linux/arm/v7 등으로 같은 os에서도 아키텍쳐, 변형이 모두 다르다. os, architecture, variant 모두 같아야 오류가 나지 않는다!
이번 이슈도 다시 정리해보자면 태그 기준 조회(inspect)가 기존 로컬 이미지 변형을 선택했기 때문이다.
- 첫 빌드(호스트 플랫폼): ARM Mac에서 기본 설정으로 빌드하면 빌더 이미지의 arm64 변형이 로컬에 풀림(예: latest)
- 두 번째 빌드(플랫폼 변경): imagePlatform=linux/amd64로 다시 빌드할 때, 코드는 빌더 이미지를 amd64로 pull 하긴 했지만 inspect를 태그로 수행
-> 태그만 주고 inspect하면 도커 데몬은 로컬에 이미 존재하던 변형(이전 arm64)을 반환하여 방금 pull한 amd64가 아닌 기존에 있던 arm64를 가리키는 문제
이렇게 코드를 수정하고 정상적으로 작동하는지 mavenLocal로 배포하여 bootBuildImage를 실행해보았는데, 이번엔 다른 오류가 뜨면서 실패했다.(이미지는 없음.. ㅠㅜ)
너무 간단하게 생각하고 수정했던 것 같다. 오류의 이유는 라이프사이클의 analyzer 단계가 런 이미지의 히스토리/구성(config)을 Docker 데몬에서 읽으려다 해당 digest를 못 찾아 실패하는 것이었다. 왜 그런지 직접 도커 API를 호출해보면서 확인해봤는데 충격적인 사실을 알아냈다.

linux/amd64를 조회하면 정상적으로 조회가 된다(sha256:cdb7bc4...). 하지만 Spring Boot에서 캡쳐하고 있는 RepoDigests는 멀티아키텍쳐 인덱스를 가리키고 있다. 따라서 우리가 실제로 원하는 linux/amd64의 Digest를 정확하게 가리키지 못한다. 그러므로 도커 데몬은 또 다시 호스트 플랫폼(sha256:4ee4065...)을 기준으로 찾게 되고, 심지어 로컬에서 가리키고 있는 Digest는 manifest의 Digest도 아닌 멀티아키의 index Digest(fff1...)을 가리키고 있다. 이 혼란스러운 상황을 정리하기 위해 Spring Boot의 Image 클래스가 어떤 값을 가지고 있는지 확인해보았다.


Image 클래스가 가지고 있는 digests는 RepoDigests이고 이것은 실제 manifest의 digest가 아니다. 따라서 이 클래스에 manifest의 digest를 안정적으로 담을 수 있는 방법을 찾아보았다. docker의 버전에 따라서 필드명이 달라지거나 없을 수도 있다고 해서 확정적으로 manifest의 digest를 담기위해 어떤 값을 확인해야하는지 공식문서를 확인해보았다.
https://specs.opencontainers.org/image-spec/descriptor/
The OpenContainers Descriptor Spec
Opencontainers Specs Documentation
specs.opencontainers.org
descriptor엔 digest 필드가 필수(REQUIRED)라고 표시되어있는 것을 확인할 수 있다. 따라서 Descriptor 필드를 Image 클래스 내부에 추가해주었다.



Descriptor를 내부 클래스로 정의하여 추가해주었고, 그에 알맞게 생성자를 추가해주었다. Descriptor 자체가 존재하지 않을 수도 있으니 @Nullable로 체크해주었다.(containerd store 옵션이 꺼져있을 때 -> 어차피 멀티 아키텍쳐 오류가 일어나지 않으므로 신경쓰지 않기로 했다)

위 메서드를 정의하여 정확한 이미지를 확인하도록 수정했고, pull 받을 때 해당 이미지로 pull 받도록 명시해주었다.

이제 다시 실행되는지 확인해보았고, 정상적으로 작동하는 것을 확인했다!

이렇게 Spring Boot의 bootBuildImage 관련 이슈를 해결해보았다. 볼륨이 크면 PR을 나눠서 올릴까 했지만 다행히도 코드 수정분은 많지 않아 하나의 PR로 합쳐서 올렸다. (머지될지는 모르지만..)
정말 도전적이었고 문제가 복합적으로 엮여있어서 정말 힘들었다... 오류 하나를 해결하면 그 뒤에 오류가 기다리고 있는,,, 회고하면서 작성한거라 생겼던 모든 오류를 담진 못했지만, 정말 처음보는 도커 오류들을 알게 되었다... 하지만 당시엔 정말 시간가는줄 모르고 코딩했고 Spring Boot의 bootBuildImage에 대해서 정말 상세하게 알게 되었다.
또한 이슈에 대한 설명과 재현이 너무 잘되어있어서 편했다. 문제 정의가 절반 이상이라는 것이 틀린 말이 아닌 것 같다. maintainer가 문제를 재현하고 기록하는 방식이 꽤 인상깊었고, 잘 배워두어야겠다는 생각이 들었다. 잘 정의된 문제는 풀이하기 쉽게 만들어준다
+ 11/14 추가
올린 PR이 반영되었다! 자고 일어나니 별다른 토론 없이 maintainer 분이 수정을 조금 거쳐서 바로 반영해주셨는데, 아마 사안이 급해서 그랬던 것 같다. (최근 일주일간 Spring Boot 레포에 docker image build fail 관련된 이슈가 많이 올라온걸 확인했다 ㅋㅋㅋ docker에서 baseline 버전을 올려버리면서 관련 이슈가 우후죽순 쏟아지는 것 같았다.)

maintainer분이 수정한 내용은 export시 플랫폼 정보를 전달하지 않아도 될 것 같다고 하셨는데, 아마 #46674 이슈만 확인하셔서 그런 것 같다. #46665도 비슷한 맥락이지만, 커스텀 빌드팩이 지정된 경우에는 buildpack에 대한 레이어를 쌓을 때 export가 이루어지기 때문에 export에도 필요하다. 마침 46665 이슈 댓글로 maintainer 분이 이슈가 해결되었는지 묻는 글이 있었고, 호다닥 가서 신규 스냅샷 버전으로도 이슈가 해결되지 않는다는 것을 제보하고, 이것에 대한 해결 방안을 제시하고 어느 부분에서 이렇게 생각하는지에 대한 글을 달았다.
https://github.com/spring-projects/spring-boot/issues/46665#issuecomment-3527899219


PR에서 제거한 부분이 실제로 필요한 것 같다는 답글을 달아주셨고, 이후 제안한 export 단계에서의 platform 정보가 추가되어 수정되었다.
이렇게 기나긴 bootBuildImage에 대한 이슈가 끝이 났다. 해당 이슈에 대한 상세한 내용, 토론들과 증상 재현은 #46665, #46674 이슈 댓글에 잘 정리되어있으니 찾아보는 것도 좋을 것 같다!



코드 수정분은 3.5.8과 3.4.12버전부터 적용될 예정이다. 여담으로 버저닝 관리하는 것이 정말 신기했다. 이렇게 체계적으로 릴리즈 버전 관리를 해본 적이 없는데, 자주 보면서 한 번 배워봐야겠다.
최근 도커 관련된 이슈가 많이 터졌는데, 내가 작성한 코드가 누군가의 오류들을 해결한다는 것이 정말 뿌듯했고 신기한 경험이었다. 이 맛에 개발자 하는거 아닐까..?
PR 링크
https://github.com/spring-projects/spring-boot/pull/47292
Fix Docker multi-architecture image platform handling in buildpack operations by hojooo · Pull Request #47292 · spring-project
Summary This PR fixes Docker platform handling issues that occur when building images with specific platforms on ARM64 Macs, particularly when targeting AMD64 platforms. The changes ensure that pla...
github.com
'Computer Science > Spring Boot' 카테고리의 다른 글
| Spring AI (0) | 2025.11.27 |
|---|---|
| Log4J2 Properties 개발기 3편 (0) | 2025.10.22 |
| Log4J2 Properties 개발기 2편 (0) | 2025.10.21 |
| Log4J2 Properties 개발기 1편 (0) | 2025.10.20 |
| Spring Security - 인증/인가와 FilterChain (w. JWT, Authorization) (0) | 2025.09.12 |