IT/컨테이너

Alpine 베이스 이미지를 Wolfi로 갈아치우면서 삽질한 이야기

gfrog 2026. 5. 2. 21:15
반응형

 

지난 한 달 반 동안 우리 팀 컨테이너 이미지 베이스를 alpine에서 Wolfi로 거의 다 갈아치웠다. 처음엔 한 주 잡고 시작한 일이었는데, 막상 들어가보니 musl/glibc 차이부터 시작해서 빌드 캐시, CI 시간, 배포 호환성까지 줄줄이 엮여 있어서 결국 6주 가까이 걸렸다. 솔직히 시작할 때 알았으면 일정을 더 넉넉하게 잡았을 거다.

이 글은 그 과정에서 마주친 문제들과 해결한 방법, 그리고 아직 풀지 못한 것들에 대한 기록이다.

왜 옮겼나

원래 우리는 거의 모든 서비스 베이스가 alpine:3.19 였다. 30MB도 안 되는 사이즈가 매력적이었고, 5년 가까이 큰 문제 없이 잘 써왔다. 근데 작년 4분기부터 보안팀이 들고 온 Trivy 리포트가 점점 두꺼워지기 시작했다. 특히 CVE 누적 속도가 alpine 패치 릴리즈 주기보다 빨랐다. apk 캐시 한 번 비웠다고 사라지는 CVE가 아니라 base image 자체에 박혀 있는 것들.

그러던 중에 보안팀에서 "Chainguard Wolfi 한번 검토해보자"는 얘기가 나왔다. Wolfi는 Chainguard에서 컨테이너 전용으로 만든 distroless-friendly 배포판인데, alpine과 다르게 musl 대신 glibc를 쓴다. 그리고 커널이 없다. 컨테이너에서 어차피 호스트 커널 쓰니까 굳이 넣을 이유가 없다는 철학이다.

CVE도 거의 zero에 가깝게 유지되고 SBOM이 이미지마다 첨부되어 있어서, 보안팀 입장에선 매력적이었다. 그래서 우리 팀도 PoC 가서 본격적으로 마이그레이션 들어갔다.

첫번째 삽질: musl과 glibc는 같이 못 산다

처음에 한 짓이 진짜 무식했다. Dockerfile에서

FROM alpine:3.19 AS builder
# ... 빌드 ...

FROM cgr.dev/chainguard/wolfi-base
COPY --from=builder /app/bin /usr/local/bin/

이렇게 멀티스테이지로 빌더 단계만 alpine으로 두고 런타임만 Wolfi로 바꿨다. 빌드는 멀쩡하게 됐다. 근데 컨테이너 띄우면 바로 죽는다.

exec /usr/local/bin/myapp: no such file or directory

처음엔 PATH 문제인 줄 알고 30분쯤 헤맸다. 근데 binary 자체는 거기 있다. file 명령어 찍어보면 잘 나온다. 뭐지 싶어서 alpine 컨테이너에서 ldd 실행해봤더니:

/lib/ld-musl-x86_64.so.1 (0x7f...)

아, musl로 링크된 바이너리를 glibc 환경에 가져다 놓았으니 동적 로더부터 안 맞는 거였다. 정적 링킹된 Go 바이너리는 괜찮았는데, libc에 링크된 것들은 죄다 안 됐다. 특히 Python wheel 중에 alpine에서 받아온 manylinux 호환 안 되는 것들이 골치였다.

Chainguard 문서에도 명시되어 있는데, 둘은 binary 호환이 안 된다. 빌더부터 Wolfi로 바꿔야 한다. 이게 머리로는 알고 있던 사실인데, 멀티스테이지 빌드를 너무 오래 굴리다 보니 깜빡한 거다.

결국 빌더를 cgr.dev/chainguard/wolfi-base 또는 언어별 cgr.dev/chainguard/python:latest-dev 같은 이미지로 바꾸는 작업을 하나하나 했다.

두번째 삽질: 패키지 이름이 미묘하게 다르다

apk add로 깔던 패키지들을 그대로 옮길 수 없다. Wolfi는 자체 apk 저장소를 쓰고, 이름 규칙도 약간 다르다. 예를 들어:

  • alpine의 ca-certificates → Wolfi의 ca-certificates-bundle
  • alpine의 tzdata → Wolfi의 tzdata (이건 같음)
  • alpine의 bash → Wolfi에도 있지만 권장은 busybox sh로 충분
  • alpine의 python3 → Wolfi에선 보통 python-3.X 형태로 버전 명시

이거 매핑하는 데만 한 이틀 정도 걸렸다. 우리는 서비스가 한 30개쯤 되는데 각자 의존성이 미묘하게 달라서, 결국 표 하나 만들어서 팀 위키에 올렸다.

