We loosened the rule and the tests stayed green that day
한국어 원문으로 표시합니다.
목표
정책 하나에 붙일 시험 하네스를 처음부터 만든다. 입력 표본·기대 판정·기대 메시지를 표 파일에 적고, 그 표를 읽어 서버 dry-run 으로 어드미션을 태우는 실행기를 써서, 메시지 변경·범위 어긋남·규칙 완화 세 가지가 시험에서 어떻게 빨간불로 드러나는지 손으로 확인한다.
왜 중요한가
정책이 고장 나는 가장 흔한 방식은 오류가 아니라 침묵이다. 매치 범위가 어긋나면 정책은 아무 소리 없이 전부 통과시키고, 대시보드에는 빨간 줄 하나 뜨지 않는다. 그래서 잘 도는 정책과 죽은 정책이 겉으로 구분되지 않는다. 여기에 두 번째 힘이 겹친다 - 정책은 누가 막힐 때마다 느슨해지는 방향으로만 고쳐진다. 시험은 이 두 가지를 동시에 막는 유일한 장치다. 다만 통과 표본만 모아 둔 시험은 정책을 통째로 지워도 초록불이라 아무것도 지키지 못한다. 그래서 이 실습은 거부 표본과 기대 메시지를 계약으로 고정하고, 시험 자체가 무의미해진 상태(표본 없음·거부 기대 없음)를 성공과 실패 어느 쪽도 아닌 세 번째 종료 코드로 빼내는 데까지 간다.
단계
/root/poltest에서 작업합니다(export KUBECONFIG=/root/.kube/config,kubectl config use-context kwok-lab)./root/poltest/policy.yaml에admissionregistration.k8s.io/v1의 ValidatingAdmissionPolicyrequire-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은 Deploymentok-two로replicas: 2,/root/poltest/cases/bad-one.yaml은 Deploymentbad-one로replicas: 1입니다. 마지막으로ok-two.yaml하나만 서버 dry-run 으로 보내 그 출력을/root/poltest/01-allow.txt에 저장하세요./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입니다./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 로 나오는지 확인하세요.- 거부 표본 둘을 더 만드세요 —
/root/poltest/cases/bad-zero.yaml은 Deploymentbad-zero로replicas: 0이고,/root/poltest/cases/bad-default.yaml은 Deploymentbad-default로 replicas 필드를 아예 적지 않습니다. 둘을cases.txt에deny-zero·deny-default라는 이름으로 더하고 기대 메시지 조각은 같은below the minimum 2 replicas로 둡니다. 표는 이제 네 줄이고 그중 세 줄이 deny 기대입니다.bash run-cases.sh가 다시 전부 PASS 로 끝나야 합니다. 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 로 끝나는 것을 확인하세요./root/poltest/cases/sts-one.yaml에 StatefulSetsts-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 로 끝나는 것을 확인하세요./root/poltest/policy-loose.yaml을 만드세요 —policy.yaml과 이름·자원·메시지는 같고 최소값만 2 에서 1 로 내린 것입니다(누가 '한 대짜리도 올리게 해 주세요' 라고 요청해 조건을 한 칸 푼 상황입니다). 그것을 적용하고bash run-cases.sh를 돌려 출력 전체를/root/poltest/07-regression.txt에 저장하세요 — FAIL 이 두 줄 이상 나옵니다. 그다음policy.yaml을 다시 적용해 되돌리고 시험이 다시 전부 PASS 로 끝나는 것을 확인하세요.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/
통과 표본 하나만 돌려 보고 정책이 산다고 믿었다
/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 에 저장하세요.
정책은 판정 방법만 정하고 어디에 적용할지는 바인딩이 정합니다. 그래서 둘 다 올려야 거부가 일어납니다. messageExpression 은 CEL 식이라 문자열을 이어 붙이고 숫자는 string(...) 으로 바꿔야 합니다. 이 단계가 보여 주려는 것은 마지막 줄입니다 — 통과해야 할 표본만 돌려 본 기록은 정책이 살아 있다는 증거가 되지 못합니다. 서버 dry-run 은 kubectl create -n poltest -f <파일> --dry-run=server 입니다. 저장만 건너뛰고 어드미션은 그대로 지나가므로 거부 메시지가 실제로 돌아오고 오브젝트는 남지 않습니다. 거부 메시지는 표준 오류로 나오니 파일에 담을 때는 2>&1 을 붙이세요.
기대를 표로 적어 두자 빠진 쪽이 드러났다
/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 입니다.
기대 메시지는 지어내지 말고 실제 거부 출력에서 잘라 옵니다. 먼저 bad-one 을 서버 dry-run 으로 한 번 보내 어떤 문장이 돌아오는지 보세요. 표본 이름은 파일을 열지 않고도 무엇이 다른지 알 수 있게 짓습니다. 통과 표본을 복사해 한 곳만 바꾼 쌍으로 두면 시험이 깨졌을 때 원인이 이름에서 읽힙니다.
표를 읽어 돌리는 실행기를 만든다
/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 로 나오는지 확인하세요.
표를 읽을 때는 while IFS='|' read -r ... done < "$table" 꼴을 씁니다. 반복문 안에서 부르는 명령이 표를 빨아들이지 않게 </dev/null 을 붙이세요. 칸 앞뒤 공백은 잘라 내야 비교가 맞습니다. 종료 코드는 마지막 명령의 것이 그대로 나가므로 마지막에 명시적으로 exit 하세요. 채점기는 이 스크립트에 다른 표를 주고 돌립니다 - 표를 읽지 않고 정해진 답만 찍는 실행기는 거기서 걸립니다.
그럴듯하지만 규칙을 어기는 표본을 더한다
거부 표본 둘을 더 만드세요 — /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 로 끝나야 합니다.
replicas 를 안 적은 Deployment 는 어드미션 전에 기본값이 채워집니다. 서버 dry-run 은 그 기본값 채우기까지 지나온 오브젝트를 정책에 보여 주므로, 적지 않은 것과 1 을 적은 것이 같은 판정을 받습니다. 이것이 오프라인 엔진이 놓치기 쉬운 자리입니다. 거부 표본은 통과 표본에서 한 곳만 바꿔 만드는 것이 좋습니다.
메시지만 다듬었는데 시험이 빨간불이 됐다
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 로 끝나는 것을 확인하세요.
거부 메시지는 정책이 개발자에게 말을 거는 유일한 통로입니다. 그 문장을 붙잡아 알림이나 티켓을 만든 파이프라인은 문구가 바뀌는 날 말없이 멈춥니다. 메시지를 기대값으로 고정해 두면 리팩터링하는 사람이 그 자리에서 알게 됩니다. 셸에서 부분 문자열 확인은 case "$out" in *"$frag"*) 가 가장 단단합니다. 정책을 apply 해도 곧바로 반영되지 않습니다 - 바뀐 판정이 보일 때까지 기다린 뒤에 시험을 돌리세요.
정책이 쳐다보지도 않은 자원이 통과로 읽혔다
/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 로 끝나는 것을 확인하세요.
정책이 그 자원을 보지 않으면 판정은 통과가 아니라 '해당 없음' 입니다. 그런데 어드미션은 그 둘을 같은 얼굴로 돌려줍니다 - 요청이 성공한 것이지요. 그래서 반드시 거부돼야 하는 표본을 표에 두는 것이 매치 범위가 어긋나지 않았다는 유일한 증거가 됩니다. StatefulSet 은 selector·template 말고 serviceName 이 더 필요합니다. 범위를 넓힌 뒤에도 반영에 1초 안팎 걸립니다.
규칙을 한 칸 풀어 줬더니 시험이 먼저 알아챘다
/root/poltest/policy-loose.yaml 을 만드세요 — policy.yaml 과 이름·자원·메시지는 같고 최소값만 2 에서 1 로 내린 것입니다(누가 '한 대짜리도 올리게 해 주세요' 라고 요청해 조건을 한 칸 푼 상황입니다). 그것을 적용하고 bash run-cases.sh 를 돌려 출력 전체를 /root/poltest/07-regression.txt 에 저장하세요 — FAIL 이 두 줄 이상 나옵니다. 그다음 policy.yaml 을 다시 적용해 되돌리고 시험이 다시 전부 PASS 로 끝나는 것을 확인하세요.
정책은 거의 언제나 느슨해지는 방향으로만 고쳐집니다. 각 변경은 그때그때 타당해 보이고, 반년이 지나면 원래 무엇을 막으려던 것이었는지 아무도 모릅니다. 거부 표본이 시험에 있으면 규칙이 풀린 그날 빨간불이 뜹니다. 이때 메시지 문구는 그대로라 replicas 0 짜리 표본은 여전히 PASS 로 남는 것도 같이 보세요 - 규칙과 문장이 따로 노는 순간입니다.
빈 시험이 초록불이 되지 않게 종료 코드를 정한다
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 가 나오는지 확인하세요.
통과 표본만 모아 둔 시험은 정책을 통째로 지워도 초록불입니다. 그래서 '시험이 아무것도 지키지 못하는 상태' 를 성공과 실패 어느 쪽도 아닌 세 번째 종료 코드로 빼는 것입니다. 게이트는 0 만 통과로 보고 1 과 2 를 모두 막습니다. report.txt 를 표 파일 옆에 두면 다른 표로 돌려도 원래 결과를 덮어쓰지 않습니다. 요약은 앞 단계 형식에 두 칸을 덧붙이는 것이니 PASS·FAIL 줄의 모양은 그대로 둡니다.