상세 컨텐츠

본문 제목

Dynamic Admission Controller - MutatingWebhook

Kubernetes

by yiaw 2021. 11. 5. 16:53

본문

앞선 포스팅에서 ValidatingWebhook을 구현을 통해 동작 방법을 확인해 보았습니다. 

Dynamic Admission Controller에서 두 번째로 소개해드릴 기능은 MutatingWebhook입니다. 

MutatingWebhook의 경우 사용자가 요청한 Request 내용을 강제로 변경할 수 있는 기능이 있습니다. 

ValidatingWebhook과 동일하게 AdmissionReview라는 메시지를 통해 검토 요청을 받은 후 별도의 내용을 Injection 하는 정보를 포함하여 메시지를 응답하게 됩니다. 

 

AdmissionReview 정보

{
	"kind":"AdmissionReview",
	"apiVersion":"admission.k8s.io/v1beta1",
	"request":{
		"object": {
			... Resource 정보 .... 
 		}
	}
}

validatingWebhook과 동일한 형식의 메시지를 수신 후 object 필드에 사용자가 요청한 k8s resource 정보를 확인하고 변경 하고자 하는 내용이 있을 경우 AdmissionReview 메시지의 response 필드에 데이터를 담아 응답 할 수 있습니다.

{
	"response":{
		"allowed":true,
		"patchType":"JSONPatch",
		"patch":"W3sib3AiOiJhZGQiLCJwYXRoIjoiL3NwZWMvY29udGFpbmVycy8tIiwidmFsdWUiOnsibmFtZSI6ImluamVjdC1wb2QiLCJpbWFnZSI6ImJ1c3lib3giLCJjb21tYW5kIjpbInNsZWVwIiwiMzYwMCJdLCJyZXNvdXJjZXMiOnt9fX1d"
	}
}

추가되는 정보는 json 형식의 데이터를 base64로 인코딩 하여 patch라는 필드에 데이터로 넣어주면 됩니다.

patch 필드의 데이터를 base64로 디코딩 하여 확인할 경우 아래의 메시지가 나옵니다.

[
	{
		"op":"add",
		"path":"/spec/containers/-",
		"value":{
			"name":"inject-pod",
			"image":"busybox",
			"command":["sleep","3600"],
			"resources":{}
		}
	}
]
op는 operation 내용으로 신규 Pod가 추가 될 경우 add를 입력 해줍니다.
path의 경우는 새로 생성되는 Pod를 Injection 하기 위한 경로라고 생각 하시면 됩니다.
여기서 주의사항으로는 기존 AdmissionReview Object 필드에 존재하는 경로로 추가 한다면 path 끝에 "-"를 없다면 경로만 입력 해주면 됩니다. 
예를 들어 위 예시에서는 기존 Pod 생성 요청에 Pod를 Injection 하기 때문에 "/spec/containers/-" 라고 기입 하였다. 
만약 ResponseReview의 내용이 Pod 생성이 아니라 다른 k8s resource이고 이때 새로운 Pod를 생성 한다면 
path의 내용은 "/spec/containers"  까지만 기입 하면 될것입니다.

HTTPS Server

https server에 대한 구현과 인증키 관련 내용은 앞선 포스팅 validatingwebhook을 구현 하는 과정에서 작성 하였기 때문에 본 포스팅에서는 MutatingWebhook 핸들러에 대한 내용만 설명하도록 하겠습니다.

MutatingWebhook Handler

HTTPS Server에서 /mutate URI를 요청 시 호출 할 핸들러를 등록해줍니다.

package main

import (
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"

    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/serializer"
)

var (
    runtimeScheme = runtime.NewScheme()
    codecs        = serializer.NewCodecFactory(runtimeScheme)
    deserializer  = codecs.UniversalDeserializer()
    defaulter     = runtime.ObjectDefaulter(runtimeScheme)
)

func HelloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello handler call\n"))
    w.WriteHeader(http.StatusOK)
}

func main() {
    http.HandleFunc("/hello", HelloHandler)
    http.HandleFunc("/validate", ValidatingWebHook)
    http.HandleFunc("/mutate", MutatingWebHook)

    log.Println("Start HTTPS Server")

    go log.Fatal(http.ListenAndServeTLS(":8443", "server.crt", "server.key", nil))

    signalChan := make(chan os.Signal, 1)
    signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
    <-signalChan

    log.Println("shutdown signal, shutting down webhook server")
}