특히 헷갈렸던 게, Wolfi의 wolfi-base는 진짜 작다. apk, busybox 정도밖에 없다. alpine에서 당연히 들어 있던 것들 (예: wget, tar, grep)이 다 없을 수 있다. 필요하면 명시적으로 깔아야 한다.

세번째 삽질: 이미지 사이즈가 줄지 않았다

기대했던 거: alpine보다 더 작아질 것이다.
실제: 거의 비슷하거나 약간 더 컸다.

처음엔 좀 충격이었다. alpine이 워낙 musl 기반으로 작게 만든 distro라서, glibc로 바꾸면 libc만 5MB 정도 더 들어간다. 거기에 우리가 깔던 패키지들이 alpine 버전보다 미묘하게 더 큰 경우도 있었다.

근데 며칠 써보니 사이즈는 별 의미 없는 지표였다. 진짜 차이는:

  1. CVE 개수: 우리 한 서비스 기준 alpine은 Trivy HIGH/CRITICAL 합쳐서 12개 정도, Wolfi는 0~1개.
  2. SBOM: 이미지 빌드 시점에 SBOM이 자동으로 붙는다. cosign download sbom으로 바로 뽑힌다.
  3. glibc 호환성: alpine에서 manylinux wheel 안 맞아서 source build 하던 Python 의존성들이 Wolfi에선 그냥 깔린다. 빌드 시간이 줄어든 케이스도 있었다.

사이즈 1MB 줄이려고 하는 게 아니었다는 걸 그제야 깨달았다. 우리가 alpine으로 옮겼을 때의 동기와, Wolfi로 옮기는 동기가 완전히 달랐던 거다.

CI 빌드 시간이 처음엔 늘었다가 다시 줄었다

이건 좀 의외였다. 마이그레이션 첫 주에 CI 평균 빌드 시간이 30% 정도 늘었다. 원인은 두 가지:

  • BuildKit 캐시 미스: 베이스 이미지 바뀌면 layer cache가 다 날아간다. 어쩔 수 없음.
  • cgr.dev 레지스트리 pull 속도가 도커허브보다 약간 느렸다 (특히 한국 리전에서).

두번째 문제는 ECR pull-through cache를 cgr.dev에 대해 설정해서 해결했다. 한 번 캐시되면 사내 ECR에서 받아오니까 빠르다.

resource "aws_ecr_pull_through_cache_rule" "chainguard" {
  ecr_repository_prefix = "chainguard"
  upstream_registry_url = "cgr.dev"
}

3주차쯤부터는 캐시 워밍이 어느 정도 되어서 평균 빌드 시간이 alpine 시절과 거의 비슷하게 돌아왔다.

아직 못 옮긴 것들

전부 다 옮기진 못했다. 몇 가지 케이스가 남아 있다:

  • Legacy Python 2.7 서비스 하나: Wolfi는 Python 2를 지원 안 한다. 어차피 EOL이라 옮기든 폐기하든 결정해야 하는데, 그 결정이 아직 안 났다.
  • Confluent kafka-connect 이미지를 베이스로 쓰는 컨테이너: 우리가 만든 게 아니라 벤더 이미지라 베이스 교체가 안 된다. 이건 어쩔 수 없이 alpine 기반 그대로 둘 듯.
  • 빌드 시점에 root 권한 필요한 native extension 빌드: Chainguard 이미지는 기본 nonroot라 일부 빌드가 깨진다. -dev 버전 이미지를 쓰면 되긴 하는데, 빌더 따로 런타임 따로 관리하는 부담이 있다.

그래서 좋아졌나

Trivy 리포트가 깔끔해졌다. 그게 제일 큰 거다. 보안팀에서 "이거 왜 이렇게 CVE 많아요?" 미팅이 사라졌다. 그것만으로도 옮긴 보람이 있다고 본다.

다만 솔직히 말하면, 옮기는 과정 자체는 즐겁지 않았다. 일정이 거의 두 배 늘었고, 중간에 한 번은 dev 환경에서 배포가 깨져서 멘탈이 좀 흔들렸다. 작은 팀이거나 서비스 종류가 단순한 경우에는 한 주 만에 끝낼 수도 있을 것 같은데, 우리처럼 30개 서비스에 언어 스택이 5종류 섞여 있으면 한 달 반은 그냥 잡아야 한다.

그리고 한 가지 더 — Wolfi 자체가 비교적 새로운 distro다 보니, 가끔 패키지가 alpine 만큼 풍부하지 않다. 그럴 때 Chainguard에 이슈를 올리거나, 우리가 직접 melange로 패키지를 빌드해서 쓰기도 했다. 이 부분은 아직 정착이 안 됐다고 본다.

다음에는 melange로 사내 패키지를 빌드해서 Wolfi 이미지에 통합하는 워크플로를 정리해서 글로 써볼까 한다. 혹시 이미 운영 중인 분 있으면 어떻게 하시는지 댓글로 알려주시면 감사하겠다.

반응형