Validation Admission Policy
개요
승인 제어을 확장하는 방법 중 가장 많이 쓰였던 방식은 웹훅 방식이었다.
그러나 이를 쿠버 자체적으로 내장하는 방향으로 조금씩 발전이 이뤄지고 있는데, 그 중 첫번째로 구현된 것이 바로 검증 승인 정책이다.
1.30버전 기준으로는 stable 상태이고, 아직 한참 멀었지만 Mutating Admission Policy도 언젠가 GA가 되지 않을까 한다.
문서에서도 검증 승인 웹훅의 선언적, 내장된 대체제라고 표현한다.
이것도 결국 하나의 오브젝트인데, 특징은 CEL 표현식을 이용한다는 것이다.
구조
(위 화살표들은 실제 요청이 검증되기 위해 거치는 흐름을 나타낸 것에 가깝다.)
정책(ValidatingAdmissionPolicy)과 정책 바인딩(ValidatingAdmissionPolicyBinding) 두 가지 기본 리소스가 있다.
- 정책
- 말 그대로 정책과 대상을 담는 오브젝트
- 바인딩
- 범위와 사용될 추가 리소스를 담는 오브젝트
- 파라미터
- 이건 그냥 검증 시 파라미터로서 사용할 다른 추가 리소스를 말한다.
- 위처럼 ConfigMap의 값을 변수마냥 쓰는 경우를 말하는데, 사용방식은 아래에서 더 자세히 보자.
- 참고로 리소스를 파라미터로 넣을 때, 정책을 만드는 유저는 해당 리소스에 대한 read 권한 정도는 있어야만 한다.
ValidatingAdmissionPolicy
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "demo-policy.example.com"
spec:
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas <= 5"
failurePolicy: Fail
정책의 기본적인 로직이 적히는 오브젝트이다.
예시를 보는 게 이해가 빠른데, 일단 디플로이먼트를 만들거나 업데이트하는 요청에 대해 이 정책이 적용된다.
그리고 검증 규칙은 CEL 표현식으로, 레플리카가 5개 이하여야 참이 된다.
만약 거짓이라면 failurePolicy
에 걸리는데, 지금의 경우는 정책에서 실패를 반환한다.
matchConstraints
위처럼 그냥 어떤 리소스를 검증하려는 건지 제약을 지정하는 필드이다.
당연히 와일드카드도 사용할 수 있다.
굳이 꼭 이걸로 씅이 안 차는 당신을 위해... 추가적으로 [[#matchConditions]]를 지정하는 것도 가능하다..
validations
validations:
- expression: "object.spec.replicas <= 5"
reason: Forbidden
message: "params missing but required to bind to this policy"
messageExpression: "'object.spec.replicas must be no greater than ' + string(params.maxReplicas)"
검증을 수행하는 필드로, 리스트로 넣으면 차례대로 검증을 수행한다.
하나라도 거짓을 반환하면 일단 거짓이 반환되는데, 이에 대한 결정은 아래 [[#failurePolicy]]에서 결정된다.
reason
필드를 넣어서 거짓이 출력될 값을 지정할 수 있다.
이 값은 그대로 사용자에게는 HTTP 에러 코드로 나타나게 된다.
가능한 값은 다음의 것들이 있는데 세팅하지 않으면 사용자에게는 StatusReasonInvalid
라고 뜨게 된다.
- Unauthorized
- Forbidden
- Invalid
- RequestEntityTooLarge
message
필드를 통해 출력 메시지를 지정할 수 있는데, 굳이 또 CEL까지 써주고 싶으면 messageExpressions
를 쓰자.
두 필드를 같이 쓸 수 있는데, 우선순위는 표현식 쪽이긴 하다.
근데 간혹 댕청한 관리자가 messageExpressions
에서 에러가 나오게 식을 썼다면, message
필드가 대신 출력된다.
CEL 변수
여기에서 CEL을 사용하게 되니, 구체적으로 어떤 변수를 사용할 수 있는 지도 보자.
검증 정책이 적용될 때 사용되는 각 변수는 다음과 같다.
- object
- 요청으로 만들고자(완성하고자) 하는 오브젝트로, 삭제 요청일 때는 null이다.
- oldObject
- 요청에 대해 현재 존재하는 오브젝트로, 생성 요청일 때는 null이다.
- request
- 기본적으로 승인 요청이 왔을 때 들어오는 모든 값이 담긴, 위의 값을 거의 모두 포함하는 큰 변수이다.
- 자세한 건 이거 참고
- params
- 파라미터를 사용할 때 파라미터를 나타내는 변수인데, 궁금하다면 바로 아래 [[#paramKind]]을 먼저 참고하자.
- namespaceObject
- 들어온 요청이 네임스페이스 종속일 때 해당 네임스페이스 오브젝트
- authorizer
- 요청의 주체에 대해서 인가를 체크할 수 있는 CEL 전용 변수이다.
- 어차피 인가 단계를 거쳐서 왔겠지만, 이걸로 추가 커스텀 체크가 가능하다!
- 방식은 이거 참고하자.
- authorizer.requestResource
- 요청으로 들어온 인가 체크 결과를 담은 변수
오브젝트라는 변수들은 전부 다음의 값은 무조건 가지고 있다.
- apiVersion
- kind
- metadata.name
- metadata.generateName
사실 당연한 거긴 한데, 아무튼 참고하자.
failurePolicy
이 정책의 검증에 대한 실패 정책을 지정한다.
무슨 말이냐, 어떤 요청이 들어오면 이 정책의 CEL 표현식에서 참이나 거짓이 반환될 것이다.
이때 거짓이 반환된 경우 이 정책이 해당 요청에 대해 어떤 결정을 내릴지를 지정한다는 것이다.
여기에는 Ignore
, Fail
두 가지 값이 가능하다.
말 그대로 Ignore는 표현식에서 거짓이 나오게 되더라도 무시라는 결정을 내릴 것이다.
다만 여기에서 Faile을 한다고 무조건 해당 요청이 실패한다는 것은 아니다.
아래 [[#ValidatingAdmissionPolicyBinding]]를 보면 알겠지만, Fail이란 결정이 일어나도, 어떻게 동작할지를 또 세부 지정할 수 있다.
라는 질문을 한 당신 다시 한번 생각해보십시오
Ignore은 정책 설정에 있어 유연성을 더해준다.
일단 테스트 환경일 때 무작정 모든 요청이 fail 뜨도록 하는 것은 디버깅이나 운영을 어렵게 만든다.
그렇기에 처음에는 Ignore로 해뒀다가 본격적으로 운영할 때부터 Fail로 둔다던가 하는 전략이 유효할 것이다.
정책을 쓸 때만 정책을 만들었다 아닐 때 삭제하는 식은 힘들 수 있잖냐..
그리고 정책은 여러 개 작성할 수 있으니, 어느 하나가 잠시 Ignore여도 다른 정책에서 평가될 때 Fail이 뜨게 하는 식으로도 운용이 가능하다.
paramKind
paramKind:
apiVersion: rules.example.com/v1
kind: ReplicaLimit
validations:
- expression: "object.spec.replicas <= params.maxReplicas"
reason: Invalid
- expression: "params != null"
message: "params missing but required to bind to this policy"
정책에 대한 특정 부분들을 정책으로부터 분리시키는 방법이 있다.
가령 다른 정책에서도 많이 쓰이고 통일시켜야 하는 값은 일찌감치 분리시켜 재사용하는 것이 유용할 텐데, 이때 파라미터를 쓴다.
파라미터의 스키마는 spec.paramKind
에 넣어주면 된다.
위 예시는 일단 마음대로 ReplicaLimit이란 CRD를 만들고, 이것을 등록한 것이다.
여기에는 maxReplicas
란 정보가 들어있고, 이것을 CEL 표현식에서 활용했다.
꼭 CRD를 이용해야 하는 건 아니고 단순한 ConfigMap을 이용해도 상관 없다.
- expression: "!has(params.optionalNumber) || (params.optionalNumber >= 5 && params.optionalNumber <= 10)"
message: "params is invaild"
정책을 쓸 때 params가 정말 제대로 된 것인지, 정책 단에서 체크를 해주는 것이 아무래도 안전할 것이다.
그래서 이런 식으로 미리 안전하게 추가 조건을 넣어주는 것이 좋다.
보통 다른 오브젝트를 오브젝트에 명시할 때는 흔히 Ref
라는 접미사를 쓰는 것을 많이 보았을 것이다.
근데 왜 여기는 Kind
접미사가 붙느냐?
하는 당신 역시 칭찬합니다
정책 오브젝트에서는 사용할 파라미터가 어떤 식의 스키마를 가지고 있는지만 명시하여 사용하기 때문이다.
그래서 문서에서는 이걸 고정된(concrete) 형태의 정책 틀을 제공한다고도 표현한다.
아무튼 실제로 이렇게 paramKind
를 이용하면 아래의 [[#ValidatingAdmissionPolicyBinding]]에서도 같이 단서를 제공해줘야 한다.
matchConditions
matchConditions:
- name: 'exclude-leases' # 이름은 고유하게
expression: '!(request.resource.group == "coordination.k8s.io" && request.resource.resource == "leases")'
- name: 'exclude-kubelet-requests'
expression: '!("system:nodes" in request.userInfo.groups)'
- name: 'rbac'
expression: 'request.resource.group != "rbac.authorization.k8s.io"'
위에서 어떤 리소스를 검증하고 싶다!라는 것만으로 부족할 때 사용할 수 있는 필드이다.
정말 열심히 검증 대상을 필터링하고 싶으신 분이라면.. 이걸 쓰자.
보다시피 또 CEL 표현식을 이용해서 검증 대상이 될 놈들을 필터링할 수 있다..
위의 예시를 해석하자면..
- lease 오브젝트가 아닌 놈들만 검증하겠다.
- 노드 그룹이 주체가 아닌 놈들만 검증하겠다.
- rbac 요청이 아닌 것들만 검증하겠다.
그냥 validations 필드에서 잘 지정해도 되는 건 아닐까..? 싶긴 하다.
다만 따지자면, 그런 게 있긴 하다.
a and b
라는 조건문은 not a or not b
와 논리적으로 상반되는데, 딱 봐도 논리적 상반을 나타내기 위해 들이는 수고가 커진다.
(괄호를 써도 되지만 그냥 예시를 위해 넘어간다.)
validations 필드는 거짓 조건을 판별하기 위한 CEL 조건문이 적히기에, 참으로 넘겨도 되는 조건문을 적을 때 상당히 귀찮아질 여지가 있다.
그래서 미리 참으로 넘겨도 될 놈들을 앞서서 필터링한다고 보면 될 것 같다.
- matchConditions에서 거짓인 놈들은 검증에 구애되지 않으니 통과된다.
- validaitions에서 거짓인 놈들은 검증 상 거짓 결정이 내려지니 통과되지 않을 수 있게 된다.
auditAnnotations
auditAnnotations:
- key: "high-replica-count"
valueExpression: "'Deployment spec.replicas set to ' + string(object.spec.replicas)"
쿠버네티스 감사가 이뤄질 때, 추가 메시지를 넣어줄 수 있다.
"annotations": {
"demo-policy.example.com/high-replica-count": "Deployment spec.replicas set to 128"
# other annotations
}
위의 예시는 실제 감사에서 이렇게 표현된다.
variables
spec:
variables:
- name: foo
expression: "'foo' in object.spec.metadata.labels ? object.spec.metadata.labels['foo'] : 'default'"
validations:
- expression: variables.foo == 'bar'
각 검증 규칙에서 여러 번 사용하고 싶은 계산식이 있으면, 이를 또 한꺼번에 모아두는 게 도움이 될 것이다.
이를 위한 변수 필드가 또 있다.
이게 중요한 것이, 결국 표현식은 api 서버의 컴퓨팅 연산을 사용한다.
그래서 컴퓨팅 자원을 가급적 적게 쓰게 만드는 게 좋은데 이걸 활용하면 불필요한 중복 연산을 줄일 수 있을 것이다.
이 값은 정책이 적용될 때 바로 계산되는 건 아니고, 첫번째로 사용하는 검증 규칙이 있을 때 계산된다.
variables:
- name: environment
expression: "'environment' in namespaceObject.metadata.labels ? namespaceObject.metadata.labels['environment'] : 'prod'"
- name: exempt
expression: "'exempt' in object.metadata.labels && object.metadata.labels['exempt'] == 'true'"
- name: containers
expression: "object.spec.template.spec.containers"
- name: containersToCheck
expression: "variables.containers.filter(c, c.image.contains('example.com/'))"
validations:
- expression: "variables.exempt || variables.containersToCheck.all(c, c.image.startsWith(variables.environment + '.'))"
messageExpression: "'only ' + variables.environment + ' images are allowed in namespace ' + namespaceObject.metadata.name"
이런 식으로 활용 예시가 있다.
kubectl create deploy --image=dev.example.com/nginx invalid
이런 명령을 넣으면 이미지가 default 네임스페이스에서 dev로 시작하는 이미지 사용한다고 화낼 것이다.
ValidatingAdmissionPolicyBinding
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: "demo-binding-test.example.com"
spec:
policyName: "demo-policy.example.com"
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels:
environment: test
정책을 실제로 적용할 때는 바인딩 오브젝트를 이용한다.
정책 이름을 적고, 검증에 대한 행동을 정의힌다.
그리고 어떤 리소스들이 정책에 해당하게 될지 범위를 지정해준다.
정책에서는 어떤 api 리소스가 적용될지를 지정했다면, 여기에서는 조금 더 클러스터 차원에서 범위를 정하는 느낌이다.
matchResources
어떤 요청이 정책으로 검증될지를 지정하는 필드이다.
정책의 [[#matchConstraints]]와 비슷하게, 여기에도 resourceRules
를 명시해서 지정하는 것도 가능하다.[1]
그래도 이쪽은 보다시피 namespaceSelector
를 사용할 수도 있어서, 조금 더 클러스터 관리적으로 지정할 수 있다는 점이 장점이랄까.
그래도 실상 같은 방식으로 이중 필터링을 하는 것이나 다름 없긴 하다고 생각한다..
여기에서는 objectSelector
필드를 사용할 수도 있는데, 이건 라벨 셀렉터와 같다.
이때의 라벨은 적용되기 이전, 적용되기 이후 전부 매칭시켜버리니, 조금 광범위하게 매칭하는 거라 보면 되겠다.
validationActions
정책에서 Fail이라고 결정이 났을 때, 해당 요청을 어떻게 처리할지에 대해 행동을 정의하는 필드이다.
- Deny
- Warn
- Audit
리스트로 작성된 것을 보면 알 수 있듯이 여러 개를 한꺼번에 써줄 수 있다.
다만 Deny와 Warn은 Deny가 Warn을 품고 있기에 의미 없다.
paramRef
paramRef:
name: "replica-limit-test.example.com"
namespace: "default"
정책에서 [[#paramKind]]를 쓴다면, 바인딩에서 이렇게 실제 리소스가 무엇인지 명시를 해줘야 한다.
즉, 여기에서 사용할 실제 리소스를 명시하게 되는 것이고, 정책 부분에서는 사용될 리소스의 스키마만 명시했다는 것을 알 수 있다.
paramKind
쪽 예시에서 params != null
이라는 조건을 추가해준 것도 바로 이것 때문이다.
실제 정책은 잘 만들었는데, 바인딩을 할 때 실수로 적절한 paramRef
를 빼먹을 수도 있다.
그런 경우를 제어하기 위해 위에서 저런 조건을 추가해주는 게 좋다.
실제 사용할 파라미터가 네임스페이스 종속적이더라도, 굳이 네임스페이스를 명시해야만 하는 것은 아니다.
만약 해당 바인딩이 test
네임스페이스를 대상으로 한다면, 알아서 그 네임스페이스에 있는 param 오브젝트를 참고하게 된다!
[[#예시]]에서는 다른 이름으로 오브젝트를 만들었지만, 그냥 다른 네임스페이스로 구분을 둔 채 같은 이름으로 오브젝트를 만들어도 알아서 이것을 인식해서 적용해준다는 말이다.
이런 방식은 각 설정을 조금 더 유연하게 해준다는 장점이 있다.
name으로 파라미터를 매칭시켜도 되지만, selector
필드를 이용해서 라벨 셀렉터를 이용하는 방법도 있다!
(진짜 일관성좀 챙겨줬으면)
이렇게 하면 여러 개의 파라미터를 실제 정책에 넘기는 것도 가능해지는데, 이 경우 정책이 평가될 때 파라미터들을 각각 이용해서 정책이 수행된다.
그리고 이 검증들을 AND 조건을 평가해서 결과를 낼 것이다.
예시
조금 더 긴밀한 이해를 위해 예시를 넣자면, 이런 식이다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "replicalimit-policy.example.com"
spec:
failurePolicy: Fail
paramKind:
apiVersion: rules.example.com/v1
kind: ReplicaLimit
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas <= params.maxReplicas"
reason: Invalid
일단 이런 형태의 정책을 만들었다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: "replicalimit-binding-test.example.com"
spec:
policyName: "replicalimit-policy.example.com"
validationActions: [Deny]
paramRef:
name: "replica-limit-test.example.com"
namespace: "default"
matchResources:
namespaceSelector:
matchLabels:
environment: test
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: "replicalimit-binding-nontest"
spec:
policyName: "replicalimit-policy.example.com"
validationActions: [Deny]
paramRef:
name: "replica-limit-prod.example.com"
namespace: "default"
matchResources:
namespaceSelector:
matchExpressions:
- key: environment
operator: NotIn
values:
- test
정책은 하나이지만, 이걸 다양한 바인딩을 할 수 있는데, 서로 다른 네임스페이스에 대해서 적용을 하는 것이 보인다.
그리고 이 둘은 각각 다른 paramRef
를 가지고 있는 게 보인다.
같은 정책인 데도 각 네임스페이스에 대해 다른 정도의 기준을 적용할 수 있다는 말이다!
apiVersion: rules.example.com/v1
kind: ReplicaLimit
metadata:
name: "replica-limit-test.example.com"
maxReplicas: 3
---
apiVersion: rules.example.com/v1
kind: ReplicaLimit
metadata:
name: "replica-limit-prod.example.com"
maxReplicas: 100
이런 식으로 설정하면, 테스트 네임스페이스에서는 레플리카가 3개까지만 가능할 것이다.
반면 테스트가 아닌 네임스페이스에서는 레플리카를 100까지도 둘 수 있게 된다.
디버깅
...
validations:
- expression: "object.replicas > 1" # "object.spec.replicas > 1"라고 써야함
message: "must be replicated"
reason: Invalid
이런 식으로 관리자가 잘못 규칙을 쓰는 케이스가 있을 수 있다.
object 아래에 replicas가 없다면, 타입 에러가 발생한다.
status:
typeChecking:
expressionWarnings:
- fieldRef: spec.validations[0].expression
warning: |-
apps/v1, Kind=Deployment: ERROR: <input>:1:7: undefined field 'replicas'
| object.replicas > 1
| ......^
다행히도 이 경우 정책 오브젝트의 status
필드에 해당 정보가 출력되니 쉽게 디버깅할 수 있다!
근데 유의사항이 있다.
- 한번이라도
matchConstraints
에 와일드카드를 쓰면 지원 안 된다. - 타입 체킹 자체가 컴퓨팅 연산을 너무 소모하기에, 한 정책 내에서 타입 체킹은 10개까지만 해준다.
- 이거 무시하고 사용하더라도 정책 자체는 적용되긴 할 것이다.
- 의도한 대로 적용이 안 될 뿐..
- CRD 파라미터는 아직 체킹을 해주지 않는다.
관련 문서
이름 | noteType | created |
---|---|---|
Admission Control | knowledge | 2025-01-20 |
Admission Webhook | knowledge | 2025-01-20 |
Validation Admission Policy | knowledge | 2025-03-17 |
Kyverno | knowledge | 2025-03-17 |
6W - api 구조와 보안 1 - 인증 | published | 2025-03-15 |
6W - api 보안 2 - 인가, 어드미션 제어 | published | 2025-03-16 |
E-Kyverno 기본 실습 | topic/explain | 2025-03-17 |
E-검증 승인 정책 실습 | topic/explain | 2025-03-17 |
S-exec 명령어가 승인 제어에 걸리는 이유 | topic/shooting | 2025-03-17 |