정책을 코드로 · 정책이 고장 나면 클러스터가 멈춘다 · 이론
범위와 고장 모드 — 정책을 켜는 날의 순서
한 줄 요약
정책의 위험은 규칙의 내용이 아니라 범위와 고장 모드에서 나오고, 그래서 정책을 켜는 날의 순서는 언제나 좁은 범위 → 경고 → 넓히기 → 차단이다.
왜 이게 필요했나
정책 도입 사고의 기록을 모아 보면 규칙이 틀려서 난 사고는 드물다. 대부분은 두 가지다. 너무 넓게 걸었거나, 고장 났을 때의 동작을 안 정했거나.
넓게 거는 것이 왜 위험한지는 비용의 성격을 보면 안다. 어떤 자원이든 매칭하는 정책 하나의 비용은 정책 한 번의 실행 시간이 아니다. API 서버로 들어오는 모든 쓰기 요청에 붙는 세금이다. 클러스터에는 사람이 보내는 요청보다 컨트롤러가 보내는 요청이 훨씬 많다. 리스·이벤트·엔드포인트슬라이스·파드 상태 갱신이 끊임없이 흐른다. 와일드카드 정책 하나가 그 전부를 한 번씩 더 붙잡는다.
두 번째가 더 무섭다. 정책이 kube-system 까지 보도록 걸어 두고 정책 엔진이 죽으면, failurePolicy: Fail 아래에서 클러스터는 자기 자신을 복구할 수 없게 된다. 엔진을 되살리려는 배포도 그 정책을 지나야 하기 때문이다. 순환이다.
어떻게 동작하나
범위는 세 층으로 좁힌다.
| 층 | 무엇을 정하는가 | 예 |
| --- | --- | --- |
| 자원 | 어떤 그룹·판·종류·동사를 볼 것인가 | apps/v1 deployments 의 CREATE·UPDATE 만 |
| 네임스페이스 | 어느 네임스페이스에 적용할 것인가 | namespaceSelector 로 특정 라벨이 붙은 곳만 |
| 요청 | 그중에서도 어떤 요청을 볼 것인가 | objectSelector, matchConditions 의 CEL 조건 |
세 층은 비용을 줄이는 순서이기도 하다. 자원 층에서 걸러진 요청은 API 서버가 정책을 떠올리지도 않는다. 네임스페이스 층은 그다음이고, 요청 층은 마지막이다. 그러니 와일드카드로 받아 놓고 CEL 조건으로 걸러 내는 정책은 세 층 중 가장 비싼 자리에서 일하는 셈이다. 같은 결과를 내면서 비용만 크다.
kube-system 을 빼는 것은 취향이 아니라 탈출구를 남기는 일이다. 정책 엔진이 사는 네임스페이스도 마찬가지다. 런북에 "웹훅 설정을 지워 클러스터를 되살린다"는 절차가 들어 있는 조직이 많은데, 애초에 범위에서 빼 두면 그 절차를 쓸 일이 줄어든다.
고장 모드 — failurePolicy 가 맞바꾸는 것.
| 값 | 정책 서버가 응답하지 못할 때 | 그때 잃는 것 |
| --- | --- | --- |
| Fail | 요청을 거부한다 | 가용성. 엔진이 죽으면 그 범위의 쓰기가 멈춘다 |
| Ignore | 요청을 통과시킨다 | 보장. 엔진이 죽은 동안 검사 없이 들어온다 |
둘 다 옳은 답이 아니다. 같은 클러스터 안에서도 규칙마다 다른 값이 맞는다. 보안 경계를 지키는 규칙은 Fail, 위생 규칙(라벨 표준, 권장 설정)은 Ignore. 그리고 Fail 을 고른 규칙일수록 범위를 좁히는 일이 중요해진다. 범위가 좁으면 엔진이 죽어도 멈추는 것이 그 범위뿐이기 때문이다. 범위와 고장 모드는 따로 고르는 값이 아니라 한 쌍으로 고르는 값이다.
timeoutSeconds 는 세 번째 손잡이다. 웹훅 응답을 기다리는 시간이고, 길게 잡으면 장애 때 API 서버가 그만큼 붙잡힌다. 짧게 잡으면 느린 응답이 실패로 처리되어 failurePolicy 로 넘어간다. 그래서 타임아웃은 "넉넉하게"가 아니라 "이 규칙이 정상일 때 걸리는 시간보다 조금 길게"로 정한다.
내장 정책과 웹훅 정책은 고장 모드가 다르다. 이 차이가 설계 선택을 바꾼다.
- VAP: API 서버 안에서 평가된다. 네트워크가 없으니 타임아웃이 없고, 엔진 파드가 죽어서 멈출 일도 없다. 고장은 "식 평가 오류"라는 좁은 형태로만 온다.
failurePolicy는 그 경우를 다룬다. - PSA: 내장 어드미션 플러그인이라 역시 외부 의존이 없다. 잘못 켰을 때의 고장은 "이 네임스페이스에서 파드가 안 뜬다"는 국소적 형태다.
- 웹훅 엔진: 외부 파드·네트워크·인증서에 의존한다. 고장 모드가 가장 넓고,
failurePolicy와 범위 설계가 그만큼 중요하다.
그래서 실무의 배치는 대개 이렇게 갈린다. 엔진 없이 표현되는 검증은 VAP·PSA 로 내려 보내 고장 표면을 줄이고, 꼭 필요한 것만 웹훅에 남긴다.
정책을 켜는 날의 순서.
1) 좁은 범위로 시작한다 (한 네임스페이스, 한 종류, 한 동사)2) 경고·감사로만 켜고 며칠 센다3) 걸린 것을 고치거나 좁은 예외로 남긴다4) 범위를 한 단계 넓히고 2..3 을 반복한다5) 마지막에 차단으로 올린다 (정책 본문이 아니라 켜는 쪽만 고친다)6) 되돌리는 절차를 미리 적어 둔다4번과 5번의 순서를 바꾸는 팀이 많다. 좁은 범위에서 곧바로 차단으로 올리고 그다음에 범위를 넓히는 것이다. 그러면 범위를 넓히는 날이 곧 사고의 날이 된다 — 새로 들어온 네임스페이스들이 관찰 없이 곧장 차단을 만나기 때문이다. 넓히는 일과 차단으로 올리는 일을 같은 날에 하지 않는 것이 이 순서의 핵심이다.
현장에서 만나는 모습
첫째, 와일드카드 정책의 지연세. 모든 자원을 매칭하는 웹훅 하나 때문에 API 서버 지연 분포의 꼬리가 통째로 올라간 사례가 흔하다. 원인 파악이 어려운 이유는 정책 자체는 빠르기 때문이다 — 느린 것은 요청 하나가 아니라 요청 전부다.
둘째, 자기 자신을 막은 정책. 엔진 네임스페이스를 범위에서 빼지 않아, 엔진을 재배포하려는 요청이 죽은 엔진에게 물어보다 막힌다. 되살리는 유일한 길이 웹훅 설정을 지우는 것뿐이라 그 절차를 아는 사람을 찾는 동안 장애가 길어진다.
셋째, Ignore 로 켜 두고 잊는 것. 사고를 피하려고 전부 Ignore 로 켜 두면, 엔진이 몇 시간 죽어 있는 동안 검사 없이 들어온 것들을 아무도 모른다. Ignore 를 골랐다면 엔진 가용성 알림이 정책의 일부여야 한다.
넷째, 노드 하나에 몰린 엔진. 레플리카가 셋이어도 같은 노드에 있으면 그 노드가 빠질 때 함께 사라진다. Fail 아래에서는 그것이 곧 쓰기 정지다. 토폴로지 분산과 PodDisruptionBudget 은 정책 도입의 선택 사항이 아니라 준비물이다.
다섯째, 이 환경에서 볼 수 있는 것. kwok 클러스터에서는 닿지 않는 주소를 가진 웹훅 설정을 만들어 Fail 과 Ignore 의 차이를 실제로 볼 수 있다. 같은 요청이 한쪽에서는 막히고 한쪽에서는 통과한다.
참고 문서
- 동적 어드미션 제어(웹훅 설정): https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
- 어드미션 컨트롤러 목록: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/
- ValidatingAdmissionPolicy: https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/
- Pod Security Admission: https://kubernetes.io/docs/concepts/security/pod-security-admission/
다음 실습에서 할 것
정책을 일부러 고장 내고 그 고장을 관찰한다. 닿지 않는 주소를 가진 웹훅 설정을 만들어 failurePolicy: Fail 에서 요청이 막히는 것과 Ignore 에서 통과하는 것을 같은 요청으로 확인하고, namespaceSelector 로 시스템 네임스페이스를 범위에서 빼 탈출구가 생기는 것을 본다. 범위를 자원·네임스페이스·요청 세 층에서 각각 좁혀 가며 어떤 요청이 정책을 지나고 어떤 요청이 지나지 않는지 세어 보고, 마지막으로 좁은 범위 → 경고 → 넓히기 → 차단 순서를 한 바퀴 돌며 되돌리는 절차까지 만든다.