정책을 코드로 · 정책도 시험해야 한다 · 이론
정책 시험 — 통과와 거부를 함께 고정한다
한 줄 요약
정책 시험의 최소 단위는 입력 표본 · 기대 판정 · 기대 메시지 세 쪽짜리 표이고, 이 표에 거부 표본이 없으면 그 시험은 아무것도 지키지 못한다.
왜 이게 필요했나
정책이 코드라는 말은 비유가 아니다. 저장소에 들어가고, 리뷰를 거치고, 배포되고, 고쳐진다. 그런데 애플리케이션 코드와 다른 점이 하나 있다. 정책이 고장 나는 가장 흔한 방식은 오류가 아니라 침묵이다. 매치 범위가 어긋나 아무것도 안 보면 정책은 조용히 전부 통과시킨다. 화면에는 아무 일도 일어나지 않고, 대시보드에 빨간 줄도 없다. 그래서 "잘 돌고 있는 정책"과 "죽은 정책"이 겉으로 구분되지 않는다.
여기에 두 번째 힘이 겹친다. 정책은 느슨해지는 방향으로만 고쳐진다. 누가 막혀서 문의를 하면 조건을 하나 풀고, 예외 네임스페이스를 하나 더 넣는다. 각 변경은 그때 그때 타당하다. 반년이 지나면 그 정책이 원래 무엇을 막으려던 것이었는지 아무도 모른다. 회귀 시험은 바로 이 지점에 박아 두는 못이다. 거부 표본이 시험에 있으면, 규칙을 느슨하게 고친 그날 시험이 빨간불이 된다.
어떻게 동작하나
표 기반(골든 케이스) 시험. 시험 하나는 세 쪽으로 이루어진다.
| 쪽 | 내용 |
| --- | --- |
| 입력 표본 | 실제로 클러스터에 갈 법한 매니페스트 조각. 통과해야 할 것과 막혀야 할 것 |
| 기대 판정 | pass / fail / skip |
| 기대 메시지 | 거부일 때 사람이 받는 문장 |
세 번째 쪽을 빠뜨리는 팀이 많은데, 메시지도 계약이다. 거부 메시지는 정책이 개발자에게 말을 거는 유일한 통로이고, 그 문장이 바뀌면 그것을 붙잡아 자동화한 파이프라인도 바뀐다. 메시지를 시험에 넣어 두면 "왜 막혔는지 모르겠다"는 문의가 리뷰 단계에서 드러난다.
표본은 한 번에 한 가지만 다르게 만드는 것이 좋다. 통과 표본을 복사해 위반 필드 하나만 바꾼 쌍을 여러 개 두면, 시험이 깨졌을 때 무엇이 달라졌는지가 파일 이름에서 바로 읽힌다.
시험을 돌리는 세 자리.
1. 클러스터에 올리기 전, 오프라인 엔진. 정책 파일과 표본 파일만 가지고 판정을 돌린다. 네트워크도 클러스터도 필요 없어서 CI 에서 가장 빠르게 돈다. Kyverno CLI 의 kyverno apply 와 kyverno test, Conftest, OPA 가 여기에 해당한다.
2. 클러스터에 올리기 전, 서버 dry-run. ?dryRun=All 로 보낸 요청은 저장 단계만 빼고 정상 경로를 그대로 지난다. 문서는 이때 관련 어드미션 컨트롤러가 모두 실행되고, 검증 어드미션은 변형이 끝난 뒤의 오브젝트를 보며, 기본값이 채워지고 스키마 검증도 일어난다고 적는다. 그러니 이것은 "흉내"가 아니라 진짜 판정이다. 오프라인 엔진이 못 보는 것(다른 정책과의 상호작용, 변형 뒤의 모양)이 여기서만 보인다.
3. 올린 뒤. 백그라운드 스캔과 리포트로 이미 있는 자원을 훑는다. 어드미션은 앞으로 들어올 요청만 보기 때문이다.
서버 dry-run 에는 짝이 되는 사실이 하나 있다. 부작용이 있는 어드미션 컨트롤러가 걸리는 요청은 dry-run 이면 차라리 실패시킨다. 그래서 웹훅은 설정 오브젝트의 sideEffects 를 None 또는 NoneOnDryRun 으로 선언해 두어야 dry-run 경로에서 정상적으로 평가된다. 내장 어드미션 플러그인은 모두 dry-run 을 지원한다.
거짓 통과의 흔한 원인. 시험이 초록불인데 클러스터는 안 막히는 경우, 원인은 대개 셋 중 하나다.
- 정책이 그 자원을 아예 안 본다. 매치 범위가
Deployment인데 표본은Pod이거나, apiVersion 이 다르다. 판정은skip인데 시험이skip을 실패로 세지 않으면 초록불이 된다. skip 을 pass 와 구분해서 세는 것이 첫 번째 방어선이다. - 종료 코드를 그대로 믿었다. 도구가 위반을 찾고도 0 으로 끝나는 경우가 실제로 있다. 이 실습 환경의
kyverno json scan이 그렇다. 그러면 파이프라인은 영원히 초록불이다. - 표본에 거부가 하나도 없다. 통과만 모아 둔 시험은 정책을 통째로 지워도 초록불이다. 시험 파일을 열었을 때 fail 기대가 하나도 없으면 그 시험은 없는 것과 같다.
정책 저장소의 CI 는 대개 이 순서다.
1) 정책 파일 스키마·문법 검사2) 오프라인 엔진으로 골든 케이스 표 실행 (pass·fail·메시지)3) skip 이 0 인지 확인 — 매치가 어긋나지 않았다는 증거4) 거부 기대 건수가 0 이 아닌지 확인5) 스테이징 클러스터에 서버 dry-run 으로 대표 표본 몇 개6) 배포3번과 4번이 이 목록에서 가장 자주 빠지고, 가장 조용히 배신한다.
현장에서 만나는 모습
첫째, 초록불인데 운영에서 안 막히는 정책. 표본의 apiVersion 이 한 판 낮아서 전부 skip 이었던 사례가 흔하다. 시험 출력의 요약줄에서 pass 수만 보고 skip 수를 안 본 것이다. 요약에서 skip 을 0 으로 강제하는 한 줄이 이 사고를 통째로 막는다.
둘째, 규칙을 느슨하게 고친 날. "이 네임스페이스만 빼 주세요"가 반복되다가, 어느 날 예외 조건이 너무 넓어져 원래 막던 것까지 통과한다. 그 순간 거부 표본이 시험에 있으면 PR 에서 빨간불이 뜨고, 없으면 반년 뒤 사고로 알게 된다.
셋째, 메시지가 조용히 바뀐 날. 정책을 리팩터링하면서 거부 메시지 문구를 다듬었는데, 그 문자열을 잡아 슬랙 알림을 만들던 파이프라인이 말없이 멈춘다. 메시지를 기대값으로 고정해 두면 리팩터링하는 사람이 그 자리에서 알게 된다.
넷째, 이 환경의 정직한 한계. 실습 파드에는 conftest 와 opa 가 없다. 대신 kyverno CLI 가 있어 같은 모양의 표 기반 시험을 돌릴 수 있고, kwok 이 띄운 진짜 API 서버가 있어 서버 dry-run 도 실제로 동작한다. 도구 이름이 달라도 배우는 것은 같다.
참고 문서
- kyverno apply 와 시험 스위트: https://kyverno.io/docs/kyverno-cli/usage/apply/
- 정책 리포트: https://kyverno.io/docs/policy-reports/
- 쿠버네티스 API 개념 중 dry-run: https://kubernetes.io/docs/reference/using-api/api-concepts/
- 동적 어드미션 제어와 sideEffects: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
다음 실습에서 할 것
정책 하나에 붙일 시험 스위트를 처음부터 만든다. 통과 표본과 거부 표본을 쌍으로 두고 기대 판정과 기대 메시지를 표에 적은 뒤, 위의 두 번째 자리(서버 dry-run)를 판정 엔진으로 삼는 실행기를 직접 쓴다. 정책이 쳐다보지도 않는 자원을 거부 표본으로 두어 거짓 통과를 손으로 만들어 보고, 그것을 잡아내는 검사를 더한다. 그다음 규칙을 한 칸 느슨하게 고쳐 거부 표본이 통과하는 것을 보고, 회귀 시험이 그 변경을 붙잡는 장면을 확인한다. 마지막에는 표본이 하나도 없는 시험이 초록불이 되지 않도록 종료 코드 규약을 정한다.