OPEN SOURCE

[Open source contribution] Apache Shardingsphere 오픈소스 기여 경험기 - Apache ShardingSphere 기여 후기: Proxy Native 도커 이미지의 ENTRYPOINT가 ${LOCAL_PATH}를 읽지 못하던 문제

ebson 2026. 10. 6. 13:46

Java 소스가 없는 모듈에서 찾은 결함

Apache ShardingSphere에는 distribution/proxy-native라는 모듈이 있습니다. ShardingSphere-Proxy를 GraalVM Native Image로 빌드해 배포하기 위한 패키징 모듈로, pom.xml의 packaging이 pom이고 Java 소스가 하나도 없습니다. 모듈 안에는 pom.xml, 배포 구조를 정하는 assembly 설정, 그리고 리눅스용 Dockerfile 세 개(Dockerfile-linux-dynamic, Dockerfile-linux-mostly, Dockerfile-linux-static)가 있을 뿐입니다.

 

보통 기여할 거리를 찾을 때는 로직 오류나 경계 조건처럼 테스트로 재현할 수 있는 결함을 먼저 떠올립니다. 그런데 이 모듈에는 테스트를 붙일 Java 코드 자체가 없었기 때문에, Dockerfile을 직접 읽게 됐습니다. 그러다 세 파일의 마지막 네 줄이 똑같은 모양이라는 점이 눈에 들어왔습니다.

ENV LOCAL_PATH=/opt/shardingsphere-proxy
ARG PROJECT_VERSION
COPY --from=nativebuild /build/distribution/proxy-native/target/apache-shardingsphere-${PROJECT_VERSION}-shardingsphere-proxy-bin ${LOCAL_PATH}
ENTRYPOINT ["${LOCAL_PATH}/bin/shardingsphere-proxy-native", "3307", "${LOCAL_PATH}/conf", "0.0.0.0"]

얼핏 보면 경로를 한곳에서 관리하는 깔끔한 구성처럼 보입니다. 문제는 ENTRYPOINT가 JSON 배열, 즉 exec 형식으로 쓰여 있다는 점이었습니다.

exec 형식은 변수를 치환하지 않습니다

Docker 공식 Dockerfile 레퍼런스는 exec 형식에 대해 "Using the exec form doesn't automatically invoke a command shell. This means that normal shell processing, such as variable substitution, doesn't happen."이라고 설명합니다. exec 형식은 셸을 거치지 않고 배열의 첫 원소를 그대로 실행하기 때문에, ${LOCAL_PATH}라는 문자열이 치환되지 않은 채 넘어갑니다. 컨테이너는 ${LOCAL_PATH}/bin/shardingsphere-proxy-native라는, 실제로는 존재하지 않는 경로를 실행하려 합니다. 배열 세 번째 원소인 설정 디렉터리 ${LOCAL_PATH}/conf도 마찬가지로 글자 그대로 전달됩니다.

 

반면 바로 윗줄의 COPY는 정상적으로 동작합니다. 같은 레퍼런스에 따르면 환경 변수 치환은 ADD, COPY, ENV, WORKDIR 같은 일부 명령에서 빌더가 직접 처리합니다. 그래서 바이너리는 /opt/shardingsphere-proxy/bin 아래에 제대로 복사되고, 이미지 빌드까지는 아무 문제가 드러나지 않습니다. 이 점이 이 결함을 오래 숨겨 두었던 이유라고 생각합니다.

 

그렇다면 셸 형식으로 바꾸면 되지 않을까 하는 생각이 먼저 들었습니다. 같은 저장소의 형제 모듈인 distribution/proxy/Dockerfile은 ENTRYPOINT ${LOCAL_PATH}/bin/start.sh ${PORT}처럼 셸 형식을 쓰고, 그래서 변수가 정상적으로 치환됩니다. 하지만 이 Dockerfile의 런타임 베이스 이미지는 eclipse-temurin이라 셸이 들어 있습니다. Proxy Native의 세 Dockerfile은 각각 gcr.io/distroless/java-base-debian12, gcr.io/distroless/base-debian12, scratch를 베이스로 씁니다. distroless와 scratch에는 셸이 없으므로 셸 형식으로 되돌리는 방법은 쓸 수 없었습니다. Docker 레퍼런스도 ENTRYPOINT는 exec 형식으로 쓰는 편이 좋다고 안내하고, 셸 형식은 /bin/sh -c의 하위 명령으로 실행돼 시그널이 전달되지 않는다고 설명합니다.

