[Kuberentes] Validating and Mutating Admission Controller / Webhook


서론

이전 페이지에서 Admission Controller의 역할에 대해서 알아봤다.

Admission Controller는 Kube-apisever에서 사용자의 요청을 검증하고 거부하는것 뿐만 아니라 요청 자체를 수정하는 역할을 한다고 하였다.

이전 페이지에서 보았던 NamespaceExists, NamespaceLifecycle Admission Controller는 네임스페이스가 존재하는지 검증하고, 존재하지 않으면 요청을 거부한다.

이러한 컨트롤러를 Validating Admission Controller라고 부른다.

그럼 또 다른 Admission Controller에 해당되는 Mutating Admission Controller에 대해 알아보자.

Mutating Admission Controller

이 컨트롤러는 오브젝트가 생성되기 전에 해당 요청이나 오브젝트를 변경할 수 있다.

이해를 돕기위해 본적으로 활성화되어 있는 DefaultStorageClass라는 Admission Controller 플러그인에 대해 알아보자.

  • 예를 들어, PVC(PersistentVolumeClaim)를 생성하는 요청이 오면 Authorization(인증)과 Authentication(권한) 과정을 거치고 Admission Controller 단계를 진입한다.
  • DefaultStorageClass Admission Controller는 PVC 생성 요청에 storageClass가 명시되어 있는지를 확인한다.
  • 만약 명시되어 있지 않다면, 요청을 수정하여 기본 StorageClass를 자동으로 추가합니다.
  • 이때 사용되는 StorageClass는 클러스터에 기본값으로 설정된 StorageClass입니다.
  • 따라서 PVC가 생성되고 확인해보면, PVC 생성 시에 명시하지 않았더라도 StorageClass가 자동으로 기본값으로 설정되어 있다.
  • 이러한 Admission Controller는 Mutating Admission Controller이라고 한다.
  • 이 컨트롤러는 오브젝트가 생성되기 전에 해당 요청이나 오브젝트를 변경할 수 있다.

Validating/Mutating Admission Controller

Mutating Admission Controller는 요청 내용바꿀 수 있는 컨트롤러이고,

Validating Admission Controller는 요청을 검증해서 허용하거나 거부할 수 있는 컨트롤러이다.

요청을 수정하고 동시에 검증도 할 수 있는 Admission Controller도 있을 수 있다.

일반적으로는 Mutating Admission Controller에 의해 변경된 사항을 검증 과정에서 반영할 수 있도록 하기 위해서 Mutating Admission Controller가 선행되고, 그 다음에 Validating Admission Controller가 실행된다.

아래 예시에서는 NamespaceAutoProvisioning Admission Controller가 Mutating Admission Controller이므로 먼저 실행되고, 그 다음으로 NamespaceExists라는 Validating Admission Controller가 실행된다.

Mutating/Validating Admission Webhook

그렇다면 사용자 정의(커스텀) Admission Controller를 만들기위해 위해 쿠버네티스에서는 MutatingAdmissionWebhook와 ValidatingAdmissionWebhook가 제공된다.

  • 이러한 Admission Webhook은 서버 형태이며, 클러스터 내부나 외부에 존재할 수 있다.
  • Admission Webhook 서버에는 Admission Webhook 서비스가 실행되어야 한다.

동작방식

사용자의 요청이 모든 기본 내장 admission Controller를 통과한 후, 우리가 구성한 webhook으로 전달된다.

  • Admission Webhook 서버에 HTTP 요청을 보낸다.
  • 요청 내용은 JSON 형식의 AdmissionReview 오브젝트로 전달된다.
  • AdmissionReview 오브젝트는 요청에 대한 모든 상세 정보를 포함하고 있다.
    • 예를 들어 요청을 보낸 사용자, 사용자가 하려는 작업의 종류(create, delete 등), 어떤 오브젝트에 대해 작업을 수행하는지, 그리고 그 오브젝트 자체의 정보 등이 포함되어 있다.
  • Admission Webhook 서버는 요청에 대해 허용할지 거부할지를 포함한 또 다른 AdmissionReview 오브젝트 형태로 반환한다.
  • allow 필드가 true이면 허용이고 false이면 거부이다.
+------------------------------+
|        kube-apiserver        |
|       (파드 생성 요청 발생)      |
+-------------+---------------+
              |
              |  Admission 단계
              v
+------------------------------+
|     AdmissionWebhook 설정     |
|     → 서버 URL or 서비스 참조    |
+-------------+---------------+
              |
              |  HTTP POST (AdmissionReview JSON)
              v
