전공영역 공부 기록

쿠버네티스 Admission controller

악분 2024. 2. 17. 21:23
반응형

1. Admission controller이란?

쿠버네티스 Admission controller는 말 그대로 Admission 기능을 수행합니다. Admission이라는 영어 단어는 허가를 의미하며, 쿠버네티스 세계에서 Admission, 쿠버네티스 요청을 수락할지 거절할지 결정합니다. 수락된 쿠버네티스 요청은 etcd에 저장됩니다.

 

Admission Controller는 변조(Mutate)와 검증(Validation) 작업을 사용하여 허가 기능을 수행합니다. 변조가 먼저 실행되고 그 다음 검증 작업이 실행됩니다. 변조는 쿠버네티스 요청을 변조하고, 검증은 요청이 기준에 맞는이 확인합니다. 변조 또는 검증 작업이 실패하면 쿠버네티스 요청은 거절됩니다.

 

2. 원리

어떻게 Admission controller가 쿠버네티스 요청을 변조하고 검증할 수 있을까요? 정답은 webhook을 사용합니다. webhook을 설정하면 쿠버네티스가 쿠버네티스 요청을 webhook 웹서버에 전달합니다.

 

지금까지 내용을 시각화 하면 아래그림과 같습니다. 참고로, Admission controller은 인증과 인가를 통과한 쿠버네티스 요청에 대해서만 허가 기능을 수행합니다.

출처: https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/

 

webhook은 쿠버네티스 manifest로 정의합니다. 변조는 MutatingWebhookConfiguration, 검증은 ValidatingWebhookConfiguration을 사용합니다.

 

3. Admission controller 웹서버 개발 방법

Admission controller은 이론적인 개념이고, 실제 동작하기 위해 Admission controller 웹서버를 개발해야 합니다.

 

웹서버는 Admission controller가 정한 규칙을 준수해야 올바르게 동작합니다. 그리고 웹서버는pod로 실행되야 합니다. 언어는 go, python, java 등 웹서버 개발이 가능한 언어면 어떤 언어를 사용하는지 상관없습니다.  웹서버 개발 가이드는 Dynamic Admission Controller문서에 자세히 설명합니다.

 

Admission controller이 정한 중요한 규칙은 아래와 같습니다.

 

3.1 웹서버는 HTTPS프로토콜을 사용한다.

웹서버는 HTTPS프로토콜을 사용해야 합니다. 그러므로 웹서버에 대한 인증서가 필요합니다. webhook manifest에서도 웹서버의 인증서를 설정해야 합니다.

 

 

3.2 쿠버네티스 요청은 AdmissionReview구조로 처리해야 한다.

쿠버네티스 요청은 http프로토콜 body에 있습니다. body포맷은 Admission controller구조를 따라야 하는데, 그 구조가 바로 AdmissionReview입니다 

 

AdmissionReview 구조는 쿠버네티스 API서버에게 어떤 요청을 하는지 내용이 담겨져 있습니다. 자세한 내용은 Dynamic Admission controller문서를 참고하세요.

apiVersion: admission.k8s.io/v1
kind: AdmissionReview
request:
  # Random uid uniquely identifying this admission call
  uid: 705ab4f5-6393-11e8-b7cc-42010a800002

  # Fully-qualified group/version/kind of the incoming object
  kind:
    group: autoscaling
    version: v1
    kind: Scale

이하생략

 

3.3 변조와 검증 작업이 끝난 후 AdmissionReview구조로 리턴해야 한다. 

Admission controller은 webhook으로 중간에 요청을 가로챈 것이기 때문에, 리턴 값도 기존 구조인 AdmissionReview구조로 리턴해야 합니다. 그리고 AdmissionReview안에는 변조와 검증의 성공/실패 결과가 있어야 합니다.

 

예를 들어 Admission controller 변조 또는 검증이 성공적으로 실행 되었다면, responseallowed:true를 리턴합니다. 자세한 내용은 Dynamic Admission controller문서를 참조하세요.

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": true
  }
}

 

 

