1. 소개
쿠버네티스로 한동안 작업한 후에는 많은 상용구 코드가 관련되어 있음을 곧 알게 될 것입니다. 간단한 서비스의 경우에도 일반적으로 매우 장황한 YAML 문서의 형식을 취하여 필요한 모든 세부 정보를 제공해야 합니다.
또한 주어진 환경에 배포된 여러 서비스를 처리할 때 해당 YAML 문서에는 반복되는 요소가 많이 포함되는 경향이 있습니다. 예를 들어 지정된 ConfigMap 또는 일부 사이드카 컨테이너를 모든 배포에 추가할 수 있습니다.
이 기사에서는 DRY 원칙을 고수하고 Kubernetes 승인 컨트롤러를 사용하여 이 모든 반복 코드를 피할 수 있는 방법을 살펴보겠습니다.
2. 입학 사정관이란?
승인 컨트롤러는 API 요청이 인증된 후 실행되기 전에 사전 처리하기 위해 Kubernetes에서 사용하는 메커니즘입니다.
API 서버 프로세스( kube-apiserver )에는 이미 API 처리의 특정 측면을 담당하는 여러 내장 컨트롤러가 있습니다.
AllwaysPullImage 가 좋은 예입니다. 이 허용 컨트롤러는 포드 생성 요청을 수정하므로 정보를 받은 값에 관계없이 이미지 가져오기 정책이 "항상"이 됩니다. Kubernetes 설명서 에는 표준 허용 컨트롤러의 전체 List이 포함되어 있습니다.
실제로 kubeapi-server 프로세스 의 일부로 실행되는 내장 컨트롤러 외에도 쿠버네티스 는 외부 허용 컨트롤러도 지원합니다. 이 경우 승인 컨트롤러는 API 서버에서 오는 요청을 처리하는 HTTP 서비스일 뿐입니다.
또한 이러한 외부 승인 컨트롤러는 동적으로 추가 및 제거할 수 있으므로 동적 승인 컨트롤러라는 이름이 지정되었습니다. 그 결과 다음과 같은 처리 파이프라인이 생성됩니다.
여기에서 들어오는 API 요청이 인증되면 지속성 계층에 도달할 때까지 각 내장 승인 컨트롤러를 통과하는 것을 볼 수 있습니다.
3. 승인 컨트롤러 유형
현재 승인 컨트롤러에는 두 가지 유형이 있습니다.
- 허용 컨트롤러 변경
- 유효성 검사 승인 컨트롤러
이름에서 알 수 있듯이 주요 차이점은 들어오는 요청에 대해 각각 수행하는 처리 유형입니다. 변경 컨트롤러는 요청을 다운스트림으로 전달하기 전에 수정할 수 있지만 유효성 검사 컨트롤러는 유효성 검사만 할 수 있습니다.
이러한 유형에 대한 중요한 점은 API 서버가 이를 실행하는 순서입니다. 변경 컨트롤러가 먼저 오고 그 다음 유효성 검사 컨트롤러가 옵니다. 변경 컨트롤러에 의해 변경될 수 있는 최종 요청이 있는 경우에만 유효성 검사가 발생하므로 이는 의미가 있습니다.
3.1. 입학 검토 요청
기본 제공 승인 컨트롤러(변경 및 유효성 검사)는 간단한 HTTP 요청/응답 패턴을 사용하여 외부 승인 컨트롤러와 통신합니다.
- 요청: 요청 속성 에서 처리할 API 호출을 포함 하는 AdmissionReview JSON 객체
- Response: 응답 속성 에 결과를 포함하는 AdmissionReview JSON 객체
다음은 요청의 예입니다.
{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "c46a6607-129d-425b-af2f-c6f87a0756da",
"kind": {
"group": "apps",
"version": "v1",
"kind": "Deployment"
},
"resource": {
"group": "apps",
"version": "v1",
"resource": "deployments"
},
"requestKind": {
"group": "apps",
"version": "v1",
"kind": "Deployment"
},
"requestResource": {
"group": "apps",
"version": "v1",
"resource": "deployments"
},
"name": "test-deployment",
"namespace": "test-namespace",
"operation": "CREATE",
"object": {
"kind": "Deployment",
... deployment fields omitted
},
"oldObject": null,
"dryRun": false,
"options": {
"kind": "CreateOptions",
"apiVersion": "meta.k8s.io/v1"
}
}
}
사용 가능한 필드 중 일부는 특히 중요합니다.
- operation : 이 요청이 리소스를 생성, 수정 또는 삭제할지 여부를 알려줍니다.
- object: 처리 중인 리소스의 사양 세부 정보입니다.
- oldObject: 리소스를 수정하거나 삭제할 때 이 필드는 기존 리소스를 포함합니다.
예상 응답은 응답 대신 응답 필드 가 있는 AdmissionReview JSON 개체 이기도 합니다 .
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "c46a6607-129d-425b-af2f-c6f87a0756da",
"allowed": true,
"patchType": "JSONPatch",
"patch": "W3sib3A ... Base64 patch data omitted"
}
}
응답 개체의 필드 를 분석해 보겠습니다 .
- uid : 이 필드의 값은 들어오는 요청 필드 에 있는 해당 필드와 일치해야 합니다.
- 허용됨: 검토 작업의 결과입니다. true 는 API 호출 처리가 다음 단계로 진행될 수 있음을 의미합니다.
- patchType: 허용 컨트롤러 변경에만 유효합니다. AdmissionReview 요청 에서 반환된 패치 유형을 나타냅니다.
- patch : 들어오는 개체에 적용할 패치입니다. 다음 섹션에 대한 세부 정보
3.2. 패치 데이터
변경 승인 컨트롤러의 응답에 있는 패치 필드는 API 서버에 요청을 진행하기 전에 변경해야 할 사항을 알려줍니다 . 해당 값은 API 서버가 수신 API 호출의 본문을 수정하는 데 사용하는 명령 배열을 포함하는 Base64로 인코딩된 JSONPatch 개체입니다.
[
{
"op": "add",
"path": "/spec/template/spec/volumes/-",
"value":{
"name": "migration-data",
"emptyDir": {}
}
}
]
이 예제 에는 배포 사양 의 볼륨 배열에 볼륨을 추가하는 단일 명령이 있습니다. 패치를 다룰 때 일반적인 문제는 요소가 원래 객체에 이미 존재하지 않는 한 기존 배열에 요소를 추가할 방법이 없다는 사실입니다 . 이는 Kubernetes API 개체를 처리할 때 특히 성가신 일입니다. 가장 일반적인 개체(예: 배포)에는 선택적 배열이 포함되기 때문입니다.
예를 들어 앞의 예는 들어오는 배포 에 이미 하나 이상의 볼륨이 있는 경우에만 유효합니다. 그렇지 않은 경우 약간 다른 명령을 사용해야 합니다.
[
{
"op": "add",
"path": "/spec/template/spec/volumes",
"value": [{
"name": "migration-data",
"emptyDir": {}
}]
}
]
여기에서 값이 볼륨 정의를 포함하는 배열인 새 볼륨 필드를 정의했습니다. 이전에는 값이 기존 배열에 추가되었기 때문에 값이 개체였습니다.
4. 샘플 사용 사례: Wait-For-It
이제 허용 컨트롤러의 예상 동작에 대한 기본적인 이해가 있으므로 간단한 예제를 작성해 보겠습니다. 특히 마이크로서비스 아키텍처를 사용할 때 Kubernetes의 일반적인 문제는 런타임 의존성을 관리하는 것입니다. 예를 들어 특정 마이크로서비스가 데이터베이스에 액세스해야 하는 경우 전자가 오프라인이면 시작할 필요가 없습니다.
이와 같은 문제를 해결하기 위해 pod 와 함께 initContainer 를 사용 하여 기본 컨테이너를 시작하기 전에 이 검사를 수행할 수 있습니다 . 이를 수행하는 쉬운 방법은 도커 이미지 로도 제공되는 대중적인 wait-for-it 셸 스크립트를 사용하는 것 입니다.
스크립트는 호스트 이름 과 포트 매개변수를 사용하여 연결을 시도합니다. 테스트가 성공하면 성공적인 상태 코드와 함께 컨테이너가 종료되고 포드 초기화가 진행됩니다. 그렇지 않으면 실패하고 연결된 컨트롤러가 정의된 정책에 따라 계속 재시도합니다. 이 비행 전 검사를 외부화할 때 멋진 점은 연결된 모든 Kubernetes 서비스가 실패를 알 수 있다는 것입니다. 결과적으로 요청이 전송되지 않아 전반적인 복원력이 향상될 수 있습니다.
4.1. 입학 컨트롤러 사례
wait-for-it init 컨테이너가 추가된 일반적인 배포는 다음과 같습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
initContainers:
- name: wait-backend
image: willwill/wait-for-it
args:
-
www.google.com:80
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
그렇게 복잡하지는 않지만(적어도 이 간단한 예에서는) 모든 배포에 관련 코드를 추가하는 데는 몇 가지 단점이 있습니다. 특히 우리는 배포 작성자에게 의존성 검사를 수행하는 방법을 정확히 지정해야 하는 부담을 부과하고 있습니다 . 대신 더 나은 경험을 위해서는 테스트 대상을 정의 하기 만 하면 됩니다.
입학 컨트롤러를 입력하십시오. 이 사용 사례를 해결하기 위해 리소스에서 특정 어노테이션의 존재를 찾고 있는 경우 initContainer 를 추가하는 변경 허용 컨트롤러를 작성합니다 . 어노테이션이 달린 배포 사양은 다음과 같습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: nginx
annotations:
com.baeldung/wait-for-it: "www.google.com:80"
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
여기에서 com.baeldung/wait-for-it 어노테이션을 사용하여 테스트해야 하는 호스트와 포트를 나타냅니다. 그러나 중요한 것은 테스트를 수행 하는 방법 을 알려주는 것이 없다는 것입니다. 이론상으로는 배포 사양을 변경하지 않고 그대로 유지하면서 어떤 식으로든 테스트를 변경할 수 있습니다.
이제 구현으로 넘어 갑시다.
4.2. 프로젝트 구조
앞에서 설명한 것처럼 외부 승인 컨트롤러는 단순한 HTTP 서비스일 뿐입니다. 이와 같이 기본 구조로 Spring Boot 프로젝트를 생성합니다. 이 예에서는 Spring Web Reactive 스타터만 있으면 되지만 실제 애플리케이션의 경우 Actuator 및/또는 일부 Cloud Config 의존성과 같은 기능을 추가하는 것도 유용할 수 있습니다.
4.3. 요청 처리
승인 요청의 진입점은 들어오는 페이로드 처리를 서비스에 Delegation하는 간단한 Spring REST 컨트롤러입니다.
@RestController
@RequiredArgsConstructor
public class AdmissionReviewController {
private final AdmissionService admissionService;
@PostMapping(path = "/mutate")
public Mono<AdmissionReviewResponse> processAdmissionReviewRequest(@RequestBody Mono<ObjectNode> request) {
return request.map((body) -> admissionService.processAdmission(body));
}
}
여기서는 ObjectNode 를 입력 매개변수로 사용하고 있습니다. 즉, API 서버에서 보낸 올바른 형식의 JSON을 처리하려고 합니다. 이 느슨한 접근 방식의 이유는 이 글을 쓰는 시점에서 아직 이 페이로드에 대해 게시된 공식 스키마가 없기 때문 입니다. 이 경우 구조화되지 않은 유형을 사용하면 약간의 추가 작업이 필요하지만 구현이 특정 Kubernetes 구현 또는 버전이 우리에게 던지기로 결정한 추가 필드를 조금 더 잘 처리하도록 합니다.
또한 요청 개체가 Kubernetes API에서 사용 가능한 리소스 중 하나일 수 있다는 점을 감안할 때 여기에 너무 많은 구조를 추가하는 것은 그다지 도움이 되지 않습니다.
4.4. 입학 요청 수정
처리의 고기는 AdmissionService 클래스에서 발생합니다. 이것은 하나의 공용 메서드인 processAdmission 을 사용하여 컨트롤러에 주입된 @Component 클래스입니다. 이 메서드는 들어오는 검토 요청을 처리하고 적절한 응답을 반환합니다.
전체 코드는 온라인에서 사용할 수 있으며 기본적으로 JSON 조작의 긴 시퀀스로 구성됩니다. 대부분은 사소하지만 일부 발췌문은 설명이 필요합니다.
if (admissionControllerProperties.isDisabled()) {
data = createSimpleAllowedReview(body);
} else if (annotations.isMissingNode()) {
data = createSimpleAllowedReview(body);
} else {
data = processAnnotations(body, annotations);
}
첫째, "비활성화" 속성을 추가하는 이유는 무엇입니까? 일부 고도로 통제된 환경에서는 기존 배포의 구성 매개변수를 제거 및/또는 업데이트하는 것보다 변경하는 것이 훨씬 쉬울 수 있습니다 . @ConfigurationProperties 메커니즘 을 사용하여 이 속성을 채우므로 실제 값은 다양한 소스에서 가져올 수 있습니다.
다음으로 누락된 어노테이션이 있는지 테스트합니다. 이를 배포를 변경하지 않고 그대로 두어야 한다는 신호로 처리합니다. 이 접근 방식은 이 경우 원하는 "옵트인" 동작을 보장합니다.
또 다른 흥미로운 스니펫은 injectInitContainer() 메서드 의 JSONPatch 생성 로직에서 가져 옵니다.
JsonNode maybeInitContainers = originalSpec.path("initContainers");
ArrayNode initContainers =
maybeInitContainers.isMissingNode() ?
om.createArrayNode() : (ArrayNode) maybeInitContainers;
ArrayNode patchArray = om.createArrayNode();
ObjectNode addNode = patchArray.addObject();
addNode.put("op", "add");
addNode.put("path", "/spec/template/spec/initContainers");
ArrayNode values = addNode.putArray("values");
values.addAll(initContainers);
들어오는 사양에 initContainers 필드 가 포함되어 있다는 보장이 없으므로 두 가지 경우를 처리해야 합니다. 누락되거나 존재할 수 있습니다. 누락된 경우 ObjectMapper 인스턴스 ( 위 스니펫의 om )를 사용하여 새 ArrayNode 를 생성합니다 . 그렇지 않으면 들어오는 배열을 사용합니다.
이렇게 하면 단일 "추가" 패치 명령을 사용할 수 있습니다. 이름에도 불구하고 그 동작은 필드가 생성되거나 동일한 이름의 기존 필드를 대체하는 것과 같습니다 . 값 필드 는 항상 원래 initContainers 배열(비어 있을 수 있음)을 포함하는 배열입니다. 마지막 단계에서는 실제 wait-for-it 컨테이너를 추가합니다.
ObjectNode wfi = values.addObject();
wfi.put("name", "wait-for-it-" + UUID.randomUUID())
// ... additional container fields added (omitted)
컨테이너 이름은 포드 내에서 고유해야 하므로 고정 접두사에 랜덤의 UUID를 추가하기만 하면 됩니다. 이렇게 하면 기존 컨테이너와 이름이 충돌하는 것을 방지할 수 있습니다.
4.5. 전개
승인 컨트롤러 사용을 시작하는 마지막 단계는 대상 Kubernetes 클러스터에 배포하는 것입니다. 예상대로 이를 위해서는 일부 YAML을 작성하거나 Terraform 과 같은 도구를 사용해야 합니다 . 어느 쪽이든 생성해야 하는 리소스는 다음과 같습니다.
- 승인 컨트롤러를 실행하기 위한 배포 입니다. 장애가 발생하면 새로운 배포가 차단될 수 있으므로 이 서비스의 복제본을 두 개 이상 돌리는 것이 좋습니다.
- API 서버에서 승인 컨트롤러를 실행하는 사용 가능한 포드로 요청을 라우팅 하는 서비스
- 서비스 로 라우팅되어야 하는 API 호출을 설명하는 MutatingWebhookConfiguration 리소스
예를 들어 배포가 생성되거나 업데이트될 때마다 Kubernetes가 승인 컨트롤러를 사용하기를 원한다고 가정해 보겠습니다. MutatingWebhookConfiguration 문서 에서 다음 과 같은 규칙 정의 를 볼 수 있습니다 .
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: "wait-for-it.baeldung.com"
webhooks:
- name: "wait-for-it.baeldung.com"
rules:
- apiGroups: ["*"]
apiVersions: ["*"]
operations: ["CREATE","UPDATE"]
resources: ["deployments"]
... other fields omitted
서버에 대한 중요한 사항: Kubernetes는 외부 승인 컨트롤러와 통신하기 위해 HTTPS가 필요합니다 . 이는 SpringBoot 서버에 적절한 인증서와 개인 키를 제공해야 함을 의미합니다. 이를 수행하는 한 가지 방법을 보려면 샘플 승인 컨트롤러를 배포하는 데 사용되는 Terraform 스크립트를 확인하십시오.
또한 간단한 팁: 설명서 어디에도 언급되지 않았지만 일부 Kubernetes 구현(예: GCP)에서는 포트 443 을 사용해야 하므로 SpringBoot HTTPS 포트를 기본값(8443)에서 변경해야 합니다.
4.6. 테스트
배포 아티팩트가 준비되면 마침내 기존 클러스터에서 승인 컨트롤러를 테스트할 차례입니다. 우리의 경우에는 Terraform을 사용하여 배포를 수행하므로 적용 만 하면 됩니다 .
$ terraform apply -auto-approve
완료되면 kubectl 을 사용하여 배포 및 승인 컨트롤러 상태를 확인할 수 있습니다 .
$ kubectl get mutatingwebhookconfigurations
NAME WEBHOOKS AGE
wait-for-it-admission-controller 1 58s
$ kubectl get deployments wait-for-it-admission-controller
NAME READY UP-TO-DATE AVAILABLE AGE
wait-for-it-admission-controller 1/1 1 1 10m
이제 어노테이션을 포함하여 간단한 nginx 배포를 만들어 보겠습니다.
$ kubectl apply -f nginx.yaml
deployment.apps/frontend created
관련 로그를 확인하여 wait-for-it init 컨테이너가 실제로 삽입되었는지 확인할 수 있습니다.
$ kubectl logs --since=1h --all-containers deployment/frontend
wait-for-it.sh: waiting 15 seconds for www.google.com:80
wait-for-it.sh: www.google.com:80 is available after 0 seconds
확실히 하기 위해 배포의 YAML을 확인하겠습니다.
$ kubectl get deployment/frontend -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
com.baeldung/wait-for-it: www.google.com:80
deployment.kubernetes.io/revision: "1"
... fields omitted
spec:
... fields omitted
template:
... metadata omitted
spec:
containers:
- image: nginx:1.14.2
name: nginx
... some fields omitted
initContainers:
- args:
- www.google.com:80
image: willwill/wait-for-it
imagePullPolicy: Always
name: wait-for-it-b86c1ced-71cf-4607-b22b-acb33a548bb2
... fields omitted
... fields omitted
status:
... status fields omitted
이 출력은 허용 컨트롤러가 배포에 추가 한 initContainer 를 보여줍니다.
5. 결론
이 기사에서는 Java에서 Kubernetes 허용 컨트롤러를 생성하고 기존 클러스터에 배포하는 방법을 다루었습니다.
늘 그렇듯이 예제의 전체 소스 코드는 GitHub 에서 찾을 수 있습니다 .