+------------------------------+
|  Webhook Server (내 로직)     |
|   → 요청 검증 / 수정 수행        |
|   → 응답: 허용 or 거부          |
+------------------------------+

Admission Webhook 서버 구성하기

정리하면 Kubernetes에서는 Pod이나 리소스가 클러스터에 생성되기 전, 사용자 정의 로직을 통해 검증(validate)하거나 수정(mutate)할 수 있게 해주는 기능이 있는데, 이를 Admission Webhook이라고 한다.

이 기능을 사용하려면 외부 서버(또는 클러스터 내부의 서버)를 배포해야 하고, 이 서버가 Webhook 서버이다.

Webhook 서버는 일반적인 REST API 서버와 같으며, Go, Python, Node.js 등 다양한 언어로 만들 수 있다.

조건

Webhook 서버가 작동하기 위해 꼭 만족해야 하는 두 가지 조건이 있다.

  1. mutate 요청을 처리할 수 있어야 한다 → 리소스를 수정 가능
  2. validate 요청을 처리할 수 있어야 한다 → 리소스를 허용할지 거절할지 결정 가능

즉, Kubernetes API 서버는 리소스를 생성하기 전에 Webhook 서버에 JSON 요청을 보내고, 이 서버는 JSON 응답으로 처리 결과를 Kubernetes에 알려줘야 한다.

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/validate", methods=["POST"])
def validate():
		object_name = request.json["request"]["object"]["metadata"]["name"]
		user_name = request.json["request"]["userInfo"]["name"]
		status = True
		
		if object_name == user_name:
			message = "You can't create objects with your own name"
			status = False
			
    return jsonify({
        "response": {
            "allowed": status,
            "uid": request.json["request"]["uid"],
            "status": {"message" : message}
        }
    })

@app.route("/mutate", methods=["POST"])
def mutate():
    user_name = request.json["request"]["userInfo"]["name"]

		# 어디를 어떻게 바꿔!
		'''
		add: 새로 추가하라
		path: metadata.labels.users 라는 새 필드를 만들어라
		value: 사용자의 이름을 값으로 넣어라
		
		결과물:
		metadata:
		  labels:
		    users: myuser
		'''
    patch = [
        {"op": "add", "path": "/metadata/labels/users", "value": user_name}
    ]  
    
    return jsonify({
        "response": {
            "allowed": True,
            "uid": request.json["request"]["uid"],
            "patch": base64.b64encode(patch),
            "patchType": "JSONPatch"
        }
    })

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=443, ssl_context=("cert.pem", "key.pem"))

Admission Webhook 서버 배포

웹훅 서버 개발이 완료되었다면 이제 배포해야한다.

쿠버네티스 안에 서버를 두는 것이 일반적으로 Deplyoment를 통해 서버를 배포하고 클러스터 내에서 Webhook서버에 접근하기 위해 내부 도메인이 필요함으로 Service를 통해 접근 경로를 제공한다.

  • kind: ValidatingWebhookConfiguration 또는 MutatingWebhookConfiguration 생성
  • clientConfig: Admission Webhook 서버의 위치(URL 또는 서비스)를 지정하는 부분
    • 외부에 서버가 있는경우는 아래 처럼 URL을 작성
    • 내부에 서버가 있는 경우는 service항목을 작성해준다. (아래그림)
  • caBundle:  API 서버와 Webhook 서버 간의 통신은 TLS(HTTPS)를 통해 암호화되어야 한다.
    • 따라서 Webhook 서버에는 서버 인증서와 개인 키 쌍이 설정되어 있어야 한다.
    • caBundle 필드에 해당 인증 기관(CA)의 인증서를 base64 인코딩하여 포함시키면된다.
  • rules: 언제 Webhook을 호출할지 조건을 명시해야 한다.
    • 예를 들어, 파드 생성(create), 삭제(delete), 디플로이먼트 생성 등의 작업에만 Webhook이 호출되도록 할 수 있다.
    • 아래 예시에서는 파드를 생성하는 요청이 들어올 때만 Webhook이 호출되도록 설정한다.

설정 오브젝트(WebhookConfiguration)가 생성되면, 파드를 생성할 때마다, API 서버는 자동으로 webhook-service로 요청을 전송하고, 그 응답에 따라 요청이 허용되거나 거부된다.

Example

예를들어 아래 처럼 구성된 webhook-configuration.yaml 파일이 있다고해보자