4. 직접 Admission controller웹서버를 만들어보자

저는 웹서버를 go언어로 개발했습니다. 다른 언어보다 go언어로 개발방법 자료가 많아 go언어를 선택했습니다. 웹서버 코드와 쿠버네티스 설정은 저의 github에 공개되어 있습니다.

 

4.1 웹서버 코드 작성

go언어 http패키지를 사용하여 웹서버 코드를 작성했습니다. HTTPS통신을 하기 위해 443포트를 오픈하고 self-signed 인증서를 사용했습니다.

func main() {
  var port int
  flag.IntVar(&port, "port", 443, "Port to listen on")
  flag.Parse()

  err := http.ListenAndServeTLS(fmt.Sprintf(":%d", port), "/etc/webhook/certs/tls.crt", "/etc/webhook/certs/tls.key", nil)
  if err != nil {
    log.Fatalf("Failed to listen and serve: %v", err)
  }
}

 

 

4.2 인증서 생성

self-signed 인증서는 openssl명령어로 생성하고 쿠버네티스 secret으로 관리했습니다. 

mkdir certs

# 인증서 생성
openssl req -x509 -newkey rsa:4096 -nodes -out certs/ca.crt -keyout certs/ca.key -days 365 -config ./cert.cnf -extensions req_ext

# 인증서를 쿠버네티스 secret을 관리
kubectl create secret tls webhook-certs --cert=certs/ca.crt --key=certs/ca.key --namespace=default

 

웹서버 podsecret을 사용하여 인증서를 마운트했습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: admission-controller-debug-deployment
  namespace: default
spec:
  replicas: 1
  template:    
    spec:
      containers:
      - name: go-container
        volumeMounts:
        - name: webhook-certs
          mountPath: /etc/webhook/certs
          readOnly: true
      volumes:
      - name: webhook-certs
        secret:
          secretName: webhook-certs

 

반응형

4.3 웹서버 pod실행 방법

저희가 개발한 웹서버 동작을 확인하기 위해, 웹서버를 pod로 실행해야 합니다. 매번 기능이 추가될 때마다 Docker build, pod 재생성해야 합니다.

저는 기능이 수정될 때마다 재배포해야 하는 불편함을 최소화하기 위해, go언어가 설치된 dummy pod를 실행하고, 수정한 코드를 수동으로 복사 붙여넣기 했습니다.

 

manifest저의 github을 참고해주세요.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: admission-controller-debug-deployment
  namespace: default
spec:
  replicas: 1
    spec:
      containers:
      - name: go-container
        image: golang
        command: ["sleep"]
        args: ["infinity"]
        ports:
        - containerPort: 443 
        volumeMounts:
        - name: webhook-certs
          mountPath: /etc/webhook/certs
          readOnly: true
      volumes:
      - name: webhook-certs
        secret:
          secretName: webhook-certs

 

 

4.4 검증 핸들러 작성

Admission Controller 기능 중 검증 핵심로직은 아래와 같습니다

 

  1. http requset를 디코딩 합니다. 검증을 하기 위해 AdmissionReview구조가 필요하기 때문에 디코딩 과정이 필요합니다.
  2. AdmissionReview구조에 있는 데이터를 참조하여 검증을 합니다.
  3. 검증 결과를 http 응답에 설정합니다.
func validate(w http.ResponseWriter, r *http.Request) {
  log.Printf("validation handler is called")

  // 요청을 AdmissionReview로 다루기 위해 http body디코딩
admissionReview, err := decodeAdmissionReview(r)

  if err != nil {
    log.Printf("Error decoding admission review request: %v", err)
    http.Error(w, "Error decoding admission review request", http.StatusBadRequest)
    return
}

  // 검증 시작
  AdmissionResponse := validationAdmissionReview(admissionReview)

  responseBody, err := json.Marshal(AdmissionResponse)
  if err != nil {
    log.Printf("Error marshalling admission response: %v", err)
    http.Error(w, "Error marshalling admission response", http.StatusInternalServerError)
    return
  }

  // 검증 결과를 http응답에 설정
  w.Header().Set("Content-Type", "application/json")
  w.Write(responseBody)
}

 

