앞선 포스팅에서 ValidatingWebhook을 구현을 통해 동작 방법을 확인해 보았습니다.
Dynamic Admission Controller에서 두 번째로 소개해드릴 기능은 MutatingWebhook입니다.
MutatingWebhook의 경우 사용자가 요청한 Request 내용을 강제로 변경할 수 있는 기능이 있습니다.
ValidatingWebhook과 동일하게 AdmissionReview라는 메시지를 통해 검토 요청을 받은 후 별도의 내용을 Injection 하는 정보를 포함하여 메시지를 응답하게 됩니다.
{
"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에 대한 구현과 인증키 관련 내용은 앞선 포스팅 validatingwebhook을 구현 하는 과정에서 작성 하였기 때문에 본 포스팅에서는 MutatingWebhook 핸들러에 대한 내용만 설명하도록 하겠습니다.
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
쿠버네티스 API 접근 제어 (0) | 2021.11.04 |
---|
댓글 영역