언제부터 이렇게 됐는지 확인하기

수정 방향을 정하기 전에, 이 줄이 처음부터 이런 모양이었는지 git 이력을 거슬러 올라가 봤습니다. Proxy Native에서 DistSQL 실행을 지원한 PR #33095 이전의 Dockerfile에는 ENTRYPOINT ${LOCAL_PATH}/${NATIVE_IMAGE_NAME} 3307 ${LOCAL_PATH}/conf "0.0.0.0" false처럼 셸 형식이 쓰였습니다. 그 PR에서 ENTRYPOINT를 JSON 배열로 바꾸면서 ${LOCAL_PATH}가 배열 안에 그대로 남았고, 이후 PR #35570에서 Dockerfile이 linux-dynamic·mostly·static 세 파일로 나뉠 때도 같은 줄이 이어졌습니다.

 

이 이력을 확인하고 나니, 이 동작이 의도된 설계가 아니라 형식을 바꾸는 과정에서 생긴 회귀라고 판단할 근거가 생겼습니다. 다만 저는 GraalVM Native Image 빌드와 컨테이너 기동을 로컬에서 직접 돌려 재현하지는 않았습니다. 판단의 근거는 Docker 공식 레퍼런스의 규칙과 세 Dockerfile의 실제 내용, 그리고 git 이력이었고, 이슈와 PR 본문에도 그 범위를 그대로 적었습니다.

첫 번째 수정: ENTRYPOINT만 바꾸기

ShardingSphere에서는 오타가 아닌 변경이라면 이슈를 먼저 열고 PR에서 그 이슈를 참조합니다. 그래서 먼저 이슈 #39143에 증상과 원인을 정리했습니다. PR 제목은 저장소 관행에 따라 접두사 없이 명령형으로 Fix unexpanded LOCAL_PATH in Proxy Native Dockerfile ENTRYPOINT라고 지었습니다. ShardingSphere는 squash merge를 쓰기 때문에 PR 제목이 그대로 master의 커밋 제목이 되고, 끝에 GitHub이  (#N)을 붙입니다.

 

처음 올린 수정은 범위를 가장 좁게 잡은 것이었습니다. 세 Dockerfile의 ENTRYPOINT에서 ${LOCAL_PATH}를 ENV에 선언된 값과 같은 /opt/shardingsphere-proxy로 바꾸고, ENV와 COPY는 그대로 두었습니다. LOCAL_PATH는 ARG가 아니라 ENV라서 --build-arg로 바꿀 수 없고, 모듈의 pom.xml이 Docker 빌드에 넘기는 빌드 인자도 PROJECT_VERSION 하나뿐이라는 점을 확인했기 때문에, 리터럴 경로가 COPY 목적지와 어긋날 일은 없다고 봤습니다.

 

동작 변경이므로 RELEASE-NOTES.md의 버그 수정 목록에 Proxy: Fix Proxy Native Docker image failing to start due to unexpanded LOCAL_PATH in ENTRYPOINT 항목도 더했습니다. 단위 테스트를 추가할 대상이 없었고, PR 본문에도 그 이유를 적었습니다. 로컬에서는 ./mvnw spotless:check checkstyle:check apache-rat:check -Pcheck -T1C -pl distribution/proxy-native로 하드 게이트만 확인했습니다. 이 모듈의 전체 install은 GraalVM Native Image 빌드를 유발하기 때문에 로컬에서 돌리지 않았고, 이 점도 체크리스트에 그대로 남겼습니다.

리뷰에서 받은 지적: ENV 한 줄이 남긴 문제

메인테이너 리뷰는 변경 요청으로 돌아왔습니다. 리뷰어는 ENTRYPOINT의 변수를 절대 경로로 바꾸는 방향 자체는 맞다고 인정하면서, 남겨 둔 ENV LOCAL_PATH가 새 문제를 만든다고 근거를 달아 설명해 주었습니다.

 