검증을 하는 로직은 별도의 함수로 생성했습니다. 검증함수는 검증이 끝나고 검증 결과를AdmissionReview구조로 리턴합니다. AdmissionReview구조 안에 AdmissionResponse구조가 있으며, 이 구조 안에는 검증 성공/실패 결과가 있습니다.

 

검증 로직에서는 pod label이 있는지 등을 검사할 수 있습니다. 이 글에서는 간단한 예제이므로 아무런 검증을 하지 않았고 검증 성공 결과만 리턴했습니다. AdmissionResponse구조의 Resulthttp.StatusOK를 사용해서 검증을 성공했다고 응답을 설정했습니다.

func validationAdmissionReview(review *admissionv1.AdmissionReview) *admissionv1.AdmissionReview {
  AdmissionResponse := &admissionv1.AdmissionResponse{
    UID:     review.Request.UID,
    Allowed: true,
    Result: &metav1.Status{
      Code:    http.StatusOK,
      Message: "Success",
    },
  }

  return &admissionv1.AdmissionReview{
    TypeMeta: metav1.TypeMeta{
      Kind:       "AdmissionReview",
      APIVersion: "admission.k8s.io/v1",
    },
    Response: AdmissionResponse,
  }
}

 

 

4.5 검증 핸들러 webhook 등록

검증함수가 webhook으로 호출하기 위해 webhook을 등록해야 합니다.

먼저 웹서버에 검증 핸들러 함수를 API로 등록합니다.

func main() {
  var port int
  flag.IntVar(&port, "port", 443, "Port to listen on")
  flag.Parse()

  http.HandleFunc("/validate", validate)
  
이하 생략
}

 

검증wehbookValidationWebhookConfiguration으로 등록합니다. webhook을 등록하기 위해서 이전에 만들었던 self-signed 인증서, 웹서버 pod의 쿠버네티스 service, 웹서버 API가 필요합니다.

 

아래manifestpod생성에 대해 webhook을 설정합니다. 인증서 내용은 설정하기 쉽게 환경변수 ${CA_BUNDLE}로 처리했습니다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: webhook-validation
webhooks:
- name: pod-validation.default.com
  sideEffects: None
  admissionReviewVersions:
  - v1
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
  clientConfig:
    service:
      name: admission-server
      namespace: default
      path: /validate/
    caBundle: "${CA_BUNDLE}"
---
# 웹서버 pod의 service
apiVersion: v1
kind: Service
metadata:
  name: admission-server
  labels:
    app: admission-controller-debug-deployment
spec:
  selector:
    app: admission-controller-debug-deployment
  ports:
  - port: 443
    targetPort: 443

 

아래 쉘스크립트를 사용하여webhook을 등록합니다. 쉘 스크립트는 환경변수 CA_BUNDLE에 인증서 내용을 담고, kubectl applywebhook을 생성합니다.

CA_BUNDLE=$(cat ./certs/ca.crt | base64 | tr -d '\n')
sed -e 's@${CA_BUNDLE}@'"$CA_BUNDLE"'@g' < ./manifests/mutate-webhook.yaml | kubectl apply -f -

 

4.6 검증 핸들러 테스트

pod생성에 대해 webhook을 설정했으므로, pod를 생성할 때마다 웹서버의 검증 핸들러가 동작합니다.

webhook이 잘 동작하는지 buysbox pod를 생성해서 테스트해봅니다.

apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: default
spec:
  terminationGracePeriodSeconds: 0
  containers:
  - name: busybox
    image: busybox
    command: ["sleep"]
    args: ["infinity"]
    resources:
      requests:
        cpu: "50m"
        memory: "64Mi"
      limits:
        cpu: "50m"
        memory: "64Mi"

 

