Kubernetes · Reliability
아무도 선언하지 않은
기본값들
drift 검사기는 Git에 선언한 내용과 클러스터에서 실제로 돌고 있는 상태를 비교한다. 방금 깔끔하게 적용해서 아무도 손대지 않은 클러스터에 돌렸는데, 온통 drift라고 나왔다. 클러스터가 거짓말을 한 건 아니다. Git에 적지도 않은 필드를 클러스터가 알아서 채워 넣었고, 검사기는 그걸 구별할 방법이 없었다.
drift 탐지는 단순한 diff처럼 들린다. Git에 선언한 것과 돌고 있는 것을 가져와 비교하고, 다른 곳을 표시하면 된다. 이걸 손으로 쓴 픽스처가 아닌 실제 클러스터로 검증하려고 이렇게 했다. 정상인 걸 아는 매니페스트를 쓰고 버릴 kind 클러스터에 적용하고, kubectl get -o yaml로 라이브 상태를 그대로 덤프했다. 그리고 그 캡처를 원본 Git 소스와 비교했다. 깔끔하게 적용하고 몇 초 뒤, 뭔가 바뀔 틈도 없을 때였다.
비어 있어야 했던 diff
diff는 비어 있지 않았다. 거의 모든 리소스에서, 아무도 건드리지 않은 필드에 drift가 찍혔다. spec.strategy, imagePullPolicy, resources, dnsPolicy, securityContext 등이 모두 라이브 캡처에는 있고 Git 매니페스트에는 없었다. 누가 몰래 클러스터를 바꾼 것도 아니다. 적용 직후에 처음 읽은 상태였다. 리소스가 만들어지는 순간 API 서버와 admission 기본값 처리가 이 필드들을 알아서 채운 것이다. Kubernetes는 원래 그렇게 동작하도록 설계돼 있다.
strategy를 지정하지 않은 Deployment라고 전략 없이 도는 게 아니다. API 서버가 RollingUpdate를 골라 객체 스펙에 다시 써 넣는다. imagePullPolicy가 없는 컨테이너는 이미지 태그를 보고 값을 하나 받는다. 이 중 drift는 하나도 없다. 하지만 객체 전체를 그대로 비교하는 검사기 눈에는 전부 drift로 보였다.
이 전제로 만든 도구는 모두 같은 가정을 깔고 간다. 아무것도 바뀌지 않았다면 선언과 라이브 상태가 필드 하나하나까지 같아야 한다는 가정이다. 플랫폼의 admission 계층이 무언가를 다시 써 넣을 수 있다면 이 가정은 바로 깨진다. 그리고 Kubernetes는 설계상 꽤 많은 걸 써 넣는다. 이걸 고려하지 않은 검사기는 방금 적용해서 완벽하게 정상인 클러스터에 drift를 최대로 보고한다. drift 신호가 전해야 할 뜻과 정반대다.
똑똑한 diff 대신 작은 diff
해결책은 명시적인 허용 목록이다. API 서버나 admission 컨트롤러가 흔히 채워 넣는 것으로 알려진 키를 SERVER_DEFAULTED_KEYS라는 고정 집합으로 두고, drift를 판정하기 전에 비교에서 뺀다. 런타임에 추론하거나 맥락으로 짐작하지 않는다. 클러스터가 알아서 추가할 필드를 구체적으로 적어 두고 관리하는 목록이고, 문서만 믿지 않고 실제 클러스터 동작으로 한 번 확인했다. 이제 목록에 있는 필드가 라이브 캡처에만 있고 Git에 없으면 drift로 치지 않는다. 목록에 없는 필드가 그러면 지금처럼 drift로 잡힌다.
첫 번째 문제에 작은 문제가 하나 더 딸려 있었다. 컨테이너는 리스트인데, 리스트를 통째로 비교하면 컨테이너 하나에만 기본값 필드가 붙어도 리스트 전체가 실패한다. 나머지 컨테이너가 Git 선언과 똑같아도 마찬가지다. 그래서 컨테이너를 리스트 순서로 맞추지 않고 이름으로 맞춰 비교하게 했다. 컨테이너 하나에 정당하게 붙은 기본값 때문에 같은 Deployment 안의 다른 컨테이너까지 오탐에 휘말리지 않게 하려는 것이다.
왜 제대로 해야 하나
깔끔하게 적용할 때마다 양치기 소년처럼 drift를 외치는 검사는 슬그머니 무시되는 정도로 끝나지 않는다. 결국 꺼진다. 더 나쁘면 한 번도 깨끗했던 적이 없으니 다들 출력을 대충 넘기는 버릇이 든다. drift 신호의 가치는 조용함에 뜻이 있다는 데서 나온다. 출력이 없으면 정말로 아무것도 어긋나지 않았다는 뜻이어야 한다. "플랫폼이 설계대로 알아서 한 것"과 "누군가 Git 모르게 손으로 바꾼 것"을 구별하지 못하면, 비교 로직이 다른 면에서 아무리 정확해도 그런 조용함을 만들 수 없다.
선언해 둔 기준을 실제로 돌고 있는 시스템과 비교하는 도구라면 다 마찬가지다. 인프라 drift 감지기, config-as-code의 plan/apply diff, 마이그레이션 이력과 데이터베이스 스키마를 비교하는 도구가 모두 그렇다. 런타임이 알아서 채우는 기본값을 고려하지 않으면, 손대지 않은 정상 배포가 매번 어긋났다고 나온다. 허용 목록도 한 번 만들고 끝낼 일이 아니다. 어떤 필드가 자동으로 채워지는지는 플랫폼의 admission 컨트롤러와 그 버전에 따라 달라진다. 한 번 써 두고 계속 믿기보다는, 실제 동작에 비춰 주기적으로 다시 확인하는 게 낫다.
kyhsa93/k8s-playbook(Kubernetes 배포 실수를 유형별로 모으고, 매니페스트에서 그 실수를 찾아내는 검사기를 붙여 둔 내 예제 프로젝트. 손으로 쓴 전후 픽스처 대신 쓰고 버리는 실제 클러스터로 검증한 drift 검사 코드가 있다)