참고블로그 : https://ddii.dev/kubernetes/mutating-web-hook/
예제깃헙 : https://github.com/morvencao/kube-sidecar-injector
Admission controller 확장
https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/
Admission controller는 쿠버네티스 api-server의 오브젝트(Pod,등) 생성 요청을 가로체어 제어를 할 수 있는 확장 기능으로 플러그인 형태로 사용자가 추가 할 수 있다.
좀더 자세히 확인해보자
- 클러스터 관리자가 kube-api를 직접 컴파일 하고 구성해야 하기 때문에 유연하게 사용하기 어려움
- 1.7 버전 이후부터는 이러한 제한사항을 해결하기 위해 alpha feature로 Initializers 와 External Admission Webhooks 기능이 도입됨
- External Admission Webhooks 는 k8s 리소스의 유효성 검사를 하는데 활용, 유효성 검사를 통과 하지 못하면 해 당 리소스는 쿠버네티스에서 생성되질 않게 할 수 있음.
- 1.9 버전에서는 External Admission Webhooks 은 beta로 승격되어 MutatingAdmissionWebhook 및 ValidatingAdmissionWebhook으로 나눠졌지만 Initializers 는 alpha 로 유지됨
- MutatingAdmissionWebhook 은 유효성 검사 이외에도 승인 과정시 k8s object에 변경을 할수 있음
- 예를 들면 resource quota를 변경한다던지 Agones 및 istio와 같은 Custom Resource 를 수정하여 object를 생성이 가능함
- Webhook 방식은 gRPC 프로토콜을 사용하는 데 개발언어에 구애 받지 않고 확장을 할 수 있다는 장점이 있음
Webhook을 언제 쓰는가?
Webhook을 사용하여 k8s cluster-admin이 api-server를 다시 컴파일하지 않고도 object생성 요청시 mutating(변경) 및 validation(유효성검증) 을 하는 플러그인을 만들 수 있다.
이를 통해 개발자는 모든 resource 에서 여러 작업 ( "CREATE", "UPDATE", "DELETE"...)에 대한 승인 로직에 대해 사용자 정의 할 수있는 유연성을 제공받는다.
Use-Case
- resource를 생성하기 전에 변경
(예, Istio 에서 처럼 traffic management 와 policy enforcement 을 위해 Envoy sidecar container를 injection) - StorageClass Provisioning 자동화
(PersistentVolumeClaim object 생성을 모니터링하고 미리 정의 된 정책에 따라 객체에 storage를 자동으로 추가. 사용자는 StorageClass 생성 에 신경 쓸 필요가 없음) - 복잡한 custom resource 검증 (Agones와 같은)namespace 제한
멀티 테넌트 시스템에서는 reserved namespace에 resource생성을 금지시킬때 사용할수 있음 - 참고 예시
https://github.com/kelseyhightower/denyenv-validating-admission-webhook
이러한 작업을 수행하기 위해 Admission Controller는 두 가지 단계를 거치도록 구현되어 있다. 하나는 Mutate 이고 다른 하나는 Validate 이다 (일부러 영어를 쓰려고 하는 것이 아니라, 쿠버네티스에서 각 단계를 정말 이렇게 부른다). Mutate 단계에서는 사용자가 요청한 API의 매니페스트를 검사해 적절한 값으로 수정하는 변형 (Mutate) 작업을 수행한다. Validate 단계에서는 매니페스트가 유효한지를 검사 (Validate) 함으로써 요청을 거절 (Deny) 할 것인지를 결정한다.
이미지 출처 : https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/

어떻게 동작하는가?
MutatingWebhookConfiguration 내에 정의된 룰에 따라 etcd로 전달되기 전에 request를 intercept한다.
webhook 서버에 승인 요청을 전송하여 변이를 실행한다.
webhook 서버는 API를 준수하는 단순한 http서버.