kubectl applybuysbox를 생성합니다. webhook이 잘 등록되어 있다면 pod가 생성되었다는 메세지가 보여야 합니다.

$ kubectl apply -f busybox.yaml
pod/busybox created

 

busybox를 생성하고 웹서버 로그를 확인해보세요. 검증 핸들러가 webhook때문에 호출되었기 때문에 로그가 생성됩니다.

 

4.7 변조 핸들러 작성

변조 핸들러 개발과정도 검증 핸들러와 동일합니다.

 

  1. http requset를 디코딩 합니다. 검증을 하기 위해 AdmissionReview구조가 필요하기 때문에 디코딩 과정이 필요합니다.
  2. AdmissionReview구조에 있는 데이터를 참조하여 쿠버네티스 요청을 변조 합니다.
  3. 변조 결과를 http 응답에 설정합니다

 

func mutateValidate(w http.ResponseWriter, r *http.Request) {
  admissionReview, err := decodeAdmissionReview(r)
  if err != nil {
    log.Printf("Error decoding admission review request: %v", err)
    http.Error(w, "Error decoding admission review request", http.StatusBadRequest)
    return
  }

  // mutate handler
  AdmissionResponse, err := mutateAdmissionReview(admissionReview)
  if err != nil {
    log.Printf("Error mutating admission review: %v", err)
    http.Error(w, "Error mutating admission review", http.StatusInternalServerError)
    return
  }

  responseBody, err := json.Marshal(AdmissionResponse)
  if err != nil {
    log.Printf("Error marshalling admission response: %v", err)
    http.Error(w, "Error marshalling admission response", http.StatusInternalServerError)
    return
  }

  w.Header().Set("Content-Type", "application/json")
  w.Write(responseBody)
}

 

변조 작업은 별도의 함수로 생성했습니다. 변조함수도 검증함수처럼 AdmissionReview구조로 변조 결과를 리턴해야 합니다. AdmissionReview안에는 AdmissionRespone구조로 변조결과가 있어야 합니다. 변조 결과에는 어떤 값을 변조했는지 내용을 설정해야 합니다.

 

아래 예제는 podinitContainer를 추가합니다. pod specinitContainer spec을 추가함으로써 initContainer를 추가했습니다.

func mutateAdmissionReview(review *admissionv1.AdmissionReview) (*admissionv1.AdmissionReview, error) {
  pod := corev1.Pod{}
  err := json.Unmarshal(review.Request.Object.Raw, &pod)
  if err != nil {
    return nil, err
  }

  // Add init container to the pod spec
  initContainer := corev1.Container{
    Name:  "busybox-init-container",
    Image: "busybox:latest",
  }
  pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer)

  containersBytes, err := json.Marshal(&pod.Spec.InitContainers)
  if err != nil {
    return nil, err
  }

  patch := []JSONPatchEntry{
    {
      OP:    "add",
      Path:  "/spec/initContainers",
      Value: containersBytes,
    },
  }

  fmt.Printf("%v", patch)

  // Marshal the patch into JSON bytes
  patchBytes, err := json.Marshal(&patch)
  if err != nil {
    return nil, err
  }

  patchType := admissionv1.PatchTypeJSONPatch

  // Create the admission review response with the patch
  admissionResponse := &admissionv1.AdmissionResponse{
    UID:       review.Request.UID,
    Allowed:   true,
    Patch:     patchBytes,
    PatchType: &patchType,
  }

  return &admissionv1.AdmissionReview{
    TypeMeta: metav1.TypeMeta{
      Kind:       "AdmissionReview",
      APIVersion: "admission.k8s.io/v1",
    },
    Response: admissionResponse,
  }, nil
}

 

 

4.8 변조 핸들러 webhook 등록

변조함수가 webhook으로 호출하기 위해 webhook을 등록해야 합니다.

 

먼저 웹서버에 변조 핸들러 함수를 API로 등록합니다.

func main() {
  var port int
  flag.IntVar(&port, "port", 443, "Port to listen on")
  flag.Parse()

  http.HandleFunc("/validate", handleValidate)
http.HandleFunc("/mutate", mutateValidate)
이하생략
}

 