그럼 작동순서는 아래와 같다.

  1. 사용자가 kubectl apply -f pod.yaml 같은 명령으로 Pod 리소스 생성을 요청
  2. Kubenetes API Server가 이 요청을 받아들인다.
  3. API Server는 내부적으로 Admission Control 단게를 거친다.
    1. MutatingWebhookConfiguration 및 ValidationWebhookConfiguration에 따라 웹훅 서버로 요청을 전송한다
  4. 지금 정의한 것은 MutatingWebhookConfiguration 이므로 웹훅 서버(webhook-server.webhook-demo.svc)에 /mutate 경로로 HTTPS POST 요청을 전송한다.
  5. 웹훅 서버는 Pod의 내용을 수정하거나 또는 그대로 반환한다.
  6. API Server는 웹훅의 응답을 반영해 리소스를 생성을 수락하거나 거절한다.
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: demo-webhook
webhooks:
  - name: webhook-server.webhook-demo.svc
    clientConfig:
      service:
        name: webhook-server
        namespace: webhook-demo
        path: "/mutate"
      caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURQekNDQWllZ0F3SUJBZ0lVSlZRS3JNMVBqNjVldzNFYXlZZGtHNk9wNy9Vd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0x6RXRNQ3NHQTFVRUF3d2tRV1J0YVhOemFXOXVJRU52Ym5SeWIyeHNaWElnVjJWaWFHOXZheUJFWlcxdgpJRU5CTUI0WERUSTFNRFl4TmpBMk1EQXpOMW9YRFRJMU1EY3hOakEyTURBek4xb3dMekV0TUNzR0ExVUVBd3drClFXUnRhWE56YVc5dUlFTnZiblJ5YjJ4c1pYSWdWMlZpYUc5dmF5QkVaVzF2SUVOQk1JSUJJakFOQmdrcWhraUcKOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXJIbHFNMkk2dVg2K3B2TnIwczMvZmp1bjI5VEJ4bThRbHpaLwpkU3BVN09haEttVjE2em1oZzhPVUV4SnhXRThTZGlWazU1d1VCOEl2QjlOdzFQaHhralhjRnNlZExlUmNibTd2Ck5FTFdlMDhkS1hvbXhnSGtlQktrcUp0cHYxVnRXYzlCMmdwQlBqbmQrZjBDelFqbU8wNGl3ZEhFTWhrSW4zc2YKd2hPMXd0emFRSzJNRHN4UmJQbkdlcVRRUTBRN0Y4RDZsdEpGUXY0R0lqSHhMTzQvT1ZSUllHQ2xJMTNJRDlONQpTT0xEbGFmbUFQa21ZdU9yR1QrSm52Tzcwc21HZWY0MVNRc3BnYnd5eTRlRUhWRnNSeDBRb3Z2TEN0REtlZHM5CnNkZjlvU1p0dmhCTHo5aEFPeW41akQ4Q29DYnRTV0JkSHZYWkRyN2M3QzVLTEdUMWNRSURBUUFCbzFNd1VUQWQKQmdOVkhRNEVGZ1FVaGRaNnBIWGFzTjZrT0I5am04QzBBUDVIVUUwd0h3WURWUjBqQkJnd0ZvQVVoZFo2cEhYYQpzTjZrT0I5am04QzBBUDVIVUUwd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DCkFRRUFEMmc0cklZbEV5NUg0dnRPZXdVejlJeUFMczhwS2NiMWNoWmQ3eVRXdzR5aWhDaGszbjFlckptT1JzRisKa2lmVTlrOC92bnUvQzVMZVhreGtJdUVucXR3bGQ3U1k5QXFWUHk4OHQ0UlJOWHBsckJHbnVWTGowaTI1N2ZpZApQMjR4aCtXSkZtVUNQczhxWG1sWjkyUXl4VGR0eEFMTDloZlcwM2xWUzU3L1YvMXdZQWZuUVhjaS9QTm9Kdnl0CkM5ZzF4MEppMkIzWmlnTkJWS0czMmZNWXdnRzFaaGV5TDNWOGEvTk9hR1ZXWGVQcUwvSDVBQ0JUNXRpa1YxUkQKcFgzYXVZZEZMWm5oWWR3ZnluY1hkbmJzOGxQZG8zZ1daSFA3bUo0ZzJPelNxbU5vVk9UM3ZrNnJxT2hVSVpPMApaQVU2TXB4M04rYTBuY0ROZm9Lc2xFMVZyZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
    admissionReviewVersions: ["v1beta1"]
    sideEffects: None