Deployment 创建成功了,却一个 Pod 都没有
한국어 원문으로 표시합니다.
목표
Kyverno 정책의 작성 도구(preconditions, apiCall 컨텍스트, 변수 기본값, autogen, JSON 패치, foreach, CEL, 백그라운드 스캔)가 실제 요청에서 무엇을 거절하고 무엇을 바꾸는지 진짜 Kyverno 1.19.1 에서 확인합니다.
왜 중요한가
정책의 문법이 맞다는 것과 의도대로 판정한다는 것은 다릅니다. match 는 리소스 종류와 위치로만 고르므로, 요청 내용에 따라 규칙을 켜고 끄려면 preconditions 가, 요청에 없는 정보가 필요하면 컨텍스트가 있어야 합니다. 변수가 해석되지 않으면 규칙은 판정 대신 오류를 내고, Enforce 에서는 그 오류가 거절로 보여서 원인을 엉뚱한 곳에서 찾게 됩니다. Pod 규칙은 기본적으로 컨트롤러용 규칙으로 자동 생성되는데, 이것을 끄면 Deployment 는 성공한 것처럼 보이고 파드만 조용히 생기지 않습니다. 변형 규칙은 요청마다 다른 값을 넣거나 목록의 일부만 고칠 수 있어야 하고, 이미 클러스터에 있던 리소스는 어드미션을 다시 거치지 않으므로 백그라운드 스캔으로만 드러납니다.
단계
- 네임스페이스
kca-pre를 만들고/root/kca-write/01-limits.yaml에 ClusterPolicylimits-for-prod(background false)를 작성해 적용하세요. 규칙memory-limit은kca-pre의 Pod 를 match 하고, preconditions 로 라벨tier가prod인 요청에만 적용되며(라벨이 없으면 빈 문자열로 취급), 모든 컨테이너에resources.limits.memory를 요구합니다(Enforce). match 에 라벨 selector 를 쓰지 않습니다. - 네임스페이스
kca-prod(라벨env=prod)와kca-dev(라벨env=dev)를 만들고,/root/kca-write/02-team.yaml에 ClusterPolicyteam-in-prod-ns(background false)를 적용하세요. 규칙team-when-prod는 두 네임스페이스의 Pod 에 대해 컨텍스트nsenv를 apiCall 로/api/v1/namespaces/{{request.namespace}}에서 읽어(라벨 env, 없으면none) env 가 prod 이고 team 라벨이 없을 때 거부합니다(Enforce). 그다음kca-dev의 env 를 잠시 prod 로 바꿔 라벨 없는 파드를 서버 dry-run 으로 보내 본 출력 전체를/root/kca-write/flip.txt에 저장하고, env 를 dev 로 되돌리세요. - 네임스페이스
kca-var에 ClusterPolicyno-platform-team(background false, 규칙not-platform)을 적용하세요. team 라벨이platform이면 거부하는 deny 조건인데, 처음에는 key 를 기본값 없이{{ request.object.metadata.labels.team }}로 씁니다. 라벨 없는 파드를 서버 dry-run 으로 보내 거절 출력 전체를/root/kca-write/missing.txt에 저장하세요. 그다음/root/kca-write/03-no-platform.yaml의 key 에 기본값(빈 문자열)을 더해 다시 적용해, 라벨 없는 파드는 받아들이고 team=platform 은 거절하게 하세요. - 네임스페이스
kca-auto와kca-noauto를 만들고, 각 네임스페이스의 Pod 에 readinessProbe 를 요구하는(Enforce, background false, 규칙need-probe) ClusterPolicyprobes-auto와probes-noauto를/root/kca-write/04-probes.yaml에 작성해 적용하세요.probes-noauto에만 어노테이션pod-policies.kyverno.io/autogen-controllers: none을 붙입니다. 그다음 두 네임스페이스에서kubectl create deployment web --image=nginx:1.27-alpine을 실행하고,kca-auto쪽 출력 전체를/root/kca-write/auto.txt에 저장하세요. - 네임스페이스
kca-mut를 만들고/root/kca-write/05-requested-by.yaml에 ClusterPolicyrequested-by(규칙annotate-user)를 작성하세요.kca-mut의 Pod 에 patchesJson6902 의 add 연산으로 어노테이션kca.io/requested-by를 넣고 값은{{request.userInfo.username}}입니다. 먼저 spec.background 를 적지 않고 적용해 거절되는 출력 전체를/root/kca-write/bg-denied.txt에 저장한 뒤, background 를 false 로 두어 적용하세요. 기존 어노테이션은 보존되어야 합니다. /root/kca-write/06-pull-policy.yaml에 ClusterPolicypull-policy(규칙latest-always)를 작성해 적용하세요.kca-mut의 Pod 에 대해 foreach 로 컨테이너마다 이미지가:latest로 끝나는지 원소 단위 preconditions 로 보고, 그런 컨테이너만 patchStrategicMerge 로imagePullPolicy: Always로 바꿉니다. 태그를 고정한 컨테이너의 imagePullPolicy 는 그대로여야 합니다.- 네임스페이스
kca-cel을 만들고/root/kca-write/07-no-priv-esc.yaml에policies.kyverno.io/v1ValidatingPolicyno-priv-esc를 적용하세요. validationActions 는[Deny], matchConstraints 는 core v1 pods 의 CREATE·UPDATE 와 namespaceSelectorkubernetes.io/metadata.name: kca-cel입니다. CEL 변수escalating에 securityContext.allowPrivilegeEscalation 이 명시적으로 false 가 아닌 컨테이너 이름 목록을 담고, 그 목록이 비어 있어야 통과하며 거부 메시지(messageExpression)에 그 이름들을 쉼표로 이어 보여 줍니다. - 네임스페이스
kca-bg에 라벨 없는 Podold-nolabel과 라벨team=a인 Podold-label을 먼저 만든 뒤,/root/kca-write/08-audit-team.yaml에 ClusterPolicyaudit-team(background true, Audit, 규칙team, kca-bg 의 Pod 에 team 라벨 요구)을 적용하세요. PolicyReport 에 두 파드의 결과가 나타날 때까지 기다린 뒤/root/kca-write/report.json에fail·pass키로 각 결과의 파드 이름 목록을 적습니다.
참고
- VM 안에 k3s 와 Kyverno v1.19.1 이 있습니다. 이 판에서
kyverno.io/v1 ClusterPolicy는 사용 중단 경고를 내지만 동작합니다. - 저장하지 않고 정책을 시험하기:
kubectl -n <ns> run t --image=nginx:1.27-alpine --dry-run=server(변형 결과는-o yaml로). - 정책 준비 확인:
kubectl get clusterpolicy <이름> -o jsonpath='{.status.conditionStatus.ready}', ValidatingPolicy 는.status.conditionStatus.ready. - 흔한 실수: 정책을 적용한 직후 바로 요청을 보내는 것. 웹훅 반영에 몇 초가 걸립니다.
- 흔한 실수: 한 네임스페이스에 여러 단계의 정책을 겹쳐 두는 것. 이 실습은 단계마다 네임스페이스를 나눕니다.
- Preconditions · External Data Sources(apiCall) · Variables · Auto-Gen Rules · Mutate Rules · ValidatingPolicy · Reporting
prod 파드만 limit 을 요구한다
네임스페이스 kca-pre 를 만들고 /root/kca-write/01-limits.yaml 에 ClusterPolicy limits-for-prod(background false)를 작성해 적용하세요. 규칙 memory-limit 은 kca-pre 의 Pod 를 match 하고, preconditions 로 라벨 tier 가 prod 인 요청에만 적용되며(라벨이 없으면 빈 문자열로 취급), 모든 컨테이너에 resources.limits.memory 를 요구합니다(Enforce). match 에 라벨 selector 를 쓰지 않습니다.
preconditions 의 key 는 JMESPath 변수입니다. 라벨이 없는 요청에서 변수가 해석되지 않으면 어떻게 되는지는 3단계에서 봅니다. 채점기는 tier 가 없는·dev·prod 인 파드를 서버 dry-run 으로 보내 봅니다.
네임스페이스 라벨 하나로 규칙이 켜진다
네임스페이스 kca-prod(라벨 env=prod)와 kca-dev(라벨 env=dev)를 만들고, /root/kca-write/02-team.yaml 에 ClusterPolicy team-in-prod-ns(background false)를 적용하세요. 규칙 team-when-prod 는 두 네임스페이스의 Pod 에 대해 컨텍스트 nsenv 를 apiCall 로 /api/v1/namespaces/{{request.namespace}} 에서 읽어(라벨 env, 없으면 none) env 가 prod 이고 team 라벨이 없을 때 거부합니다(Enforce). 그다음 kca-dev 의 env 를 잠시 prod 로 바꿔 라벨 없는 파드를 서버 dry-run 으로 보내 본 출력 전체를 /root/kca-write/flip.txt 에 저장하고, env 를 dev 로 되돌리세요.
네임스페이스 이름을 정책에 박아 넣으면 정책을 고치지 않고는 판정이 바뀌지 않습니다. apiCall 은 요청마다 API 서버의 현재 값을 읽습니다. dry-run 은 kubectl run ... --dry-run=server 입니다.
라벨이 없는 파드가 엉뚱한 이유로 거절됐다
네임스페이스 kca-var 에 ClusterPolicy no-platform-team(background false, 규칙 not-platform)을 적용하세요. team 라벨이 platform 이면 거부하는 deny 조건인데, 처음에는 key 를 기본값 없이 {{ request.object.metadata.labels.team }} 로 씁니다. 라벨 없는 파드를 서버 dry-run 으로 보내 거절 출력 전체를 /root/kca-write/missing.txt 에 저장하세요. 그다음 /root/kca-write/03-no-platform.yaml 의 key 에 기본값(빈 문자열)을 더해 다시 적용해, 라벨 없는 파드는 받아들이고 team=platform 은 거절하게 하세요.
JMESPath 로 없는 키를 읽으면 변수 치환이 실패하고, Kyverno 는 그 규칙을 판정하지 못한 오류로 다룹니다. Enforce 규칙의 오류가 요청을 어떻게 만드는지 출력의 문구를 읽어 보세요. || 연산자가 기본값을 줍니다.
Deployment 는 만들어졌는데 파드가 하나도 없다
네임스페이스 kca-auto 와 kca-noauto 를 만들고, 각 네임스페이스의 Pod 에 readinessProbe 를 요구하는(Enforce, background false, 규칙 need-probe) ClusterPolicy probes-auto 와 probes-noauto 를 /root/kca-write/04-probes.yaml 에 작성해 적용하세요. probes-noauto 에만 어노테이션 pod-policies.kyverno.io/autogen-controllers: none 을 붙입니다. 그다음 두 네임스페이스에서 kubectl create deployment web --image=nginx:1.27-alpine 을 실행하고, kca-auto 쪽 출력 전체를 /root/kca-write/auto.txt 에 저장하세요.
Kyverno 는 Pod 규칙을 Deployment·ReplicaSet 등 파드 템플릿을 가진 리소스용 규칙으로 자동으로 늘립니다. 늘리지 않으면 Deployment 는 통과하고, 거절은 ReplicaSet 컨트롤러가 파드를 만들 때 일어납니다. kubectl describe rs 의 조건과 이벤트를 보세요.
요청한 사람의 이름을 파드에 새긴다
네임스페이스 kca-mut 를 만들고 /root/kca-write/05-requested-by.yaml 에 ClusterPolicy requested-by(규칙 annotate-user)를 작성하세요. kca-mut 의 Pod 에 patchesJson6902 의 add 연산으로 어노테이션 kca.io/requested-by 를 넣고 값은 {{request.userInfo.username}} 입니다. 먼저 spec.background 를 적지 않고 적용해 거절되는 출력 전체를 /root/kca-write/bg-denied.txt 에 저장한 뒤, background 를 false 로 두어 적용하세요. 기존 어노테이션은 보존되어야 합니다.
JSON 포인터에서 / 는 ~1 로 씁니다. 요청자 정보는 어드미션 요청에만 있고, 이미 저장된 리소스를 다시 훑는 백그라운드 처리에는 없습니다. 서버 dry-run 에 -o jsonpath 를 붙이면 변형된 결과를 볼 수 있습니다.
latest 태그 컨테이너만 매번 새로 받게 한다
/root/kca-write/06-pull-policy.yaml 에 ClusterPolicy pull-policy(규칙 latest-always)를 작성해 적용하세요. kca-mut 의 Pod 에 대해 foreach 로 컨테이너마다 이미지가 :latest 로 끝나는지 원소 단위 preconditions 로 보고, 그런 컨테이너만 patchStrategicMerge 로 imagePullPolicy: Always 로 바꿉니다. 태그를 고정한 컨테이너의 imagePullPolicy 는 그대로여야 합니다.
foreach 안에서 현재 원소는 element 입니다. 목록에서 특정 컨테이너만 고치려면 조건 앵커 (name) 으로 이름을 맞춥니다. JMESPath 에는 ends_with 함수가 있습니다.
CEL 한 줄이 Deployment 까지 막았다
네임스페이스 kca-cel 을 만들고 /root/kca-write/07-no-priv-esc.yaml 에 policies.kyverno.io/v1 ValidatingPolicy no-priv-esc 를 적용하세요. validationActions 는 [Deny], matchConstraints 는 core v1 pods 의 CREATE·UPDATE 와 namespaceSelector kubernetes.io/metadata.name: kca-cel 입니다. CEL 변수 escalating 에 securityContext.allowPrivilegeEscalation 이 명시적으로 false 가 아닌 컨테이너 이름 목록을 담고, 그 목록이 비어 있어야 통과하며 거부 메시지(messageExpression)에 그 이름들을 쉼표로 이어 보여 줍니다.
CEL 의 filter·map 으로 목록을 만들고 has() 로 없는 필드를 먼저 확인합니다. 이 판의 ValidatingPolicy 도 파드 규칙을 파드 템플릿을 가진 리소스로 자동 생성합니다(status.autogen). 채점기는 Deployment 도 dry-run 으로 보내 봅니다.
정책보다 먼저 있던 파드도 보고서에 올랐다
네임스페이스 kca-bg 에 라벨 없는 Pod old-nolabel 과 라벨 team=a 인 Pod old-label 을 먼저 만든 뒤, /root/kca-write/08-audit-team.yaml 에 ClusterPolicy audit-team(background true, Audit, 규칙 team, kca-bg 의 Pod 에 team 라벨 요구)을 적용하세요. PolicyReport 에 두 파드의 결과가 나타날 때까지 기다린 뒤 /root/kca-write/report.json 에 fail·pass 키로 각 결과의 파드 이름 목록을 적습니다.
Audit 는 요청을 막지 않고 기록만 합니다. 이미 저장된 리소스는 어드미션을 다시 거치지 않으므로 background 가 있어야 평가됩니다. 결과는 네임스페이스의 PolicyReport(리소스마다 하나)에 모입니다.