변조wehbookMutatingWebhookConfiguration으로 등록합니다. webhook을 등록하기 위해서 이전에 만들었던 self-signed 인증서, 웹서버 pod의 쿠버네티스 service, 웹서버 API가 필요합니다.

 

아래manifestpod생성에 대해 webhook을 설정합니다. 인증서 내용은 설정하기 쉽게 환경변수 ${CA_BUNDLE}로 처리했습니다.

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: webhook-mutation
webhooks:
- name: pod-mutation.default.com
  sideEffects: None
  failurePolicy: Fail
  admissionReviewVersions:
  - v1
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
  clientConfig:
    service:
      name: admission-server
      namespace: default
      path: /mutate/
    caBundle: "${CA_BUNDLE}"
---
# 웹서버 pod의 service
apiVersion: v1
kind: Service
metadata:
  name: admission-server
  labels:
    app: admission-controller-debug-deployment
spec:
  selector:
    app: admission-controller-debug-deployment
  ports:
  - port: 443
    targetPort: 443

 

아래 쉘스크립트를 사용하여webhook을 등록합니다. 쉘 스크립트는 환경변수 CA_BUNDLE에 인증서 내용을 담고, kubectl applywebhook을 생성합니다.

CA_BUNDLE=$(cat ./certs/ca.crt | base64 | tr -d '\n')
sed -e 's@${CA_BUNDLE}@'"$CA_BUNDLE"'@g' < ./manifests/mutate-webhook.yaml | kubectl apply -f -

 

4.9 변조 핸들러 테스트

pod생성에 대해 변조 webhook을 설정했으므로, pod를 생성할 때마다 웹서버의 변조 핸들러가 동작합니다. 변조 핸들러가 잘 동작하면 pod에 initContainer가 추가됩니다.

 

webhook이 잘 동작하는지 buysbox pod를 생성해서 테스트해봅니다. initContainer를 설정하지 않았다는 것을 기억하세요.

apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: default
spec:
  terminationGracePeriodSeconds: 0
  containers:
  - name: busybox
    image: busybox
    command: ["sleep"]
    args: ["infinity"]
    resources:
      requests:
        cpu: "50m"
        memory: "64Mi"
      limits:
        cpu: "50m"
        memory: "64Mi"

 

kubectl applybuysbox를 생성합니다. webhook이 잘 등록되어 있다면 pod가 생성되었다는 메세지가 보여야 합니다.

$ kubectl apply -f busybox.yaml
pod/busybox created

 

웹 서버 로그를 보면, 변조 핸들러가 호출 된 다음 검증 핸들러가 호출되었습니다. Admission controller동작 순서가 변조->검증 순서로 실행된다는 것을 확인했습니다.

 

busybox에는 놀랍게도 initContainer가 있습니다. busybox pod manifest에는 initContainer를 설정하지 않았지만, 저희가 개발한 Admission controller 변조 핸들러가 initContainer를 추가했습니다.

kubectl get pod busybox -oyaml

 

5. Admission controller 사용 예

Admission controller 대표적인 사용 예는 쿠버네티스 정책 설정입니다. pod를 생성할 때는 label이 없으면 pod생성을 거절하는 등, 운영에 필요한 정책을 강제할 수 있습니다. 참고로 웹서버를 직접 개발하지 않고 정책을 설정하는 오픈소스가 있습니다. 대표적인 오프소스는 gatekeeper, kyverno입니다.

또 다른 예는 변조를 사용하여 오픈소스 동작에 필요한 설정을 주입합니다. 대표적인 오픈소스는 istio, datadog입니다. istio는 서비스메쉬 기능을 위해 pod가 생성될 때마다 envory proxy컨테이너를 pod에 추가합니다. datadog는 애플리케이션 성능 모니터링(APM)을 하기 위해 initContainer를 pod에 추가하여 모니터링 설정을 합니다.

반응형