generate 는 왜 조용히 실패하는가
한 줄 요약
mutate 는 admission 에서 오브젝트를 고쳐서 돌려준다. generate 는 admission 에서 아무것도 만들지 않고 UpdateRequest 를 남긴 뒤 background 컨트롤러가 실제 생성을 한다. 이 차이 때문에 generate 는 admission 이 성공하고 리소스만 안 생기는, 가장 알아채기 어려운 형태로 실패한다.
왜 이게 필요했나
mutate 가 있는 이유는 검사만으로는 사람이 지치기 때문이다. "모든 파드에 팀 라벨을 붙이세요" 를 validate 로만 강제하면 수백 개 매니페스트에 같은 줄을 넣게 되고, 빠뜨린 팀은 배포가 막힌다. 조직 전체에 예외 없이 적용되는 값이라면 사람이 쓰게 하지 말고 시스템이 넣는 편이 낫다.
generate 는 다른 문제를 푼다. 네임스페이스가 만들어지는 순간에만 알 수 있는 사실이 있다. 새 네임스페이스에 default-deny NetworkPolicy 를 넣고 싶다면, 네임스페이스 생성 이벤트에 반응해야 한다. GitOps 로는 "네임스페이스가 생긴 뒤" 라는 시점을 표현하기 어렵고, 사람에게 맡기면 잊는다.
새 네임스페이스에 NetworkPolicy 를 넣는 generate 규칙의 경로다. mutate 는 어드미션 안에서 끝나지만 generate 는 그렇지 않다.
- 어드미션은 통과시키고 UpdateRequest 만 남긴다네임스페이스 생성은 성공한다. 이 시점에 NetworkPolicy 는 아직 만들어지지 않았다.
- background 컨트롤러가 실제로 만든다UpdateRequest 를 처리하는 쪽이다. 최소 권한으로 설치되므로 표준 리소스가 아닌 것을 만들려면 규칙을 쓰는 쪽이 그 리소스의 권한을 붙여야 한다. 권한이 없으면 어드미션은 성공하고 리소스만 조용히 안 생긴다.
- synchronize 가 true 면 계속 지켜본다원본이 바뀌면 생성물도 따라 바뀌고 생성물을 손으로 고치면 되돌린다. 대상 네임스페이스 수만큼 감시와 쓰기가 늘어난다.
여기서 구분할 것 네임스페이스를 만든 직후에 곧바로 생성물을 확인하면 아직 없을 수 있다. 버그가 아니라 어드미션과 실제 생성이 서로 다른 컨트롤러의 일이라서 생기는 시간차다.
잠깐, 예측해 보세요 네임스페이스를 만들고 3초 뒤 NetworkPolicy 를 조회했더니 없었는데 30초 뒤에는 있었다. 이 정책을 고쳐야 할까?
설명 확인 · 채점 없는 자가 점검
고칠 이유가 없다. generate 는 어드미션에서 리소스를 직접 만들지 않고 background 컨트롤러가 나중에 만든다. 시간이 지나도 안 생길 때 비로소 UpdateRequest 가 있는지부터 본다.
어떻게 동작하나
mutate 의 문법은 두 가지다. patchStrategicMerge 는 쿠버네티스의 전략적 병합 패치라 배열을 이름 키로 병합한다. 컨테이너 목록에 사이드카를 하나 더하는 것처럼 "기존을 남기고 얹는" 작업에 알맞다. patchesJson6902 는 RFC 6902 JSON Patch 라 op/path/value 로 정확한 위치를 지목한다. 경로가 슬래시 표기이고, 키 안에 슬래시가 들어가면 ~1 로 이스케이프해야 한다는 점이 실무에서 자주 걸린다. kca.io/owner 어노테이션을 추가하려면 경로가 /metadata/annotations/kca.io~1owner 가 된다. foreach mutate 는 컬렉션의 원소마다 패치를 적용한다.
generate 의 문법은 data(정책에 값을 직접 쓴다)와 clone(다른 네임스페이스의 리소스를 복제한다) 둘로 갈린다. 둘을 함께 쓸 수는 없다. 여기에 synchronize 가 붙는다. true 면 원본이 바뀔 때 생성물도 따라 바뀌고, 생성물을 손으로 고치면 되돌린다.
문제는 이 편의가 공짜가 아니라는 점이다. synchronize 는 대상 네임스페이스 수만큼 감시와 쓰기를 늘린다. 네임스페이스 다섯 개짜리 클러스터에서는 아무 느낌이 없지만 수백 개짜리에서는 background 컨트롤러의 상시 부하가 된다. 그리고 이 컨트롤러는 최소 권한만 갖고 설치된다. 표준 리소스가 아닌 것을 generate 하기 시작하면 그 리소스에 대한 권한을 쓰는 쪽이 붙여야 하고, 권한이 없으면 admission 은 성공하고 리소스만 조용히 안 생긴다.
그래서 진단 순서가 정해져 있다. 생성물이 안 보일 때 정책 YAML 부터 들여다보지 말고 UpdateRequest 를 먼저 본다. kubectl -n kyverno get updaterequests 가 비어 있으면 match 가 안 된 것이고, 요청은 있는데 리소스가 없으면 background 컨트롤러의 권한이나 동작 문제다. 이 한 번의 분기로 문제 범위가 절반으로 줄어든다. 권한 확인은 kubectl auth can-i <동사> <리소스> --as system:serviceaccount:kyverno:kyverno-background-controller 로 한다. 그리고 네임스페이스를 만든 직후 곧바로 생성물을 확인하면 아직 없을 수 있다는 점도 기억해야 한다. 이것은 버그가 아니라 설계다.
마지막으로 mutate 의 운영상 반대급부를 짚고 넘어가자. mutate 로 넣은 값은 Git 에 보이지 않는다. Helm values 에 넣으면 리뷰되고 태그로 롤백되지만, mutate 로 넣으면 클러스터에서만 보인다. 반년 뒤에 왜 이 어노테이션이 붙어 있는지 아무도 모르는 상태가 되고, 매니페스트와 실제 오브젝트가 다르다는 사실이 GitOps 도구의 드리프트 감지와 계속 싸운다. 조직 전체에 강제해야 하는 값만 mutate 로 두고, 팀이 바꿀 수 있어야 하는 값은 차트에 두는 것이 기준이다.
현장에서 만나는 모습
저자의 홈랩은 GitOps 를 ArgoCD 로 굴리고 있고 10.0.0.201 로 떠 있다. 여기서 mutate 와 GitOps 가 충돌하는 모습을 직접 보게 되는데, 조정 루프가 Git 의 매니페스트와 클러스터 실제 상태를 계속 비교하는 도구이기 때문이다. Kyverno 가 넣은 라벨이나 사이드카는 Git 에 없으므로 diff 에 잡히고, 도구가 그것을 지우려 들면 Kyverno 가 다시 넣는다. 해결은 diff 에서 그 경로를 빼거나 mutate 대신 차트 기본값으로 옮기는 것인데, 이 판단을 미루면 두 컨트롤러가 서로를 되돌리는 상태가 오래 이어진다.
또 하나 이 클러스터에서 반복 확인한 것이 권한과 관측의 관계다. GPU Operator 사고나 KubeVirt 사고 모두 "상태 표시는 정상인데 실제로는 동작하지 않는" 형태였고, generate 의 조용한 실패도 정확히 같은 계열이다. 그래서 generate 를 쓰는 정책을 배포할 때는 정책 자체보다 먼저 background 컨트롤러의 권한을 확인하는 절차를 넣어 두는 편이 낫다.
같은 라벨이나 어노테이션을 어디에서 넣느냐의 비교다. 본문은 Helm values 를 예로 든다.
- Helm values 나 차트 기본값으로 넣은 값Git 에 보이고 코드 검토를 거치며 태그로 롤백된다. 팀이 바꿀 수 있어야 하는 값은 이쪽이 맞다.
- mutate 로 넣은 값클러스터에서만 보인다. 반년 뒤에 이 어노테이션을 누가 왜 붙였는지 아무도 모른다. 조직 전체에 예외 없이 강제할 값만 이쪽에 둔다.
여기서 구분할 것 Git 의 매니페스트와 실제 오브젝트가 달라져 GitOps 의 드리프트 감지와 부딪힌다. Kyverno 가 넣은 라벨이 diff 에 잡혀 지워지면 Kyverno 가 다시 넣는 순환이 생긴다. 이 충돌 서술의 근거는 본문의 홈랩 경험이다.
잠깐, 예측해 보세요 ArgoCD 가 Kyverno 가 넣은 라벨을 지우고 Kyverno 가 다시 넣기를 반복한다. 이 순환을 끊는 방법 두 가지는?
설명 확인 · 채점 없는 자가 점검
diff 에서 그 경로를 빼거나, mutate 대신 차트 기본값으로 값을 옮긴다. 이 판단을 미루면 두 컨트롤러가 서로를 되돌리는 상태가 오래 이어진다.
다음 실습에서 할 것
/root/kca-mutate/ 에 라벨 주입과 사이드카 주입 mutate 규칙, JSON Patch 규칙, NetworkPolicy 를 만드는 generate 규칙과 ConfigMap 을 복제하는 clone 규칙을 작성한다. 그리고 background 컨트롤러가 복제 원본을 읽을 수 있도록 Role 과 RoleBinding, 서비스어카운트를 실제로 만들고 auth can-i 로 검증한다.