정책을 코드로 · 정책도 시험해야 한다 · 실습
규칙을 풀어 줬는데 시험은 그날도 초록불이었다
목표
정책 하나에 붙일 시험 하네스를 처음부터 만든다. 입력 표본·기대 판정·기대 메시지를 표 파일에 적고, 그 표를 읽어 서버 dry-run 으로 어드미션을 태우는 실행기를 써서, 메시지 변경·범위 어긋남·규칙 완화 세 가지가 시험에서 어떻게 빨간불로 드러나는지 손으로 확인한다.
왜 중요한가
정책이 고장 나는 가장 흔한 방식은 오류가 아니라 침묵이다. 매치 범위가 어긋나면 정책은 아무 소리 없이 전부 통과시키고, 대시보드에는 빨간 줄 하나 뜨지 않는다. 그래서 잘 도는 정책과 죽은 정책이 겉으로 구분되지 않는다. 여기에 두 번째 힘이 겹친다 - 정책은 누가 막힐 때마다 느슨해지는 방향으로만 고쳐진다. 시험은 이 두 가지를 동시에 막는 유일한 장치다. 다만 통과 표본만 모아 둔 시험은 정책을 통째로 지워도 초록불이라 아무것도 지키지 못한다. 그래서 이 실습은 거부 표본과 기대 메시지를 계약으로 고정하고, 시험 자체가 무의미해진 상태(표본 없음·거부 기대 없음)를 성공과 실패 어느 쪽도 아닌 세 번째 종료 코드로 빼내는 데까지 간다.
단계
1. /root/poltest 에서 작업합니다(export KUBECONFIG=/root/.kube/config, kubectl config use-context kwok-lab). /root/poltest/policy.yaml 에 admissionregistration.k8s.io/v1 의 ValidatingAdmissionPolicy require-min-replicas 를 쓰세요 — matchConstraints.resourceRules 는 apps 그룹 v1 의 deployments 에 대한 CREATE·UPDATE 를 잡고, validation 은 object.spec.replicas >= 2 이며 messageExpression 이 below the minimum 2 replicas 라는 조각을 담은 문장을 만듭니다. /root/poltest/binding.yaml 에 바인딩 require-min-replicas-bind 를 쓰세요 — policyName 은 require-min-replicas, validationActions 는 ["Deny"], matchResources.namespaceSelector.matchLabels 는 policy-test: "yes" 입니다. 둘 다 적용하고 네임스페이스 poltest 를 만들어 라벨 policy-test=yes 를 붙이세요. 표본 둘도 만듭니다 — /root/poltest/cases/ok-two.yaml 은 Deployment ok-two 로 replicas: 2, /root/poltest/cases/bad-one.yaml 은 Deployment bad-one 로 replicas: 1 입니다. 마지막으로 ok-two.yaml 하나만 서버 dry-run 으로 보내 그 출력을 /root/poltest/01-allow.txt 에 저장하세요.
2. /root/poltest/cases.txt 에 골든 케이스 표를 쓰세요. 한 줄이 한 표본이고 칸은 | 로 나눕니다 — 이름 | 매니페스트 경로 | allow 또는 deny | 기대 메시지 조각 네 칸입니다. 매니페스트 경로는 표 파일이 있는 디렉터리 기준으로 적고, # 로 시작하는 줄과 빈 줄은 주석입니다. 기대가 allow 인 줄은 메시지 칸에 - 를 적습니다. 지금은 두 줄을 넣으세요 — allow-two 는 cases/ok-two.yaml 을 allow 로, deny-one 은 cases/bad-one.yaml 을 deny 로 적고 메시지 조각은 실제 거부 메시지에서 그대로 옮긴 below the minimum 2 replicas 입니다.
3. /root/poltest/run-cases.sh 를 만드세요. 인자로 표 파일을 받고(없으면 스크립트 옆의 cases.txt) 표의 줄마다 그 매니페스트를 kubectl create -n poltest -f <파일> --dry-run=server 로 보냅니다. 명령이 성공하면 실제 판정은 allow, 실패하면 deny 입니다. 기대와 같으면 PASS <이름> 한 줄을, 다르면 FAIL <이름> <까닭> 한 줄을 찍습니다. 매니페스트 경로는 표 파일이 있는 디렉터리 기준으로 풉니다. 마지막 줄에는 total=<수> pass=<수> fail=<수> 를 찍고, 실패가 하나라도 있으면 0 이 아닌 값으로 끝냅니다. 만들었으면 bash run-cases.sh 로 돌려 두 줄이 다 PASS 로 나오는지 확인하세요.
4. 거부 표본 둘을 더 만드세요 — /root/poltest/cases/bad-zero.yaml 은 Deployment bad-zero 로 replicas: 0 이고, /root/poltest/cases/bad-default.yaml 은 Deployment bad-default 로 replicas 필드를 아예 적지 않습니다. 둘을 cases.txt 에 deny-zero·deny-default 라는 이름으로 더하고 기대 메시지 조각은 같은 below the minimum 2 replicas 로 둡니다. 표는 이제 네 줄이고 그중 세 줄이 deny 기대입니다. bash run-cases.sh 가 다시 전부 PASS 로 끝나야 합니다.
5. run-cases.sh 를 고쳐서 기대가 deny 인 줄은 거부 메시지에 기대 조각이 들어 있어야 PASS 가 되게 하세요(조각이 - 이거나 비어 있으면 메시지는 보지 않습니다). 그다음 /root/poltest/policy-msgdrift.yaml 을 만드세요 — 이름과 규칙은 policy.yaml 과 같고 messageExpression 만 다른 문장으로 바꾼 것으로, 그 문장에는 below the minimum 2 replicas 가 들어 있으면 안 됩니다. 그것을 적용하고 bash run-cases.sh 를 돌려 출력 전체를 /root/poltest/05-drift.txt 에 저장하세요(FAIL 줄이 세 줄 이상 나와야 합니다). 마지막으로 policy.yaml 을 다시 적용해 원래 메시지로 되돌리고, 시험이 다시 전부 PASS 로 끝나는 것을 확인하세요.
6. /root/poltest/cases/sts-one.yaml 에 StatefulSet sts-one 을 쓰세요 — replicas: 1, serviceName: sts-one 입니다. cases.txt 에 deny-sts-one 줄을 deny 기대로 더하고(메시지 조각은 같은 below the minimum 2 replicas) bash run-cases.sh 를 돌려 출력 전체를 /root/poltest/06-falsepass.txt 에 저장하세요 — 그 표본이 FAIL 로 나옵니다. 지금 정책은 deployments 만 보기 때문에 이 요청을 아예 판정하지 않고, 거부가 없으니 통과처럼 보이는 것입니다. 이제 policy.yaml 의 resources 를 ["deployments", "statefulsets"] 로 넓혀 다시 적용하고, 시험이 다섯 줄 모두 PASS 로 끝나는 것을 확인하세요.
7. /root/poltest/policy-loose.yaml 을 만드세요 — policy.yaml 과 이름·자원·메시지는 같고 최소값만 2 에서 1 로 내린 것입니다(누가 '한 대짜리도 올리게 해 주세요' 라고 요청해 조건을 한 칸 푼 상황입니다). 그것을 적용하고 bash run-cases.sh 를 돌려 출력 전체를 /root/poltest/07-regression.txt 에 저장하세요 — FAIL 이 두 줄 이상 나옵니다. 그다음 policy.yaml 을 다시 적용해 되돌리고 시험이 다시 전부 PASS 로 끝나는 것을 확인하세요.
8. run-cases.sh 를 마지막으로 고쳐 파이프라인 게이트로 쓸 수 있게 하세요. 요약 줄을 total=<수> pass=<수> fail=<수> deny=<수> result=<PASS|FAIL|INVALID> 로 늘리고 (deny 는 기대가 deny 인 줄의 수), 찍은 줄 전체를 표 파일이 있는 디렉터리의 report.txt 에도 그대로 남기세요. 종료 코드 규약은 셋입니다 — 전부 통과면 0, 하나라도 기대와 어긋나면 1, 표에 표본이 하나도 없거나 deny 기대가 하나도 없으면 2(result 는 INVALID). 고친 뒤 bash run-cases.sh 를 돌려 /root/poltest/report.txt 를 남기고, 거부 기대를 뺀 표로도 한 번 돌려 2 가 나오는지 확인하세요.
참고
- 작업 디렉터리는
/root/poltest이고 클러스터는export KUBECONFIG=/root/.kube/config로 붙습니다(컨텍스트kwok-lab). - 서버 dry-run 은
kubectl create -n poltest -f <파일> --dry-run=server입니다. 저장만 건너뛰고 어드미션은 그대로 지나가므로 거부 메시지가 실제로 돌아오고 오브젝트는 남지 않습니다. 거부 메시지는 표준 오류로 나오니 파일에 담을 때는2>&1을 붙이세요. - 정책을 apply 해도 곧바로 반영되지 않습니다. API 서버가 새 식을 컴파일하는 데 1초 안팎 걸리니, 바꾼 직후에는 바뀐 판정이 보일 때까지 짧게 기다렸다가 시험을 돌리세요. 이것 때문에 '고쳤는데 그대로다' 로 착각하기 쉽습니다.
- 흔한 실수 하나 - 반복문 안에서 부르는 명령이 표 파일을 통째로 읽어 버려 한 줄만 돌고 끝납니다.
kubectl ... </dev/null을 붙이세요. - 흔한 실수 둘 - 거부 메시지는 표준 출력이 아니라 표준 오류로 나옵니다. 파일에 담거나 비교할 때
2>&1을 빠뜨리지 마세요. - ValidatingAdmissionPolicy: https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/
- CEL 문법과 쓰이는 자리: https://kubernetes.io/docs/reference/using-api/cel/
- dry-run 이 무엇을 지나가는가: https://kubernetes.io/docs/reference/using-api/api-concepts/
- 어드미션과 sideEffects: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
단계 8개
- 통과 표본 하나만 돌려 보고 정책이 산다고 믿었다
- 기대를 표로 적어 두자 빠진 쪽이 드러났다
- 표를 읽어 돌리는 실행기를 만든다
- 그럴듯하지만 규칙을 어기는 표본을 더한다
- 메시지만 다듬었는데 시험이 빨간불이 됐다
- 정책이 쳐다보지도 않은 자원이 통과로 읽혔다
- 규칙을 한 칸 풀어 줬더니 시험이 먼저 알아챘다
- 빈 시험이 초록불이 되지 않게 종료 코드를 정한다