Docker 레퍼런스에 따르면 ENV로 선언한 값은 이미지에 남아 컨테이너 실행 시에도 유지됩니다. 그런데 수정 후의 LOCAL_PATH는 이미 끝난 COPY에도, 하드코딩된 ENTRYPOINT에도 영향을 주지 못합니다. 사용자가 -e LOCAL_PATH=...로 설치 경로를 바꿀 수 있을 것처럼 보이지만 실제로는 아무 효과가 없는 설정이 이미지에 노출되는 것입니다. 또 설치 경로가 COPY의 ${LOCAL_PATH}와 ENTRYPOINT의 리터럴, 두 곳에서 따로 관리되게 됩니다. 누군가 나중에 ENV 값만 바꾸면 배포본은 새 위치로 복사되는데 ENTRYPOINT는 옛 위치를 가리켜, 이 PR이 고치려던 기동 실패가 다시 생길 수 있다는 지적이었습니다. 다른 메인테이너도 같은 PR에서 ENV LOCAL_PATH 정의는 왜 함께 지우지 않았는지 물었습니다.

 

돌아보면 저는 "ENV라서 build-arg로 바꿀 수 없으니 값이 어긋나지 않는다"는 지금 시점의 등가성만 확인했고, 앞으로 이 파일을 고칠 사람이 보게 될 구조까지는 생각하지 못했습니다.

두 번째 수정: 경로를 한 가지 표현으로

후속 커밋에서는 세 Dockerfile 모두에서 ENV LOCAL_PATH 줄을 지우고, COPY의 목적지도 절대 경로로 바꿨습니다. 머지된 최종 형태는 다음과 같습니다.

ARG PROJECT_VERSION
COPY --from=nativebuild /build/distribution/proxy-native/target/apache-shardingsphere-${PROJECT_VERSION}-shardingsphere-proxy-bin /opt/shardingsphere-proxy
ENTRYPOINT ["/opt/shardingsphere-proxy/bin/shardingsphere-proxy-native", "3307", "/opt/shardingsphere-proxy/conf", "0.0.0.0"]

PROJECT_VERSION은 여전히 ARG로 받는데, COPY는 빌더가 변수를 치환하는 명령이라 이 부분은 원래대로 동작합니다. 리뷰 코멘트에는 지적이 맞다는 점과 무엇을 바꿨는지만 짧게 답했습니다.

 

두 번째 리뷰에서 메인테이너는 assembly 설정이 바이너리를 bin, 설정을 conf 아래에 두어 새 경로와 맞는다는 점과 릴리스 노트 항목까지 확인한 뒤 승인했고, PR은 2026년 7월 22일에 master에 머지되었습니다. 전체 변경은 apache/shardingsphere#39146에서 볼 수 있습니다.

돌아보며

이번 기여에서 가장 오래 남은 것은 Dockerfile 문법 하나보다, "고친다"의 범위를 어디까지로 볼 것인가에 대한 감각이었습니다. 처음에는 변경을 작게 유지하는 것이 곧 좋은 수정이라고 생각해 ENTRYPOINT만 건드렸습니다. 변경을 작게 유지하라는 저장소의 원칙은 지금도 맞다고 생각합니다. 다만 리뷰를 거치면서, 작게 고친다는 것이 줄 수를 최소로 하는 것만을 뜻하지는 않는다는 점을 직접 겪으며 배웠습니다. 결함의 원인이 "같은 값을 두 군데서 따로 들고 있는 구조"라면, 그 구조를 남긴 채 한쪽만 고치는 것은 반쪽짜리 수정일 수 있습니다.

또 하나는 빌드가 성공한다고 해서 이미지가 동작한다는 보장은 없다는 점입니다. 앞으로 Dockerfile을 볼 때는 각 명령이 변수를 누가, 언제 해석하는지를 먼저 확인하려고 합니다.

 

마지막으로, 리뷰어가 공식 문서 링크와 함께 문제의 영향까지 구체적으로 적어 준 덕분에 저도 무엇을 왜 바꿔야 하는지 바로 납득하고 후속 커밋을 올릴 수 있었습니다. 저도 앞으로 PR 본문과 코멘트를 쓸 때 근거와 영향을 함께 적는 습관을 이어 가려고 합니다.