LabHub
배우기 러닝패스 코스

KCA — Kyverno 인증 어소시에이트 · 정책 작성 심화 — 조건·외부 데이터·자동 생성·정리 · 이론

match 가 못 보는 것을 preconditions 와 context 가 본다

LabHub 에서 이어서 보기

한 줄 요약

match/exclude 는 종류·이름·라벨 같은 겉모양만 고릅니다. 리소스의 spec 안을 들여다보거나 다른 리소스와 비교해서 규칙을 적용할지 정하려면 preconditionscontext 가 필요합니다. 이 글은 그 둘이 어떤 순서로 평가되고, 어디까지 할 수 있는지를 [공식 문서의 preconditions 절](https://kyverno.io/docs/policy-types/cluster-policy/preconditions/)과 [외부 데이터 소스 절](https://kyverno.io/docs/policy-types/cluster-policy/external-data-sources/)을 따라 설명합니다.

왜 이게 필요했나

"NodePort 서비스는 반드시 externalTrafficPolicy: Local 이어야 한다" 는 정책을 쓴다고 해 봅시다. match 블록은 kinds: [Service] 까지만 고를 수 있고, spec.typeNodePort 인지는 볼 수 없습니다. 그래서 모든 Service 가 일단 규칙에 걸리고, 그중 NodePort 만 골라내는 두 번째 관문이 필요합니다. 그 관문이 preconditions 입니다.

두 번째 요구는 더 자주 옵니다. "이 네임스페이스에 이미 파드가 몇 개인지", "허용 레지스트리 목록이 ConfigMap 에 있는데 그것과 비교하고 싶다", "요청한 사람이 실제로 권한이 있는지 SubjectAccessReview 로 물어보고 싶다". 이런 판단은 요청 본문(AdmissionReview) 만으로는 할 수 없고, 다른 곳에서 데이터를 가져와야 합니다. 그것이 context 입니다.

어떻게 동작하나

평가 순서

문서는 순서를 분명히 적습니다. 리소스가 match 에 걸리고 exclude 에 빠지지 않은 뒤에 preconditions 가 평가되고, 전체가 TRUE 일 때 규칙 본문(validate·mutate 등) 이 실행됩니다. match/exclude 안에서는 변수를 쓸 수 없습니다 — [변수 문서](https://kyverno.io/docs/policy-types/cluster-policy/variables/)가 "데이터를 읽어 들이지 않고 규칙을 빨리 고르기 위해서" 라고 이유를 밝힙니다. 반면 preconditions 는 변수·JMESPath·연산자를 전부 씁니다.

결과 보고에서도 둘은 다릅니다. exclude 에 걸린 리소스는 아예 무시되지만, match 에 걸리고 preconditions 에서 떨어진 리소스는 skip 으로 채점됩니다. PolicyReport 에서 skip 이 많이 보이면 preconditions 가 걸러 낸 것입니다.

any 와 all

preconditions 의 표현식은 any 또는 all 블록 아래에 둡니다. any 는 논리 OR, all 은 논리 AND 이고, 한 규칙에 둘을 같이 둘 수도 있습니다. 문서의 표현대로 "각 any/all 블록이 전체적으로 TRUE 여야" 규칙이 진행되고, 하나라도 TRUE 가 아니면 규칙은 적용되지 않습니다. deny 규칙의 conditions 와 같은 구조이며, 같은 방식으로 단락 평가(short circuiting)합니다.

preconditions:  any:  - key: "{{ request.object.metadata.labels.color || '' }}"    operator: Equals    value: blue  - key: "{{ request.object.metadata.labels.app || '' }}"    operator: Equals    value: busybox  all:  - key: "{{ request.object.metadata.labels.env || '' }}"    operator: Equals    value: qa

표현식 하나는 key·operator·value 로 이루어집니다. 연산자는 Equals·NotEquals, GreaterThan·GreaterThanOrEquals·LessThan·LessThanOrEquals, 집합 비교인 AnyIn·AllIn·AnyNotIn·AllNotIn, 그리고 기간 비교인 DurationGreaterThan 계열입니다. 위 예시의 || '' 는 라벨이 없을 때 JMESPath 평가가 실패하지 않도록 빈 문자열로 받는 관용구인데, 선택 필드를 다룰 때 거의 항상 필요합니다.

요청에서 오는 변수

Kyverno 가 미리 만들어 주는 변수는 AdmissionReview 에서 옵니다. request.object 는 만들어지거나 바뀌는 오브젝트(DELETE 에서는 null), request.oldObject 는 바뀌기 전 오브젝트(CREATE 에서는 null), request.operationCREATE·UPDATE·DELETE·CONNECT 중 하나, request.userInfousernamegroups, request.namespace 는 대상 네임스페이스입니다. 여기에 serviceAccountName·serviceAccountNamespace, request.roles·request.clusterRoles, 컨테이너 이미지 정보를 담은 images 가 더해집니다.

한 가지 함정이 있습니다. 사용자 이름처럼 어드미션 요청에만 있는 변수를 규칙에 쓰면 정책의 backgroundfalse 로 두어야 합니다. 백그라운드 스캔은 이미 있는 리소스를 다시 훑는 것이라 요청 정보가 없기 때문이고, [CRD 문서의 kubectl explain 출력](https://kyverno.io/docs/crds/)에 그 조건이 그대로 적혀 있습니다.

context — 다른 곳에서 데이터를 가져오기

context 항목은 규칙 안에 정의하고, 정의한 순서대로 평가됩니다. 앞의 변수는 뒤에서 참조할 수 있지만 뒤의 것을 앞에서 참조하면 오류입니다. 종류는 다섯입니다.

| 종류 | 무엇을 하나 | 참조 방법 |
| --- | --- | --- |
| configMap | name·namespace 로 ConfigMap 을 읽는다 | {{ 이름.data.키 }} |
| apiCall | 쿠버네티스 API 나 외부 서비스를 호출한다 | urlPath + jmesPath |
| globalReference | 미리 캐시된 GlobalContextEntry 를 참조한다 | name |
| imageRegistry | OCI 이미지의 메타데이터를 가져온다 | reference + jmesPath |
| variable | JMESPath 로 계산한 값을 저장한다 | jmesPath + default |

apiCallkubectl get --raw 와 똑같은 경로를 씁니다. 문서는 그래서 정책에 넣기 전에 다음처럼 손으로 시험해 보라고 권합니다.

kubectl get --raw /api/v1/namespaces/kyverno/pods | kyverno jp query "items | length(@)"

같은 것을 정책에 옮기면 이렇게 됩니다. urlPath 안에도 변수를 쓸 수 있고, 기본 메서드는 GET 이며 method: POSTdata 를 주면 SubjectAccessReview 같은 쓰기 API 도 부를 수 있습니다. API 서버가 오류를 돌려줄 때를 대비해 default 로 대체값을 둘 수 있습니다.

context:- name: podCount  apiCall:    urlPath: "/api/v1/namespaces/{{ request.namespace }}/pods"    jmesPath: "items | length(@)"    default: 0

configMap 은 ConfigMap 에 cache.kyverno.io/enabled: "true" 라벨을 붙이면 Kyverno 가 자동으로 캐시해서, 정책 판단 때마다 API 서버를 부르지 않습니다. globalReference 는 한 걸음 더 나아간 것으로, GlobalContextEntry 리소스에 kubernetesResource(group·version·resource·namespace) 나 apiCall(외부 호출 + refreshInterval) 을 선언해 두면 Kyverno 가 informer 로 캐시를 유지하고, 여러 정책이 같은 캐시를 씁니다. 다만 GlobalContextEntry 가 준비되지 않으면 그것을 참조하는 정책도 준비되지 않은 상태가 되어 처리되지 않습니다.

권한도 잊으면 안 됩니다. apiCall 로 어떤 리소스를 읽으려면 Kyverno 컨트롤러의 ClusterRole 에 그 권한이 있어야 하는데, [설치 커스터마이즈 문서](https://kyverno.io/docs/installation/customization/)는 rbac.kyverno.io/aggregate-to-admission-controller: "true" 같은 라벨을 붙인 ClusterRole 을 만들어 집계(aggregate)하는 방식을 안내합니다. 1.13 부터 와일드카드 view 권한이 빠졌기 때문에 커스텀 리소스는 명시적으로 열어 주어야 합니다.

현장에서 만나는 모습

한 팀이 "네임스페이스당 LoadBalancer 서비스는 두 개까지" 라는 규칙을 넣었습니다. match 로는 개수를 셀 수 없으니 apiCall 로 그 네임스페이스의 서비스 목록을 가져와 items[?spec.type == 'LoadBalancer'] | length(@) 로 세고, preconditions 의 GreaterThanOrEquals 로 2 이상일 때만 deny 하도록 했습니다. 처음에는 어드미션 지연이 눈에 띄게 늘었는데, 원인은 요청마다 API 호출이 한 번씩 나가서였습니다. 문서가 말하는 대로 API 호출은 어드미션 요청마다 실행되므로, 자주 쓰는 데이터는 GlobalContextEntry 로 옮겨 캐시하는 것이 답이었습니다.

또 다른 팀은 사용자 이름을 preconditions 에 넣은 정책이 PolicyReport 에서 이상하게 보이는 것을 겪었습니다. 백그라운드 스캔에는 request.userInfo 가 없으니, 그 정책은 background: false 여야 했습니다.

다음 글에서 이어질 것

이어지는 글에서는 파드 규칙이 Deployment·CronJob 규칙으로 자동 생성되는 autogen 과, 정책이 리소스를 지우는 cleanup 을 다룹니다. 그다음 퀴즈에서는 이 글의 평가 순서, skipexclude 의 차이, context 다섯 종류를 확인합니다.