BlockChain

클러스터를 지웠는데 디스크가 안 줄어든다 — 익명 볼륨과 삭제 순서

  • #BlockChain
  • #AI
  • #Claude Code
  • #Docker
  • #Kubernetes
  • #셸스크립트

문제: 지웠는데 공간이 안 돌아온다

  • 폐쇄망 서버 한 대 위에 기업별 kind 클러스터가 여러 개 뜨는 구조
  • 클러스터를 만들고 지우는 일이 잦다 — 그때마다 삭제 스크립트가 돈다
  • 증상: 클러스터를 삭제해도 디스크 사용량이 줄지 않는다

에러는 없었다. 스크립트는 정상 종료하고, kind get clusters에도 안 나온다. 그런데 공간만 안 돌아온다.

원인: 이름으로 못 찾는 볼륨

정리 코드는 이랬다.

docker volume ls --filter "name=${CLUSTER_NAME}" --format "{{.Name}}"
  • 클러스터 이름이 들어간 볼륨을 찾아 지운다 — 합리적으로 보인다
  • 그런데 kind의 control-plane 컨테이너가 만드는 볼륨은 익명 볼륨이다
  • 익명 볼륨은 이름이 없다. Docker가 붙여주는 64자리 16진수 ID(4f2a9c...)가 이름 자리를 대신하므로 클러스터 이름이 안 들어간다

즉 필터가 잘못된 게 아니라 불완전했다. 이름 있는 볼륨은 잘 지웠고, 익명 볼륨만 매번 남았다. 클러스터를 지울 때마다 조용히 쌓인 것이다.

에러가 없었던 이유도 여기 있다 — 스크립트 입장에서는 지울 걸 다 지웠다.

왜 prune 을 쓰지 않았나

가장 쉬운 답은 docker volume prune 이다. 어디에도 안 붙은 볼륨을 한 번에 지운다. 익명이든 아니든 상관없다.

이걸 쓸 수 없었던 이유가 이 환경의 핵심 제약이다.

  • 이 서버에는 기업마다 하나씩, 여러 개의 kind 클러스터가 동시에 떠 있다
  • prune 은 대상을 “안 붙은 것 전부”로 잡는다 — 어느 클러스터 것인지 구분하지 않는다
  • 다른 기업 클러스터가 잠깐 컨테이너를 내린 상태라면, 그 볼륨까지 지운다
  • 폐쇄망이라 지운 이미지·데이터를 다시 받아올 수도 없다

정확히 지우는 비용을 치를 것인가, 넓게 지우는 위험을 감수할 것인가의 선택이었다. 단일 클러스터 개발 머신이라면 prune 한 줄이 맞다. 여러 테넌트가 한 호스트를 공유하면 그 한 줄이 남의 데이터를 지운다.

그래서 “이 클러스터가 쓰던 볼륨만 정확히” 골라내는 쪽을 택했고, 그 대가로 아래의 순서 문제를 만났다.

고치려다 만난 순서 문제

익명 볼륨도 찾아 지우면 될 것 같았다. 컨테이너가 어떤 볼륨을 물고 있는지는 docker inspect로 알 수 있다.

문제는 언제 부르느냐다.

  • 볼륨 정리는 자연스럽게 클러스터 삭제 뒤에 온다
  • 그런데 kind delete cluster가 끝나면 컨테이너가 사라진다
  • 컨테이너가 없으면 docker inspect로 마운트 정보를 볼 방법이 없다

조회의 근거가 되는 대상을 먼저 없애버린 것이다. 그래서 순서를 뒤집었다.

삭제 전에 캡처

CONTAINERS=$(docker ps -a --filter "name=${CLUSTER_NAME}" --format "{{.Names}}" 2>/dev/null || true)
ANONYMOUS_VOLUMES=""
if [ -n "$CONTAINERS" ]; then
  ANONYMOUS_VOLUMES=$(echo "$CONTAINERS" | xargs -I{} docker inspect {} \
    --format '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' \
    2>/dev/null | sort -u | grep -v '^$' || true)
fi
  • .Mounts를 순회하며 Typevolume인 것만 이름을 뽑는다
  • bind mount(호스트 디렉터리 연결)는 제외해야 한다 — 그건 지우면 안 되는 것이다
  • 이 블록이 kind delete cluster 앞에 온다는 게 이 수정의 전부다

삭제 후에 제거

# 6-1. 클러스터 이름이 들어간 볼륨 (named volume)
NAMED_VOLUMES=$(docker volume ls --filter "name=${CLUSTER_NAME}" --format "{{.Name}}" 2>/dev/null || true)
if [ -n "$NAMED_VOLUMES" ]; then
  echo "$NAMED_VOLUMES" | xargs docker volume rm 2>/dev/null || true
fi

# 6-2. 사전 캡처해둔 익명 볼륨 (sha256 해시 이름)
if [ -n "${ANONYMOUS_VOLUMES:-}" ]; then
  echo "$ANONYMOUS_VOLUMES" | xargs -r docker volume rm 2>/dev/null || true
fi
  • 기존 이름 매칭 코드(6-1)는 지우지 않고 남겼다. 틀린 게 아니라 부족했던 것이므로
  • xargs -r은 입력이 비면 명령 자체를 실행하지 않는다(GNU 확장). 빈 docker volume rm 호출을 막는다

회수량은 커밋에 이렇게 적어뒀다.

클러스터 1회 삭제당 약 8-12GB 회수 가능.

정직하게 말하면 이건 추정치다. “약”과 “가능”이라는 표현이 그 증거고, 측정 로그를 남겨두지 않았다. 클러스터 이미지 구성에 따라 달라질 값이라 범위로 적은 것인데, 다시 쓴다면 docker system df를 전후로 찍어 붙였을 것이다.

남는 교훈

되짚을 수 없는 삭제 앞에서는, 목록 확보가 삭제보다 먼저 온다.

  • 지우고 나서 “뭘 지웠더라”를 알아낼 방법이 없는 자원이 있다
  • 컨테이너 → 마운트 볼륨이 그렇고, 프로세스 → 열린 파일, 파드 → PVC도 비슷하다
  • 정리 스크립트를 쓸 때는 “이 조회가 앞 단계 때문에 불가능해지지 않나”를 먼저 본다

조용한 실패가 가장 오래간다.

  • 이 버그는 에러를 안 냈다. 스크립트는 매번 성공했고 로그도 깨끗했다
  • 발견 경로가 “디스크가 왜 안 줄지”라는 관찰이었다는 게 문제다 — 코드가 알려준 게 아니다
  • 지금은 삭제할 볼륨 목록을 로그로 남긴다. 무엇을 지웠는지 출력하지 않는 정리 스크립트는 지웠는지 아닌지 알 수 없다

필터가 매칭하지 못하는 대상이 있는지 의심한다.

  • --filter name=은 이름이 있는 것에만 쓸 수 있다. 익명 자원에는 애초에 안 통한다
  • 이름 기반 조회를 쓸 때는 “이름이 없는 케이스가 존재하나”가 첫 질문이어야 한다
  • 여기서는 그 답이 “있다, 그리고 그게 제일 크다”였다

호스트를 여럿이 공유하면 “전부 지우는 명령”은 선택지에서 빠진다.

  • prune 계열은 대상을 소유자가 아니라 상태(“안 쓰는 것”)로 고른다
  • 테넌트가 하나면 그 정의가 맞고, 여럿이면 남의 것까지 포함된다
  • 편한 명령이 안 되는 순간 정확한 목록을 직접 만들어야 하고, 그 목록을 언제 만드느냐가 위의 순서 문제였다