그리고 아래 소스코드는 Mutate 핸들러 구현부 입니다. 

 

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strings"

    "k8s.io/api/admission/v1beta1"
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
    MutationAnnotations = "yiaw.webhook/mutation"
)

type patchOperation struct {
    Op    string      `json:"op"`
    Path  string      `json:"path"`
    Value interface{} `json:"value,omitempty"`
}

func addContainer(path string) (patch []patchOperation) {
    container := corev1.Container{
        Name:  "inject-pod",
        Image: "busybox",
        Command: []string{
            "sleep", "3600",
        },
    }

    var value interface{}
    value = container
    patch = append(patch, patchOperation{
        Op:    "add",
        Path:  path,
        Value: value,
    })

    return patch
}

func createPatch(pod *corev1.Pod) *v1beta1.AdmissionResponse {
    var patch []patchOperation
    patch = append(patch, addContainer("/spec/containers/-")...)
    patchByte, err := json.Marshal(patch)
    if err != nil {
        return &v1beta1.AdmissionResponse{
            Allowed: true,
        }
    }
    
    log.Printf("Patch: %s", string(patchByte))

    return &v1beta1.AdmissionResponse{
        Allowed: true,
        Patch:   patchByte,
        PatchType: func() *v1beta1.PatchType {
            pt := v1beta1.PatchTypeJSONPatch
            return &pt
        }(),
    }
}

func Mutating(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
    req := ar.Request
    var pod corev1.Pod
    if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
        log.Printf("Could not unmarshal raw object: %v", err)
        return &v1beta1.AdmissionResponse{
            Result: &metav1.Status{
                Message: err.Error(),
            },
        }
    }

    annotations := pod.ObjectMeta.GetAnnotations()
    if annotations == nil {
        return &v1beta1.AdmissionResponse{
            Allowed: true,
        }
    }

    var resp *v1beta1.AdmissionResponse
    switch strings.ToLower(annotations[MutationAnnotations]) {
    case "yes", "true", "ok":
        resp = createPatch(&pod)
    default:
        resp = &v1beta1.AdmissionResponse{
            Allowed: true,
        }
    }

    return resp
}

func MutatingWebHook(w http.ResponseWriter, r *http.Request) {
    var body []byte
    if r.Body != nil {
        if data, err := ioutil.ReadAll(r.Body); err == nil {
            body = data
        }
    }
    if len(body) == 0 {
        log.Println("empty body")
        http.Error(w, "empty body", http.StatusBadRequest)
        return
    }

    log.Printf("Recv Message : %s\n", string(body))

    contentType := r.Header.Get("Content-Type")
    if contentType != "application/json" {
        log.Printf("Content-Type=%s, expect application/json", contentType)
        http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
        return
    }

    var admissionResponse *v1beta1.AdmissionResponse
    ar := v1beta1.AdmissionReview{}
    if _, _, err := deserializer.Decode(body, nil, &ar); err != nil {
        admissionResponse = &v1beta1.AdmissionResponse{
            Result: &metav1.Status{
                Message: err.Error(),
            },
        }
    } else {
        admissionResponse = Mutating(&ar)
    }

    responseReview := v1beta1.AdmissionReview{}
    responseReview.Response = admissionResponse

    resp, err := json.Marshal(responseReview)
    if err != nil {
        http.Error(w, fmt.Sprintf("json Marshal Fail.. err=%v", err), http.StatusInternalServerError)
        return
    }

    log.Printf("Send Message : %s\n", string(resp))
    w.Write(resp)
}

Pod 생성 요청에서 annotations 항목에 해당 "yiaw.webhook/mutation": "true" 라는 값이 존재 한다면 Pod를 Injection 하도록 응답합니다.

이미지 생성 및 배포

이 내용 역시 앞선 포스팅 의 "이미지 생성 및 배포" 내용을 확인해 주시면 감사하겠습니다.

