쿠버네티스는 다른 플랫폼처럼 인증(Authentication)/인가(Athorization) 를 제공해줍니다. 따라서 인증받지 못한 사용자라면 401 (UnAuthorized) 를 응답 받고, 인증은 되었지만 권한이 없다면 403(Forbidden) 응답을 받게 되죠.
아주 평범한 인증/인가 프로세스입니다. 그런데 인증/인가가 완료되었다고쿠버네티스 환경에 바로 적용할 순 있는 건 아닙니다. 사용자가 보낸 요청을 적용하기 직전 한 군데를 더 들립니다. 그것이 바로 Adimission Controller 입니다.
이번 글에서는 Adimission Controller 에 대한 개념을 간단하게 설명하고, Adimission Controller 를 구현해볼 생각입니다. 😎
Admission Controller 가 뭐죠?
공식 문서에 있는 Admission Controller 관련 글을 번역해보자면 이렇습니다.
Admission Controller 는 클러스터의 사용 방식을 제어하는 플러그인입니다. 인증/인가된 API 요청을 Admission Controller 이 가로채어 요청 개체를 변경하거나 요청을 모두 거부할 수 있는 게이트키퍼로 생각할 수 있습니다.
이 그림이 바로 Admission Controller 의 Phases 이죠. 인증(Authentication)/인가(Athorization) 를 거친 후 Mutating Admission 과 Validating Admission 이 API Request 를 가로채게 되는데 이 2개가 Admission Controller 의 컴포넌트입니다.
가로챈 API Request 는 WebHook 방식으로 Admission Controller Server 로 보내지게 됩니다. 그럼 여기서 요청을 변경하거나 거부할 수 있는 로직을 구현하게 되죠.
Mutating Admission 은 주로 API Request 의 개체를 변경하는 용도이고, Validating Admission 은 주로 API Request 를 거부할 지를 결정합니다.
논리적으로는 이 둘의 역할이 각가 다르지만 Validating Admission 에서 꼭 거부만 할 필요 없이 개체를 변경해도 무방합니다.
Admission Controller 의 대표적인 예로는 LimitRanger 가 있습니다. 위에서 Admission Controller 은 플러그인이라고 말씀드렸죠? 30개 정도의 플러그인을 쿠버네티스에서 기본적으로 제공해주는데 LimitRanger 은 그 중 하나입니다.
LimitRanger 은 쿠버네티스에 파드를 배포할 때 만약 resource 를 지정하지 않았다면 default resource 를 작성하여 자동으로 설정해놓은 resource 를 갖게되거나, resource 를 지정하지 않은 파드는 배포 제한을 할 수 있도록 도와줍니다.
적용하는 방법도 어렵지 않습니다. kube-apiserver.yaml 에서 아래와 같이 추가만 해주면 됩니다.
apiVersion: v1
kind: Pod
metadata:
annotations:
kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 10.178.0.2:6443
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/etcd/ec.yaml
- --anonymous-auth=true
- --advertise-address=10.178.0.2
- --allow-privileged=true
- --authorization-mode=Node,RBAC
- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --enable-admission-plugins=LimitRanger # 추가
...
LimitRanger 말고 눈여겨볼만한 플러그인은 PodSecurityPolicy 가 있는데요. 이 플러그인 Pod 와 관련된 보안 정책을 지정할 수 있게 도와주는 플러그인입니다.
예를 들면 SecurityContext 는 1000 이 아닌 root 로 생성하려고 한다면 파드 생성을 거부할 수 있는 정책을 적용할 수 있죠.
하지만 PodSeuciryPolicy 는 쿠버네티스 v1.21 에서 deprecated 되었고, v1.25 에선 완전히 삭제되기 때문에 사용할 수가 없습니다. 그럼 이러한 정책은 어떻게 생성해서 적용해야 할까요?
답은 간단합니다. 만들면 되죠 🤟
그럼 한번 간단하게 Admission Controller 를 만들어보죠!
Admission Controller 를 직접 만들어도 되지만, 이미 잘 만들어진 CNCF 프로젝트 일부인 OPA Gatekeeper 라는 것이 있습니다. 쿠버네티스에서 매우 밀고 있는 프로젝트이기도 하고, 개발해야한다는 번거로움 때문에 아마 많은 분들이 OPA Gatekeeper 를 사용하지 않을까 라는 생각은 듭니다.
그래도 원리는 무엇인지 파악하고 싶으니 만들어보죠.
Admission Controller 를 만들어보자!
매우 간단하게 만들것이기 때문에 Node.js 의 Express 프레임워크를 사용해보도록 하겠습니다!
Node 버전은 18.5 에서 진행됩니다.
Express 프로젝트를 생성해줍니다. 이후 package.json 에 스크립트를 추가합니다.
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon app start",
"prod": "pm2-runtime start app.js -i 1"
}
프로젝트 관련 npm 라이브러리를 설치하고, 정상적으로 동작하는 지 확인합니다.
npm install -i nodemon pm2
npm run dev
Admission Controller Server 는 반드시 TLS 통신을 해야합니다.
즉, http 통신이 아닌 https 로 통신해야합니다.
코드로는 다음과 같습니다.
const bodyParser = require('body-parser');
const express = require('express');
const fs = require('fs');
const https = require('https');
const app = express();
app.use(bodyParser.json());
# 서버 8443 리스닝
const port = 8443;
# tls 통신에 사용할 ca.crt, server.crt, server.key 읽기
const options = {
ca: fs.readFileSync('ca.crt'),
cert: fs.readFileSync('server.crt'),
key: fs.readFileSync('server.key'),
};
# https 통신
const server = https.createServer(options, app);
# 서버 실행
server.listen(port, () => {
console.log(`Server running on port ${port}/`);
});
그럼 이번엔 ca.crt, server.crt, server.key 를 생성해주죠.
# CA Key 와 CA CRT 생성
# X.509는 PKI 기술 중에서 가장 널리 알려진 표준 포맷
openssl req -nodes -new -x509 -keyout ca.key -out ca.crt -subj "/CN=Admission Controller Webhook Demo CA" -sha256
# 서버 Key 생성
openssl genrsa -out server.key 2048
# server.conf 생성
cat >server.conf <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
CN = admission-controller-server.default.svc
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = admission-controller-server.default.svc
EOF
# CSR 생성
openssl req -new -key server.key -out server.csr -config server.conf
# Server CRT 생성
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extensions v3_req -extfile server.conf -sha256
위 명령어대로 진행하는 걸 추천드립니다.
그럼 이제 health check 용 엔드포인트와 webhook 용 엔드포인트 코드를 작성합니다.
app.get('/health', (req, res) => {
res.send('ok');
});
app.post('/', (req, res) => {
if (req.body.request === undefined) {
res.status(400).send();
return;
}
console.log(req.body); // DEBUGGING
const { request: { uid } } = req.body;
res.send({
apiVersion: "admission.k8s.io/v1",
kind: "AdmissionReview",
response: {
uid,
allowed: validate(req)
}
})
});
// Create Pod 에 대한 제한
function validate(req) {
if (req.body['request']['object']['kind'] == 'Pod' && req.body['request']['operation'] == 'CREATE') {
return false
} else {
return true
}
}
GET /health 는 health check 용 엔드포인트이고, POST / 는 webhook 용 엔드포인트 입니다.
그리고 간단하게 Create Pod 에 대한 이벤트를 거부합니다.
즉, Pod 생성은 제한되지만 Pod 외에 Deploy 나 Service 등은 생성이 가능합니다.
참고로 쿠버네티스가 webhook 으로 보내는 Request 객체는 아래와 같습니다.
자세한 정보는 여기를 참고해주세요.
{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "687a5fd9-0939-4d17-a398-d80582b199f8",
"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": "hello-pod",
"namespace": "default",
"operation": "CREATE",
"userInfo": {
"username": "kubernetes-admin",
"groups": []
},
"object": {
"kind": "Pod",
"apiVersion": "v1",
"metadata": [],
"spec": [],
"status": []
},
"oldObject": null,
"dryRun": "false",
"options": {
"kind": "CreateOptions",
"apiVersion": "meta.k8s.io/v1",
"fieldManager": "kubectl-client-side-apply",
"fieldValidation": "Strict"
},
"apiVersion": "v1"
}
}
그럼 이제 이미지로 만들 수 있도록 Dockerfile 을 작성하죠.
FROM node:12.18.2
RUN mkdir /var/node
COPY ./ /var/node
WORKDIR /var/node
RUN npm i
RUN npm i -g pm2
CMD [ "npm", "run", "prod" ]
모든 준비가 끝났습니다. 바로 쿠버네티스 환경에서 방금 만든 Adimission Controller Server 를 배포해보죠 😀
쿠버네티스에 배포
제 쿠버네티스 클러스터는 kubeadm 으로 만들었으며, v1.25 입니다.
admission-controller-server.yaml 을 만듭니다.
apiVersion: v1
kind: Service
metadata:
name: admission-controller-server
spec:
ports:
- port: 443
protocol: TCP
targetPort: 8443
selector:
run: admission-controller-server
---
apiVersion: v1
kind: Pod
metadata:
labels:
run: admission-controller-server
name: admission-controller-server
spec:
containers:
- image: kingbj0429/admission-controller-server
name: admission-controller-server
ports:
- containerPort: 8443
imagePullPolicy: Always
livenessProbe:
httpGet:
port: 8443
path: /health
scheme: HTTPS
readinessProbe:
httpGet:
port: 8443
path: /health
scheme: HTTPS
여기서 정말정말 중요한게 있는데 Service 의 .metadata.name 은 위 server.crt 를 생성하기 위해 사용했던 server.conf 에 명시한 CN 과 이름이 같아야 합니다.
Service 이름이 admission-controller-server 이고, Namespace 가 default 이니 admission-controller-server.default.svc 가 되어야 합니다.
이제 마지막 단계인 WebhookConfiguration 을 생성해주죠. mutate 와 valid 2 종류가 있는데, 여기선 valid 로 생성하겠습니다.
자세한 사항은 여기를 참고해주세요.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: "pod-policy.example.com"
webhooks:
- name: "pod-policy.example.com"
rules:
- apiGroups: ["*"]
apiVersions: ["*"]
operations: ["CREATE"]
resources: ["*"]
scope: "Namespaced"
clientConfig:
service:
namespace: "default"
name: "admission-controller-server" # CN 과 일치해야함
caBundle: $(cat ca.crt | base64 | tr -d '\n')
admissionReviewVersions: ["v1"]
sideEffects: None
timeoutSeconds: 5
간단히 설명하자면 CREATE 이벤트가 발생했을 경우 clientConfig 에 등록된 Service 도메인(admission-controller-server)으로 webhook 을 보내게 됩니다.
모든 준비가 끝났으니 제대로 되었는 지 확인해보죠.
Admission Controller Server 검증
아주 심플한 Pod 를 생성해보죠. 제대로 만들었다면 제한이 되겠죠?
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
Admission Webhook Denied 가 난 걸 보니 문제가 없어 보입니다. 그럼 다른 리소스는 어떨까요?
Error from server: error when creating "nginx.yaml": admission webhook "pod-policy.example.com" denied the request without explanation
Deploy 로 테스트 해보죠.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
생성이 아주 잘됩니다.
deployment.apps/nginx-deployment created
이번 글에서는 Admission Controller 에 대해 알아보고 직접 Server 를 생성까지 해보았습니다.
긴 글 읽어주셔서 감사합니다 😎
코드는 깃헙에 있습니다
'DevOps > Kubernetes' 카테고리의 다른 글
[Kubernetes] 슈뢰딩거의 노드? 노드가 있었는데 없어요 - EKS Fargate (0) | 2023.03.12 |
---|---|
[Kubernetes] PVC로 마음대로 떼었다 붙이는 AWS EBS (0) | 2023.03.06 |
[Kubernetes] Istio Ingress 에 ALB 를 붙여보자 ! (0) | 2023.02.12 |
[Kubernetes] User Account 랑 Service Account 가 다른거였어? (2) | 2022.11.20 |
[Kubernetes] CKA Certification 취득 후기 및 Tips (2) | 2022.01.04 |