정책을 코드로 · validate 정책 · 이론
validate — 패턴, deny, 그리고 매치 범위
한 줄 요약
검증 정책의 품질은 조건식이 아니라 매치 범위와 실패 메시지로 결정된다.
왜 이게 필요했나
정책을 처음 쓰는 사람은 조건을 어떻게 표현할지부터 고민한다. 그런데 실제로 사고를 내는 것은 조건이 아니다. 조건이 틀리면 테스트에서 잡히지만, 매치 범위가 틀리면 아무 일도 안 일어나기 때문에 아무도 모른다. 클러스터 전체에 건 정책이 kube-system 의 시스템 파드를 막아 노드 부팅을 방해하거나, 반대로 네임스페이스 하나를 빠뜨려 그곳만 규칙 밖에 있는 상태가 몇 달씩 유지된다.
그래서 검증 정책을 쓰는 순서는 조건이 아니라 범위부터다. 어떤 종류(kinds), 어떤 네임스페이스(namespaces), 어떤 라벨(selector)에 걸 것인가. 그리고 반드시 짝을 이루는 질문이 있다. 무엇을 뺄 것인가(exclude). 클러스터 컴포넌트, 정책 엔진 자신, 그리고 아직 정리되지 않은 레거시 네임스페이스는 처음부터 빼 두는 편이 낫다.
어떻게 동작하나
검증 규칙에는 조건을 표현하는 방식이 여러 개 있고, 표현력과 읽기 쉬움이 반비례한다.
| 방식 | 쓰는 자리 | 성격 |
| --- | --- | --- |
| validate.pattern | 필드가 있어야 한다 / 값이 특정 모양이어야 한다 | 오브젝트와 같은 모양으로 쓰므로 읽기 쉽다 |
| validate.deny.conditions | 목록 비교, 문자열 매칭 등 패턴으로 못 쓰는 조건 | 명시적 거부. any 는 하나라도 참, all 은 전부 참 |
| validate.cel | 계산이 필요한 조건 | 표현력은 높지만 팀 전체가 읽을 수 있어야 한다 |
| validate.foreach | 컨테이너처럼 목록의 각 항목마다 | 항목별로 판정하고 메시지도 항목별로 낸다 |
패턴에는 연산자가 붙는다. ?* 는 "비어 있지 않은 아무 값", * 는 "아무 값(없어도 됨)", X|Y 는 택일, !X 는 부정, >=256Mi 같은 수치 비교도 된다. metadata.labels 아래에 team: "?*" 라고 쓰면 "team 라벨이 있고 비어 있지 않을 것"이 된다.
앵커는 패턴의 의미를 바꾸는 접두사다. 조건 앵커 () 는 "이 값이 맞을 때만 아래를 검사", 동등 앵커 =() 는 "이 키가 있으면 값이 같아야", 부정 앵커 X() 는 "이 키가 있으면 안 됨"이다. 특히 부정 앵커를 값 비교로 착각하는 실수가 잦다. X(privileged) 는 privileged 가 false 여야 한다는 뜻이 아니라 그 키가 존재하면 안 된다는 뜻이다.
preconditions 와 deny.conditions 는 모양이 비슷해서 자주 헷갈린다. 차이는 결과가 아니라 평가 여부다. preconditions 가 거짓이면 그 규칙은 아예 실행되지 않고 넘어간다(skip). deny.conditions 가 참이면 규칙은 실행됐고 그 결과가 거부다. 리포트에 남는 모습도 다르다 — 전자는 흔적이 없고 후자는 fail 로 남는다. 그래서 "이 규칙은 CREATE 요청에만 적용" 같은 것은 preconditions 로, "이 이미지는 안 된다" 는 deny 로 쓴다.
마지막으로 강제 수준이다. 예전에는 정책 수준 spec.validationFailureAction 하나가 정책 안의 모든 규칙을 지배했다. 지금은 규칙 단위 validate.failureAction 으로 옮겨 가는 중이고, 값은 Enforce(위반 요청을 막음)와 Audit(통과시키되 리포트에 남김) 두 가지다. 규칙 단위가 되면서 한 정책 안에서 어떤 규칙은 이미 강제하고 어떤 규칙은 아직 관찰만 하는 상태가 가능해졌다. 정책을 쪼개지 않고도 새 규칙을 조용히 얹어 며칠 지켜볼 수 있다는 뜻이다.
현장에서 만나는 모습
첫째, 메시지가 정책의 절반이다. 거부당하는 사람은 정책 YAML 을 읽을 수 없다. 그가 보는 것은 kubectl apply 가 뱉은 한 줄뿐이다. "Validation error" 같은 메시지는 문의를 만들고, "Pod 에는 team 라벨이 필요합니다 — 소유 팀을 적어 주세요" 같은 메시지는 스스로 고치게 만든다. 메시지 품질이 곧 정책 운영 비용이다.
둘째, 아무것도 매칭하지 않는 정책. 매치 블록의 오타나 잘못된 네임스페이스 때문에 정책이 통째로 비어 있는 경우가 흔하다. 이걸 잡는 방법은 하나뿐이다. 통과해야 할 리소스와 막혀야 할 리소스를 둘 다 넣어 보는 것. 통과만 확인하면 "아무것도 매칭 안 되는 정책"과 "잘 도는 정책"을 구분할 수 없다.
셋째, 정책보다 예외가 먼저 필요할 때가 있다. 이미 돌고 있는 워크로드에 새 규칙을 걸면 반드시 걸리는 것들이 나온다. 전부 고칠 때까지 기다리면 정책은 영원히 도입되지 않는다. PolicyException 으로 정책 이름·규칙 이름·리소스 이름까지 좁혀서 예외를 문서로 남기고, 그 목록이 줄어드는 것을 관리하는 편이 낫다. 예외를 정책 전체에 거는 것은 정책을 끄는 것과 같다.
다음 실습에서 할 것
/root/policy/validate/require-labels.yaml 에 필수 라벨을 요구하는 ClusterPolicy 를 쓰고, 매치와 제외를 명시한다. 그다음 통과하는 파드와 걸리는 파드를 각각 넣어 판정이 갈리는 것을 확인한다. 이어서 레지스트리를 제한하는 deny 규칙과 preconditions 를 쓰고, 마지막에는 여러 리소스를 한 번에 검사한 PolicyReport 와 범위가 좁은 PolicyException 을 만든다. 이 환경에서는 클러스터가 요청을 막아 주지 않으므로, 판정은 kyverno apply 를 로컬에서 돌려 확인한다.