:> cat > mutate.yaml << EOF
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: yiaw-mutator
webhooks:
  - name: rev.mutation.yiaw.io
    namespaceSelector:
      matchExpressions:
      - key: yiaw-org-webhook
        operator: In
        values:
        - "true"
    admissionReviewVersions:
    - v1beta1
    - v1
    clientConfig:
      caBundle:  $(cat ca.crt | base64 | tr -d '\n') ## 앞서 생성한 ca.crt를 경로를 작성
                                                     ## ex) /home/key/ca.crt 에 있으면 
                                                     ## caBundle: $(cat /home/key/ca.crt | base64 | tr -d '\n')
      service:
        name: webhook
        namespace: yiaw
        path: /mutate
        port: 443
    rules:
    - apiGroups:
      - '*'
      apiVersions:
      - v1
      operations:
      - CREATE
      resources:
      - pods
      scope: '*'
    sideEffects: None
EOF

위 내용을 간략하게 설명 하자면 아래와 같습니다.

    namespaceSelector:
      matchExpressions:
      - key: yiaw-org-webhook
        operator: In
        values:
        - "true"

Namspace의 Label 설정이"yiaw-org-webhook" 이 true로 설정된 Namespace들에 대해서만 ResponseReview를 전송하여 검토를 요청 받게 됩니다.

clientConfig:
  caBundle: $(cat ca.crt | base64 | tr -d '\n')
  service:
    name: webhook
    namespace: yiaw
    path: /mutate
    port: 443

kube-apiserver에서는 ResponseReview를 전송하기 위해 필요한 인증키 정보와 webhook 서버의 서비스의 정보 및 URL Path 정보입니다.

caBundle필드에 값은 ca.crt 내용을 base64로 인코딩 된 데이터를 직접 넣어줘야한다. 
"cat > 파일명 << EOF" 구문을 사용한 이유는 inline command를 활용하기 위해서이다.
직접 파일을 생성하여 MutatingWebhookConfiguration을 작성 할 경우 
:> cat ca.crt | base64 | tr -d '\n' 의 출력 결과를 복사해서 caBundle에 넣어 주면 된다.
    rules:
    - apiGroups:
      - '*'
      apiVersions:
      - v1
      operations:
      - CREATE
      resources:
      - pods

이 조건을 통해 pod 생성 요청시에만 AdmissionReview를 요청 받을 수 있습니다. 

정리하자면 

Namspace의 Label 설정이"yiaw-org-webhook=true"고 해당 Namespace에서 Pod 생성 요청이 온 경우 

AdmissionReview를 요청하기 위한 서버 서비스를 정의 하는 내용이 되겠습니다.

자 이제 해당 MutatingWebhookConfiguration Resource를 생성하도록 하겠습니다.

:> kubectl apply -f mutate.yaml
mutatingwebhookconfiguration.admissionregistration.k8s.io/yiaw-mutator created

테스트 진행

다음 명령어를 수행하여 Namespace에 yiaw-org-webhook Label을 설정합니다.

:> kubectl label ns yiaw yiaw-org-webhook=true
namespace/yiaw labeled

:> kubectl get ns -L yiaw-org-webhook
NAME              STATUS   AGE    YIAW-ORG-WEBHOOK
default           Active   39d
kube-system       Active   39d
yiaw              Active   23m    true

이제 yiaw Namespace에 Pod를 생성 해보겠습니다.

apiVersion: v1
kind: Pod
metadata:
  name: test-mutation-deploy
  namespace: yiaw
  labels:
    app: busybox
  annotations:
    yiaw.webhook/mutation: "true"
spec:
  containers:
  - name: main
    image: busybox
    command: ["sleep", "3600"]
:> kubectl get pods 
NAME                   READY   STATUS    RESTARTS   AGE
test-mutation-deploy   2/2     Running   0          92s
webhook                1/1     Running   0          4m12s

새로운 Pod가 Injection것을 확인 할 수 있습니다.


마치며

이번 포스팅에서는 mutatingwebhook을 구현해보았습니다. 

사용자의 필요에 따라 자동으로 Pod를 Injection 할 수도 있으며 요청 정보를 강제로 변경 할 수 있습니다. 

구현된 코드 및 예제는 아래 Github에서 확인 하실 수 있습니다.

https://github.com/yiaw/k8s-example/tree/main/AdmissionController

 

GitHub - yiaw/k8s-example: k8s-example

k8s-example. Contribute to yiaw/k8s-example development by creating an account on GitHub.

github.com

 

'Kubernetes' 카테고리의 다른 글

쿠버네티스 API 접근 제어  (0) 2021.11.04

관련글 더보기

댓글 영역