튜토리얼
https://github.com/morvencao/kube-mutating-webhook-tutorial
위 튜토리얼은 object가 생성되기 전에 pod에 nginx sidecar container를 inject하는 MutatingAdmissionWebhook을 배포하는 내용을 담고 있다.
우선 admissionregistration.k8s.io/v1beta1 API를 사용할수 있는 k8s 1.9+ 이상의 클러스터가 필요하다.
확인방법
$ kubectl api-versions | grep admissionregistration.k8s.io/v1beta1
아래와 같은 결과가 나와야함
admissionregistration.k8s.io/v1beta1
Build하기
일단 Go가 설치되어 있어야 한다. ~/go/src 아래에 clone을 하였음.
$ cd ~/go/src
$ git clone https://github.com/morvencao/kube-mutating-webhook-tutorial.git
의존성 관리를 위해 repo는 dep를 사용함
$ cd kube-mutating-webhook-tutorial
$ go get -u github.com/golang/dep/cmd/dep
build 파일 확인하고 registry 위치를 바꿈
dep ensure
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o kube-mutating-webhook-tutorial .
docker build --no-cache -t registry.*****.io/agones/sidecar-injector:v1 .
rm -rf kube-mutating-webhook-tutorial
docker push registry.*****.io/agones/sidecar-injector:v1
build하고 docker image push
$ ./build
Sending build context to Docker daemon 44.29MB
Step 1/3 : FROM alpine:latest
---> 3fd9065eaf02
Step 2/3 : ADD kube-mutating-webhook-tutorial /kube-mutating-webhook-tutorial
---> 8679ccbab536
Step 3/3 : ENTRYPOINT ["./kube-mutating-webhook-tutorial"]
---> Running in 7699ff5c0885
Removing intermediate container 7699ff5c0885
---> 2014100d460e
Successfully built 2014100d460e
Successfully tagged registry.*****.io/agones/sidecar-injector:v1
The push refers to repository [registry.*****.io/agones/sidecar-injector]
2456c1309a51: Pushed
cd7100a72410: Layer already exists
v1: digest: sha256:15c335daeba40ddcbfbc3631ab6daa7cf623b63420f0ae8b657755322ef0582d size: 739
sidecar deployment에 사용되는 secret(cert/key)을 생성한다.
./deployment/webhook-create-signed-cert.sh \
--service sidecar-injector-webhook-svc \
--secret sidecar-injector-webhook-certs \
--namespace default
위에서 생성된 클러스터의 caBundle값을 가지고 MutatingWebhookConfiguration 생성한다.
cat deployment/mutatingwebhook.yaml | \
deployment/webhook-patch-ca-bundle.sh > \
deployment/mutatingwebhook-ca-bundle.yaml
resource들 deploy
$ kubectl create -f deployment/nginxconfigmap.yaml
kubectl create -f deployment/configmap.yaml
kubectl create -f deployment/deployment.yaml
kubectl create -f deployment/service.yaml
kubectl create -f deployment/mutatingwebhook-ca-bundle.yaml
configmap "nginx-configmap" created
configmap "sidecar-injector-webhook-configmap" created
deployment.extensions "sidecar-injector-webhook-deployment" created
service "sidecar-injector-webhook-svc" created
mutatingwebhookconfiguration.admissionregistration.k8s.io "sidecar-injector-webhook-cfg" created
webhook deployment 확인
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
sidecar-injector-webhook-deployment-796955558f-js6bb 1/1 Running 0 3m
$ kubectl get deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
sidecar-injector-webhook-deployment 1 1 1 1 3m
default 네임스페이스에 sidecar-injector 라벨링을 한다. 이렇게 하면 해당 네임스페이스에 생성되는 모든 app에 자동으로 injection하게 됨
$ kubectl get namespace -L sidecar-injector
NAME STATUS AGE SIDECAR-INJECTOR
agones-system Active 1d
default Active 19d enabled
ibm-cert-store Active 19d
ibm-system Active 19d
ingress-test Active 6d
kube-public Active 19d
kube-system Active 19d
spinnaker Active 12d
xonotic Active 1d
샘플앱을 디플로이 해보자
$ cat <<EOF | kubectl create -f -
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: sleep
spec:
replicas: 1
template:
metadata:
annotations:
sidecar-injector-webhook.morven.me/inject: "yes"
labels:
app: sleep
spec:
containers:
- name: sleep
image: tutum/curl
command: ["/bin/sleep","infinity"]
imagePullPolicy:
EOF
deployment.extensions "sleep" created
sidecar container injection 확인
아래 결과를 보면 하나의 deployment 하나의 container 생성을 요청했지만 nginx sidecar 컨테이너가 injection 된것을 확인할 수 있다.
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
sidecar-injector-webhook-deployment-796955558f-js6bb 1/1 Running 0 2h
sleep-74b46f8bd7-r9l7f 2/2 Running 0 42s
$ kubectl describe pod sleep-74b46f8bd7-r9l7f
Name: sleep-74b46f8bd7-r9l7f
Namespace: default
Node: 10.178.188.16/10.178.188.16
Start Time: Wed, 27 Jun 2018 13:12:47 +0900
Labels: app=sleep
pod-template-hash=3060294683
Annotations: kubernetes.io/psp=ibm-privileged-psp
sidecar-injector-webhook.morven.me/inject=yes
sidecar-injector-webhook.morven.me/status=injected
Status: Running
IP: 172.30.169.30
Controlled By: ReplicaSet/sleep-74b46f8bd7
Containers:
sleep:
Container ID: docker://728ca7f8e741ad29369312bc006c79683e7e605f3b04586df2477e233f93e451
Image: tutum/curl
Image ID: docker-pullable://tutum/curl@sha256:b6f16e88387acd4e6326176b212b3dae63f5b2134e69560d0b0673cfb0fb976f
Port: <none>
Host Port: <none>
Command:
/bin/sleep
infinity
State: Running
Started: Wed, 27 Jun 2018 13:13:01 +0900
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-czzpj (ro)
sidecar-nginx:
Container ID: docker://94fd41a0e153de6d5639873ccbd6b6325cee1ea8351dd02ab4a48ab4004d0b58
Image: nginx:1.12.2
Image ID: docker-pullable://nginx@sha256:72daaf46f11cc753c4eab981cbf869919bd1fee3d2170a2adeac12400f494728
Port: 80/TCP
Host Port: 0/TCP
State: Running
Started: Wed, 27 Jun 2018 13:13:08 +0900
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/etc/nginx from nginx-conf (rw)
Conditions:
Type Status
Initialized True
Ready True
PodScheduled True
Volumes:
default-token-czzpj:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-czzpj
Optional: false
nginx-conf:
Type: ConfigMap (a volume populated by a ConfigMap)
Name: nginx-configmap
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 1m default-scheduler Successfully assigned sleep-74b46f8bd7-r9l7f to 10.178.188.16
Normal SuccessfulMountVolume 1m kubelet, 10.178.188.16 MountVolume.SetUp succeeded for volume "nginx-conf"
Normal SuccessfulMountVolume 1m kubelet, 10.178.188.16 MountVolume.SetUp succeeded for volume "default-token-czzpj"
Normal Pulling 1m kubelet, 10.178.188.16 pulling image "tutum/curl"
Normal Pulled 55s kubelet, 10.178.188.16 Successfully pulled image "tutum/curl"
Normal Created 55s kubelet, 10.178.188.16 Created container
Normal Started 55s kubelet, 10.178.188.16 Started container
Normal Pulling 55s kubelet, 10.178.188.16 pulling image "nginx:1.12.2"
Normal Pulled 48s kubelet, 10.178.188.16 Successfully pulled image "nginx:1.12.2"
Normal Created 48s kubelet, 10.178.188.16 Created container
Normal Started 48s kubelet, 10.178.188.16 Started container
정리
결국 위에서 언급한것 처럼 MutationWebhook은 istio RouteRule같은 별도의 CustomResource등을 injection 하거나 agones 등과 같이 게임서버외에 client sdk 통신을 위한 injection 형태로 기존 resource에 추가적인 변경(mutation) 또는 검증(validation)등의 추가적인 작업을 kube-api의 컴파일없이 가능하다는데 목적이 있다고 볼 수 있다. 추가적으로 기능에 대한 내용은 이후 다시 정리해볼 예정이다.
실습
https://github.com/morvencao/kube-sidecar-injector
튜토리얼은 object가 생성되기 전에 pod에 nginx sidecar container를 inject하는 MutatingAdmissionWebhook을 배포하는 내용을 담고 있다.
GitHub - morvencao/kube-sidecar-injector: A Kubernetes mutating webhook server that implements sidecar injection
A Kubernetes mutating webhook server that implements sidecar injection - GitHub - morvencao/kube-sidecar-injector: A Kubernetes mutating webhook server that implements sidecar injection
github.com

admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1beta1
다음과 같이 나와야함
참고: 또한 MutatingAdmissionWebhook및 ValidatingAdmissionWebhook허용 컨트롤러가 kube-apiserver의 허용 제어 플래그에 올바른 순서로 추가되고 나열되어야 한다.
빌드 및 배포
make 안쓰고 직접 했음
cp deploy/kustomization.yaml deploy/kustomization.yaml.tmp
cd deploy && $ kustomize edit set image sidecar-injector=docker.io/hellouz818/sidecar-injection-test:0.1
kustomize build deploy | kubectl apply -f -
mv deploy/kustomization.yaml.tmp deploy/kustomization.yaml




Name: alpine
Namespace: test-ns
Priority: 0
Service Account: default
Node: docker-desktop/192.168.65.4
Start Time: Thu, 25 May 2023 03:31:16 +0900
Labels: run=alpine
Annotations: sidecar-injector-webhook.morven.me/status: injected
Status: Running
IP: 10.1.0.27
IPs:
IP: 10.1.0.27
Containers:
alpine:
Container ID: docker://3111aca346d09e8a7f3eee3e548865760a17a29328b8b9b44775ab745262bace
Image: alpine
Image ID: docker-pullable://alpine@sha256:02bb6f428431fbc2809c5d1b41eab5a68350194fb508869a33cb1af4444c9b11
Port: <none>
Host Port: <none>
Command:
sleep
infinity
State: Running
Started: Thu, 25 May 2023 03:31:22 +0900
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-pslwp (ro)
sidecar-nginx:
Container ID: docker://c1941bdcccff612bb21bffe2e75896a8094444c9bac3af0bee101d0694268b8a
Image: nginx:1.12.2
Image ID: docker-pullable://nginx@sha256:72daaf46f11cc753c4eab981cbf869919bd1fee3d2170a2adeac12400f494728
Port: <none>
Host Port: <none>
State: Running
Started: Thu, 25 May 2023 03:31:33 +0900
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-pslwp (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
kube-api-access-pslwp:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
nginx-conf:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
Medium:
SizeLimit: <unset>
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 52s default-scheduler Successfully assigned test-ns/alpine to docker-desktop
Normal Pulling 51s kubelet Pulling image "alpine"
Normal Pulled 46s kubelet Successfully pulled image "alpine" in 4.497154169s
Normal Created 46s kubelet Created container alpine
Normal Started 46s kubelet Started container alpine
Normal Pulling 46s kubelet Pulling image "nginx:1.12.2"
Normal Pulled 35s kubelet Successfully pulled image "nginx:1.12.2" in 11.345005504s
Normal Created 35s kubelet Created container sidecar-nginx
Normal Started 35s kubelet Started container sidecar-nginx
sidecar container injection 확인
아래 결과를 보면 하나의 deployment 하나의 container 생성을 요청했지만 nginx sidecar 컨테이너가 injection 된것을 확인할 수 있다.