1. 소개

쿠버네티스로 한동안 작업한 후에는 많은 상용구 코드가 관련되어 있음을 곧 알게 될 것입니다. 간단한 서비스의 경우에도 일반적으로 매우 장황한 YAML 문서의 형식을 취하여 필요한 모든 세부 정보를 제공해야 합니다.

또한 주어진 환경에 배포된 여러 서비스를 처리할 때 해당 YAML 문서에는 반복되는 요소가 많이 포함되는 경향이 있습니다. 예를 들어 지정된 ConfigMap 또는 일부 사이드카 컨테이너를 모든 배포에 추가할 수 있습니다.

이 기사에서는 DRY 원칙을 고수하고 Kubernetes 승인 컨트롤러를 사용하여 이 모든 반복 코드를 피할 수 있는 방법을 살펴보겠습니다.

2. 입학 사정관이란?

승인 컨트롤러는 API 요청이 인증된 후 실행되기 전에 사전 처리하기 위해 Kubernetes에서 사용하는 메커니즘입니다.

API 서버 프로세스( kube-apiserver )에는 이미 API 처리의 특정 측면을 담당하는 여러 내장 컨트롤러가 있습니다.

AllwaysPullImage 가 좋은 예입니다. 이 허용 컨트롤러는 포드 생성 요청을 수정하므로 정보를 받은 값에 관계없이 이미지 가져오기 정책이 "항상"이 됩니다. Kubernetes 설명서 에는 표준 허용 컨트롤러의 전체 List이 포함되어 있습니다.

실제로 kubeapi-server 프로세스 의 일부로 실행되는 내장 컨트롤러 외에도 쿠버네티스 는 외부 허용 컨트롤러도 지원합니다. 이 경우 승인 컨트롤러는 API 서버에서 오는 요청을 처리하는 HTTP 서비스일 뿐입니다.

또한 이러한 외부 승인 컨트롤러는 동적으로 추가 및 제거할 수 있으므로 동적 승인 컨트롤러라는 이름이 지정되었습니다. 그 결과 다음과 같은 처리 파이프라인이 생성됩니다.

k8s 승인 컨트롤러

여기에서 들어오는 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 에서 찾을 수 있습니다 .

Generic footer banner