Mutating Admission Controller 생성해보기
[Admission Controller 관련 글목록]
Kubernetes Version 1.18 에 해당함.
Kubernetes Admission Controller
Mutating Admission Controller 생성해보기
Validating Admission Controller
예제로
iksoon-ns namespace에 Pod가 생성이 되면
mutate: admission-test
labels을 추가하고
먄약 첫번째 container가
peksoon/iksoon_tomcat:1.0.6 image를 사용한다면
peksoon/iksoon_mysql:1.0.2
로 변경하는
Mutating Admission Controller를 생성해보겠음.
0. MutatingAdmissionWebhook 활성화
kubernetes에서는MutatingAdmissionWebhook은
default로 이미 활성화과 되어있는 상태라
default 환경이라면 따로 설정하지 않고 진행해도 됨.
[활성화된 admission controller 조회 명령어]
kube-apiserver -h | grep enable-admission-plugins
만약 목록에 MutatingAdmissionWebhook 항목이 없다면
kube-apiserver container에 들어가서 아래의 명령을 실행
[명령어]
kube-apiserver --enable-admission-plugins=MutatingAdmissionWebhook
1. iksoon-ns namespace생성
iksoon-ns namespace에 pod가 생성됐을 때를 고려해야하니
먼저 namespace를 미리 생성해놓음
[명령어]
kubectl create ns iksoon-ns
[생성 확인]
kubectl get ns
항목 중 iksoon-ns가 있는지 확인
2. Webhook Server Container 생성
2.1. Python Flask를 사용해서 Webhook Server를 구현.
[iksoon-webhook-mutating.py 예제]
from flask import Flask, request, jsonify import pprint import requests import logging import copy import base64 import jsonpatch from requests.auth import HTTPBasicAuth logging.basicConfig(filename = "admission_controller.log", level = logging.DEBUG) app = Flask(__name__) @app.route('/iksoon/mutate', methods=['POST']) def mutate(): message_text = "" patch = "" admissionReview = "" container_image= "" mutating_result = [] # kubernetes API Server의 Request를 해당 webhook으로 가져옴 kube_api_request= request.json logging.info("\n") logging.info('--Kube-apiserver request---') logging.info(kube_api_request) logging.info("\n") #namespace data를 알아옴 request_namespace = kube_api_request["request"]["namespace"] #namesapce 가 "iksoon-ns"과 동일한지 확인 logging.info("----namespace name check result----") logging.info("namespace = %s", request_namespace) #namesapce 가 "iksoon-ns"가 아니라면 mutating 하지 않고 return if "iksoon-ns" != request_namespace: message_text = "namespace is not iksoon-ns (Don't Mutating)" admissionReview = admission_response(True, message_text) logging.info(admissionReview) return jsonify(admissionReview) else: message_text = "namespace is iksoon-ns (Mutating start)" logging.info(message_text) # 첫번째 container image정보를 확인 container_image = kube_api_request["request"]["object"]["spec"]["containers"][0]["image"] logging.info("container image = %s", container_image) # 첫번째 container image가 "peksoon/iksoon_tomcat:1.0.6"과 동일한지 확인 if "peksoon/iksoon_tomcat:1.0.6" == container_image: container_mutating={'op': 'replace', 'path': '/spec/containers/0/image', 'value': 'peksoon/iksoon_mysql:1.0.2'} mutating_result.append(container_mutating) # labels 추가 ( mutating: admission-test ) # 먼저 pod 생성 yaml에 labels 항목이 있는지 확인 # (labels 항목이 없이 생성될 수도 있음) if 'labels' in kube_api_request["request"]["object"]["metadata"]: message_text = "labels is exsist" logging.info(message_text) # labels 항목 중에 mutate가 있는지 확인 # mutate key가 있다면 value를 무조건 admission-test로 변경 if 'mutate' in kube_api_request["request"]["object"]["metadata"]["labels"]: message_text = "mutate labels is exsist (labels mutate)" logging.info(message_text) container_mutating={'op': 'replace', 'path': '/metadata/labels/mutate', 'value': 'admission-test'} # labels 항목 중에 mutate가 없다면 labels 추가 else: message_text = "mutate labels is null (labels add)" logging.info(message_text) labelAdd_mutating={'op': 'add', 'path': '/metadata/labels/mutate', 'value': 'admission-test'} mutating_result.append(labelAdd_mutating) logging.info(labelAdd_mutating) else: message_text = "labels is null (Don't Mutating)" logging.info(message_text) if 0 != len(mutating_result): message_text = "Data mutating" logging.info(mutating_result) patch = jsonpatch.JsonPatch(mutating_result) #jsonpath를 base64 인코딩함. base64_patch = base64.b64encode(patch.to_string().encode("utf-8")).decode("utf-8") uid = kube_api_request["request"]["uid"] admissionReview = admission_response_mutating(True, message_text, uid, base64_patch) else: message_text = "nothing to change" admissionReview = admission_response(True, message_text) # Webhook Server에서 Kubernetes API Server로 response logging.info("\n") logging.info('--Kube-apiserver response result---') logging.info(admissionReview) logging.info("\n") return jsonify(admissionReview) def admission_response(result, message_text): admissionReview = { "response": { "allowed": result, "status": {"message": message_text} } } return admissionReview def admission_response_mutating(result, message_text, uid_data, path_data): #Kube-apiserver로 보내는 response 필드로 아래와 같이 data 포맷이 정해져있어서 #해당 규칙대로 return 해줘야함 #return 값은 patch 항목도 base64로 설정되는것에 주의함. #allowed 항목을 보면 #원래 요청 거절을 validate에서 하지만 해당 값이 false면 mutate 단계에서도 요청을 거절할 수 있음 #거절할 경우 deny를 추가해서 거절에 대한 이유를 같이 return 해주는것이 좋음 admissionReview = { "response": { "allowed": True, "uid": uid_data, "patch": path_data, "patchtype": "JSONPatch", "status": {"message": message_text} } } return admissionReview if __name__ == '__main__': app.run(host= '0.0.0.0', port=6000, debug=True, ssl_context=('/run/secrets/tls/tls.crt', '/run/secrets/tls/tls.key')) |
[jsonpatch 참고]
[response 참고]
https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
log 확인을 위해 logging을 사용함.
ssl_context=('/run/secrets/tls/tls.crt', '/run/secrets/tls/tls.key') 은
해당 flask webhook에 HTTPS로 접근하기 위한 인증서을 설정하는 부분으로
아래에서 인증서를 생성하고
deployment 시 해당 경로에 volume 형태로 mount할 것임.
[python flask test 쿼리 예제]
curl -d '{"request": {"uid":"de47", "namespace":"iksoon-ns", "object": {"spec":{"containers": [{"image": "peksoon/iksoon_tomcat:1.0.6"}] }, "metadata": { "labels": {"mutate": "admission"}} }}}' -H "Content-Type: application/json" -X POST http://10.0.2.5:6000/iksoon/mutate
[kubectl patch 명령으로 jsonpath test]
kubectl patch --type json --local --filename test.yaml --patch "[{"op": "replace", "path": "/namespace", "value": "iksoon-ns"}]" --output yaml
이 명령으로 patch 시 정상적으로 적용되는지 test해보고 webhook code를 수정하는것을 추천
[참고 : Pod 생성 kube_api_request변수에 저장되어있는 data상태 예시]
추후에 구성이 완료되고 Mutating Admission Controller를 통해 request를 받았을 경우
최초의 data상태를 미리 보여드리겠음.
{
"kind":"AdmissionReview",
"apiVersion":"admission.k8s.io/v1beta1",
"request":{
"uid":"6b876f8e-0046-4d81-b59e-c06b1b03f876",
"kind":{
"group":"",
"version":"v1",
"kind":"Pod"
},
"resource":{
"group":"",
"version":"v1",
"resource":"pods"
},
"requestKind":{
"group":"",
"version":"v1",
"kind":"Pod"
},
"requestResource":{
"group":"",
"version":"v1",
"resource":"pods"
},
"name":"iksoon-pod",
"namespace":"iksoon-ns",
"operation":"CREATE",
"userInfo":{
"username":"kubernetes-admin",
"groups":[
"system:masters",
"system:authenticated"
]
},
"object":{
"kind":"Pod",
"apiVersion":"v1",
"metadata":{
"name":"iksoon-pod",
"namespace":"iksoon-ns",
"creationTimestamp":"None",
"labels":{
"app":"test"
},
"annotations":{
"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"test\"},\"name\":\"iksoon-pod\",\"namespace\":\"iksoon-ns\"},\"spec\":{\"containers\":[{\"image\":\"peksoon/iksoon_tomcat:1.0.6\",\"name\":\"iksoon-tomcat\",\"ports\":[{\"containerPort\":8080}]}]}}\n"
},
"managedFields":[
{
"manager":"kubectl",
"operation":"Update",
"apiVersion":"v1",
"time":"2020-09-18T06:59:07Z",
"fieldsType":"FieldsV1",
"fieldsV1":{
"f:metadata":{
"f:annotations":{
".":{
},
"f:kubectl.kubernetes.io/last-applied-configuration":{
}
},
"f:labels":{
".":{
},
"f:app":{
}
}
},
"f:spec":{
"f:containers":{
"k:{\"name\":\"iksoon-tomcat\"}":{
".":{
},
"f:image":{
},
"f:imagePullPolicy":{
},
"f:name":{
},
"f:ports":{
".":{
},
"k:{\"containerPort\":8080,\"protocol\":\"TCP\"}":{
".":{
},
"f:containerPort":{
},
"f:protocol":{
}
}
},
"f:resources":{
},
"f:terminationMessagePath":{
},
"f:terminationMessagePolicy":{
}
}
},
"f:dnsPolicy":{
},
"f:enableServiceLinks":{
},
"f:restartPolicy":{
},
"f:schedulerName":{
},
"f:securityContext":{
},
"f:terminationGracePeriodSeconds":{
}
}
}
}
]
},
"spec":{
"volumes":[
{
"name":"default-token-stkjw",
"secret":{
"secretName":"default-token-stkjw"
}
}
],
"containers":[
{
"name":"iksoon-tomcat",
"image":"peksoon/iksoon_tomcat:1.0.6",
"ports":[
{
"containerPort":8080,
"protocol":"TCP"
}
],
"resources":{
},
"volumeMounts":[
{
"name":"default-token-stkjw",
"readOnly":True,
"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"
}
],
"terminationMessagePath":"/dev/termination-log",
"terminationMessagePolicy":"File",
"imagePullPolicy":"IfNotPresent"
}
],
"restartPolicy":"Always",
"terminationGracePeriodSeconds":30,
"dnsPolicy":"ClusterFirst",
"serviceAccountName":"default",
"serviceAccount":"default",
"securityContext":{
},
"schedulerName":"default-scheduler",
"tolerations":[
{
"key":"node.kubernetes.io/not-ready",
"operator":"Exists",
"effect":"NoExecute",
"tolerationSeconds":300
},
{
"key":"node.kubernetes.io/unreachable",
"operator":"Exists",
"effect":"NoExecute",
"tolerationSeconds":300
}
],
"priority":0,
"enableServiceLinks":True
},
"status":{
}
},
"oldObject":"None",
"dryRun":False,
"options":{
"kind":"CreateOptions",
"apiVersion":"meta.k8s.io/v1"
}
}
}
2.2. 상위 Webhook Server가 실행되는 Container image 생성
DockerFile을 사용해서 생성
[Dockerfile 생성]
원활한 디버깅을 위해 필요한 tool을 전부 설치한 이미지를 생성
FROM centos:7 RUN yum install -y net-tools RUN yum install -y gcc openssl-devel bzip2-devel libffi-devel wget RUN yum install -y make RUN yum update RUN wget https://www.python.org/ftp/python/3.8.5/Python-3.8.5.tgz RUN tar xvf Python-3.8.5.tgz WORKDIR Python-3.8.5/ RUN ./configure --enable-optimizations RUN make altinstall RUN unlink /bin/python RUN ln -s /usr/local/bin/python3.8 /bin/python RUN pip3.8 install flask RUN pip3.8 install jsonpatch RUN pip3.8 install requests RUN mkdir /admission_test WORKDIR /admission_test ADD ./iksoon-webhook-mutating.py . CMD ["python", "/admission_test/iksoon-webhook-mutating.py"] |
[Dockerfile 빌드 명령어]
docker build --tag mutatingadmission:1.0.0 .
[Docker hub에 push 해놓음]
docker tag [생성된 image id] peksoon/admission_test:1.0.0
docker login
docker push peksoon/admission_test:1.0.0
3. Flask Webhook HTTPS 접속용 인증서 생성
[명령어]
webhook-server-tls.crt 생성 시 CN을 iksoon-admission-service.default.svc로 설정했는데
이는 아래에서 생성할 webhook deployment 접속 service object의 domain 을 넣어줘야함.
openssl req -nodes -new -x509 -keyout ca.key -out ca.crt -subj "/CN=iksoon admission controller" openssl genrsa -out webhook-server-tls.key 2048 openssl req -new -key webhook-server-tls.key -subj "/CN=iksoon-admission-service.default.svc" \ | openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -out webhook-server-tls.crt |
[결과]
4. TLS Secret 생성
Secret 을 생성해서 deployment에서 해당 secret 를
volume 행태로 mount해서
flask webhook container에 인증서를 전달할 것임.
[명령어]
kubectl -n default create secret tls webhook-certs \
--cert "webhook-server-tls.crt" \
--key "webhook-server-tls.key"
[결과]
5. Webhook Server deployment 와 Service 생성
[Deployment, Servcie yaml 예시]
apiVersion: apps/v1 kind: Deployment metadata: name: iksoon-admission-webhook namespace: default labels: app: mutating-webhook spec: replicas: 1 selector: matchLabels: app: admission-pod template: metadata: labels: app: admission-pod spec: containers: - name: webhook image: peksoon/admission_test:1.0.0 imagePullPolicy: Always ports: - containerPort: 6000 name: flask-webhook volumeMounts: - name: webhook-tls mountPath: /run/secrets/tls readOnly: true volumes: - name: webhook-tls secret: secretName: webhook-certs --- apiVersion: v1 kind: Service metadata: name: iksoon-admission-service namespace: default spec: ports: - port: 443 targetPort: flask-webhook selector: app: admission-pod |
6. ca.crt 파일 base64 인코딩
[명령어]
openssl base64 -A <"ca.crt"
결과 복사 해놓음
7. Webhook Server deployment 와 Service 생성
MutatingWebhookConfiguration name 은
상위 webhook deployment와 연결되어있는 service의 domain이여야함
service name = iksoon-admission-service
namespace name = default
임으로 생성되는 domain = iksoon-admission-service.default.svc
clientConfig의 service name과 namespace는
상위 webhook deployment와 연결되어있는 service의
service name = iksoon-admission-service
namespace name = default
을 입력함.
path: "/iksoontest"
에서 경로를 "/iksoontest" 로 설정한 이유는
상위 webhook python 코드의
@app.route('/iksoontest', methods=['POST'])
부분에서 경로를 "/iksoontest"로 설정했기 때문.
[mutating_admission_controller.yaml]
apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: mutating-admission-webhook webhooks: - name: iksoon-admission-service.default.svc clientConfig: service: name: iksoon-admission-service namespace: default path: "/iksoon/mutate" caBundle: [여기에 상위에서 복사한 base64 encoding ca.crt 값이 들어감.] rules: # Pod를 생성하는 경우에만 해당 mutating admission이 적용됨. 예) deployment로 생성하면 안됨. - operations: [ "CREATE" ] apiGroups: [""] apiVersions: ["v1"] resources: ["pods"] |
8. 결과 확인 pod 생성 test
[test.yaml]
apiVersion: v1 kind: Pod metadata: name: iksoon-pod namespace: iksoon-ns labels: app: test spec: containers: - name: iksoon-tomcat image: peksoon/iksoon_tomcat:1.0.6 ports: - containerPort: 8080 |
[Pod생성 결과]
상위 예제 yaml로 pod 배포 시
labels 에
mutate: admission-test
항목이 추가되어있고
peksoon/iksoon_tomcat:1.0.6 image를 명시했는데
peksoon/iksoon_mysql:1.0.2로 생성되어있는것을 확인할 수 있음.
제 글을 복사할 시 출처를 명시해주세요.
글에 오타, 오류가 있다면 댓글로 알려주세요! 바로 수정하겠습니다!
참고
[mutatingwebhookconfiguration]
https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
[mutating webhook]
https://github.com/alicek106/k8s-admission-controller-python
https://github.com/morvencao/kube-mutating-webhook-tutorial
'Kubernetes > Kubernetes 이론' 카테고리의 다른 글
Kubernetes Custom Resource 개념 (0) | 2020.11.05 |
---|---|
Validating Admission Controller (0) | 2020.11.05 |
Admission Controller (0) | 2020.11.05 |
Pod Security Policy (PSP) (2) | 2020.09.20 |
Security Context (0) | 2020.09.20 |