LabHub
배우기 러닝패스 코스

Policy as Code

We loosened the rule and the tests stayed green that day

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

정책 하나에 붙일 시험 하네스를 처음부터 만든다. 입력 표본·기대 판정·기대 메시지를 표 파일에 적고, 그 표를 읽어 서버 dry-run 으로 어드미션을 태우는 실행기를 써서, 메시지 변경·범위 어긋남·규칙 완화 세 가지가 시험에서 어떻게 빨간불로 드러나는지 손으로 확인한다.

왜 중요한가

정책이 고장 나는 가장 흔한 방식은 오류가 아니라 침묵이다. 매치 범위가 어긋나면 정책은 아무 소리 없이 전부 통과시키고, 대시보드에는 빨간 줄 하나 뜨지 않는다. 그래서 잘 도는 정책과 죽은 정책이 겉으로 구분되지 않는다. 여기에 두 번째 힘이 겹친다 - 정책은 누가 막힐 때마다 느슨해지는 방향으로만 고쳐진다. 시험은 이 두 가지를 동시에 막는 유일한 장치다. 다만 통과 표본만 모아 둔 시험은 정책을 통째로 지워도 초록불이라 아무것도 지키지 못한다. 그래서 이 실습은 거부 표본과 기대 메시지를 계약으로 고정하고, 시험 자체가 무의미해진 상태(표본 없음·거부 기대 없음)를 성공과 실패 어느 쪽도 아닌 세 번째 종료 코드로 빼내는 데까지 간다.

단계

  1. /root/poltest 에서 작업합니다(export KUBECONFIG=/root/.kube/config, kubectl config use-context kwok-lab). /root/poltest/policy.yamladmissionregistration.k8s.io/v1 의 ValidatingAdmissionPolicy require-min-replicas 를 쓰세요 — matchConstraints.resourceRulesapps 그룹 v1deployments 에 대한 CREATE·UPDATE 를 잡고, validation 은 object.spec.replicas >= 2 이며 messageExpression 이 below the minimum 2 replicas 라는 조각을 담은 문장을 만듭니다. /root/poltest/binding.yaml 에 바인딩 require-min-replicas-bind 를 쓰세요 — policyNamerequire-min-replicas, validationActions["Deny"], matchResources.namespaceSelector.matchLabelspolicy-test: "yes" 입니다. 둘 다 적용하고 네임스페이스 poltest 를 만들어 라벨 policy-test=yes 를 붙이세요. 표본 둘도 만듭니다 — /root/poltest/cases/ok-two.yaml 은 Deployment ok-tworeplicas: 2, /root/poltest/cases/bad-one.yaml 은 Deployment bad-onereplicas: 1 입니다. 마지막으로 ok-two.yaml 하나만 서버 dry-run 으로 보내 그 출력을 /root/poltest/01-allow.txt 에 저장하세요.
  2. /root/poltest/cases.txt 에 골든 케이스 표를 쓰세요. 한 줄이 한 표본이고 칸은 | 로 나눕니다 — 이름 | 매니페스트 경로 | allow 또는 deny | 기대 메시지 조각 네 칸입니다. 매니페스트 경로는 표 파일이 있는 디렉터리 기준으로 적고, # 로 시작하는 줄과 빈 줄은 주석입니다. 기대가 allow 인 줄은 메시지 칸에 - 를 적습니다. 지금은 두 줄을 넣으세요 — allow-twocases/ok-two.yaml 을 allow 로, deny-onecases/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-zeroreplicas: 0 이고, /root/poltest/cases/bad-default.yaml 은 Deployment bad-defaultreplicas 필드를 아예 적지 않습니다. 둘을 cases.txtdeny-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.txtdeny-sts-one 줄을 deny 기대로 더하고(메시지 조각은 같은 below the minimum 2 replicas) bash run-cases.sh 를 돌려 출력 전체를 /root/poltest/06-falsepass.txt 에 저장하세요 — 그 표본이 FAIL 로 나옵니다. 지금 정책은 deployments 만 보기 때문에 이 요청을 아예 판정하지 않고, 거부가 없으니 통과처럼 보이는 것입니다. 이제 policy.yamlresources["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, kubectl config use-context kwok-lab). /root/poltest/policy.yamladmissionregistration.k8s.io/v1 의 ValidatingAdmissionPolicy require-min-replicas 를 쓰세요 — matchConstraints.resourceRulesapps 그룹 v1deployments 에 대한 CREATE·UPDATE 를 잡고, validation 은 object.spec.replicas >= 2 이며 messageExpression 이 below the minimum 2 replicas 라는 조각을 담은 문장을 만듭니다. /root/poltest/binding.yaml 에 바인딩 require-min-replicas-bind 를 쓰세요 — policyNamerequire-min-replicas, validationActions["Deny"], matchResources.namespaceSelector.matchLabelspolicy-test: "yes" 입니다. 둘 다 적용하고 네임스페이스 poltest 를 만들어 라벨 policy-test=yes 를 붙이세요. 표본 둘도 만듭니다 — /root/poltest/cases/ok-two.yaml 은 Deployment ok-tworeplicas: 2, /root/poltest/cases/bad-one.yaml 은 Deployment bad-onereplicas: 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-twocases/ok-two.yaml 을 allow 로, deny-onecases/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-zeroreplicas: 0 이고, /root/poltest/cases/bad-default.yaml 은 Deployment bad-defaultreplicas 필드를 아예 적지 않습니다. 둘을 cases.txtdeny-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.txtdeny-sts-one 줄을 deny 기대로 더하고(메시지 조각은 같은 below the minimum 2 replicas) bash run-cases.sh 를 돌려 출력 전체를 /root/poltest/06-falsepass.txt 에 저장하세요 — 그 표본이 FAIL 로 나옵니다. 지금 정책은 deployments 만 보기 때문에 이 요청을 아예 판정하지 않고, 거부가 없으니 통과처럼 보이는 것입니다. 이제 policy.yamlresources["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 줄의 모양은 그대로 둡니다.