이 튜토리얼은 특히 첫 번째 kubernetes 연산자를 빠르게 작성하는 방법을 배우고 싶은 Java 배경이 있는 개발자를 위한 것입니다. 왜 운영자인가? 몇 가지 장점이 있습니다:
- 유지보수 대폭 감소, 키 입력 절약
- 만드는 모든 시스템에 기본으로 제공되는 탄력성
- Kubernetes 기본 사항에 대해 진지하게 배우고 배우는 재미
이론을 최소한으로 제한하고 "케이크 굽는" 방법을 확실한 레시피로 보여드리겠습니다. 저는 Java가 제 업무 경험과 가깝고 솔직히 Go보다 쉽기 때문에 선택했습니다(그러나 일부는 동의하지 않을 수도 있습니다).
바로 넘어가겠습니다.
이론 및 배경
긴 문서를 읽는 것을 좋아하는 사람은 없지만, 빨리 가슴 속으로 읽어 볼까요?
포드란 무엇인가요?
Pod는 공유 네트워크 인터페이스(고유한 IP 주소가 제공됨)와 저장소를 갖춘 컨테이너 그룹입니다.
레플리카세트란 무엇인가요?
복제본 세트는 포드 생성 및 삭제를 제어하여 매 순간 특정 템플릿에 정확히 지정된 수의 포드가 있도록 합니다.
배포란 무엇인가요?
배포는 복제본 세트를 소유하고 포드를 간접적으로 소유합니다. 생성하면 배포 포드가 생성되고 삭제하면 포드가 사라집니다.
서비스란 무엇인가요?
서비스는 여러 포드에 대한 단일 인터넷 엔드포인트입니다(로드를 균등하게 분배합니다). 클러스터 외부에서도 볼 수 있도록 노출할 수 있습니다. 엔드포인트 슬라이스 생성을 자동화합니다.
Kubernetes의 문제점은 처음부터 Stateless로 설계되었다는 것입니다. 복제본 세트는 포드의 ID를 추적하지 않습니다. 특정 포드가 사라지면 새 포드가 생성됩니다. 데이터베이스 및 캐시 클러스터와 같은 상태가 필요한 일부 사용 사례가 있습니다. 상태 저장 세트는 문제를 부분적으로만 완화합니다.
이것이 사람들이 유지 관리의 부담을 덜기 위해 연산자를 쓰기 시작한 이유입니다. 패턴과 다양한 SDK에 대해 깊이 다루지는 않겠습니다. 여기서부터 시작할 수 있습니다.
컨트롤러 및 조정
Kubernetes에서 작동하는 모든 것, 모든 작은 기계 장치는 제어 루프라는 간단한 개념을 기반으로 합니다. 따라서 이 제어 루프가 특정 리소스 유형에 대해 수행하는 작업은 매니페스트에 정의된 대로 무엇이고 무엇이 되어야 하는지 확인하는 것입니다. 불일치가 있는 경우 이를 수정하기 위해 몇 가지 작업을 수행하려고 시도합니다. 이것을 화해라고 합니다.
그리고 실제로 연산자는 동일한 개념이지만 사용자 지정 리소스에 대한 것입니다. 사용자 정의 리소스는 kubernetes api를 사용자가 정의한 일부 리소스 유형으로 확장하는 수단입니다. kubernetes에서 crd를 설정하면 가져오기, 나열, 업데이트, 삭제 등과 같은 모든 작업이 이 리소스에서 가능해집니다. 그리고 실제 작업은 어떻게 될까요? 맞습니다. 저희 운영자입니다.
동기 부여 예제 및 Java 앱
처음으로 기술을 테스트하는 경우 일반적으로 가장 기본적인 문제를 선택합니다. 개념이 특히 복잡하기 때문에 이 경우 hello world는 약간 길어질 것입니다. 어쨌든 대부분의 소스에서 가장 간단한 사용 사례는 정적 페이지 제공을 설정하는 것임을 확인했습니다.
그래서 프로젝트는 다음과 같습니다. 우리가 제공하려는 두 페이지를 나타내는 사용자 정의 리소스를 정의합니다. 해당 리소스 운영자를 적용한 후 Spring Boot에서 서비스 제공 애플리케이션을 자동으로 설정하고 페이지 콘텐츠가 포함된 구성 맵을 생성하고 구성 맵을 앱 포드의 볼륨에 마운트하고 해당 포드에 대한 서비스를 설정합니다. 여기서 재미있는 점은 리소스를 수정하면 즉시 모든 것을 다시 바인딩하고 새로운 페이지 변경 사항이 즉시 표시된다는 것입니다. 두 번째로 재미있는 점은 리소스를 삭제하면 클러스터를 깨끗하게 유지하는 모든 항목이 삭제된다는 것입니다.
자바 앱 제공
이것은 Spring Boot에서 매우 간단한 정적 페이지 서버가 될 것입니다. spring-boot-starter-web만 필요하므로 spring 초기화로 가서 다음을 선택하세요.
- 메이븐
- 자바 21
- 최신 안정 버전(저는 3.3.4)
- 그랄 VM
- 그리고 스프링 부트 스타터 웹
앱은 다음과 같습니다.
@SpringBootApplication @RestController public class WebpageServingApplication { @GetMapping(value = "/{page}", produces = "text/html") public String page(@PathVariable String page) throws IOException { return Files.readString(Path.of("/static/"+page)); } public static void main(String[] args) { SpringApplication.run(WebpageServingApplication.class, args); } }
경로 변수로 전달하는 모든 것은 /static 디렉토리(이 경우 page1 및 page2)에서 가져옵니다. 따라서 정적 디렉토리는 구성 맵에서 마운트되지만 이에 대해서는 나중에 설명합니다.
이제 네이티브 이미지를 빌드하여 원격 저장소에 푸시해야 합니다.
팁 1번
<plugin> <groupid>org.graalvm.buildtools</groupid> <artifactid>native-maven-plugin</artifactid> <configuration> <buildargs> <buildarg>-Ob</buildarg> </buildargs> </configuration> </plugin>
GraalVM을 이와 같이 구성하면 가장 낮은 메모리 소비(약 2GB)로 가장 빠른 빌드를 얻을 수 있습니다. 저에게는 메모리가 16GB밖에 없고 설치되어 있는 것도 많기 때문에 필수였습니다.
팁 2번
<plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> <configuration> <image> <publish>true</publish> <builder>paketobuildpacks/builder-jammy-full:latest</builder> <name>ghcr.io/dgawlik/webpage-serving:1.0.5</name> <env> <bp_jvm_version>21</bp_jvm_version> </env> </image> <docker> <publishregistry> <url>https://ghcr.io/dgawlik</url> <username>dgawlik</username> <password>${env.GITHUB_TOKEN}</password> </publishregistry> </docker> </configuration> </plugin>
- use paketobuildpacks/builder-jammy-full:latest while you are testing because -tiny and -base won’t have bash installed and you won’t be able to attach to container. Once you are done you can switch.
- publish true will cause building image to push it to repository, so go ahead and switch it to your repo
- BP_JVM_VERSION will be the java version of the builder image, it should be the same as the java of your project. As far as I know the latest java available is 21.
So now you do:
mvn spring-boot:build-image
And that’s it.
Operator with Fabric8
Now the fun starts. First you will need this in your pom:
<dependencies> <dependency> <groupid>io.fabric8</groupid> <artifactid>kubernetes-client</artifactid> <version>6.13.4</version> </dependency> <dependency> <groupid>io.fabric8</groupid> <artifactid>crd-generator-apt</artifactid> <version>6.13.4</version> <scope>provided</scope> </dependency> </dependencies>
crd-generator-apt is a plugin that scans a project, detects CRD pojos and generates the manifest.
Since I mentioned it, these resources are:
@Group("com.github.webserving") @Version("v1alpha1") @ShortNames("websrv") public class WebServingResource extends CustomResource<webservingspec webservingstatus> implements Namespaced { } </webservingspec>
public record WebServingSpec(String page1, String page2) { }
public record WebServingStatus (String status) { }
What is common in all resource manifests in Kubernetes is that most of them has spec and status. So you can see that the spec will consist of two pages pasted in heredoc format. Now, the proper way to handle things would be to manipulate status to reflect whatever operator is doing. If for example it is waiting on deployment to finish it would have status = “Processing”, on everything done it would patch the status to “Ready” and so on. But we will skip that because this is just simple demo.
Good news is that the logic of the operator is all in main class and really short. So step by step here it is:
KubernetesClient client = new KubernetesClientBuilder() .withTaskExecutor(executor).build(); var crdClient = client.resources(WebServingResource.class) .inNamespace("default"); var handler = new GenericResourceEventHandler(update -> { synchronized (changes) { changes.notifyAll(); } }); crdClient.inform(handler).start(); client.apps().deployments().inNamespace("default") .withName("web-serving-app-deployment").inform(handler).start(); client.services().inNamespace("default") .withName("web-serving-app-svc").inform(handler).start(); client.configMaps().inNamespace("default") .withName("web-serving-app-config").inform(handler).start();
So the heart of the program is of course Fabric8 Kuberenetes client built in first line. It is convenient to customize it with own executor. I used famous virtual threads, so when waiting on blocking io java will suspend the logic and move to main.
How here is a new part. The most basic version would be to run forever the loop and put Thread.sleep(1000) in it or so. But there is more clever way - kubernetes informers. Informer is websocket connection to kubernetes api server and it informs the client each time the subscribed resource changes. There is more to it you can read on the internet for example how to use various caches which fetch updates all at once in batch. But here it just subscribes directly per resource. The handler is a little bit bloated so I wrote a helper class GenericResourceEventHandler.
public class GenericResourceEventHandler<t> implements ResourceEventHandler<t> { private final Consumer<t> handler; public GenericResourceEventHandler(Consumer<t> handler) { this.handler = handler; } @Override public void onAdd(T obj) { this.handler.accept(obj); } @Override public void onUpdate(T oldObj, T newObj) { this.handler.accept(newObj); } @Override public void onDelete(T obj, boolean deletedFinalStateUnknown) { this.handler.accept(null); } } </t></t></t></t>
Since we only need to wake up the loop in all of the cases then we pass it a generic lambda. The idea for the loop is to wait on lock in the end and then the informer callback releases the lock each time the changes are detected.
Next:
for (; ; ) { var crdList = crdClient.list().getItems(); var crd = Optional.ofNullable(crdList.isEmpty() ? null : crdList.get(0)); var skipUpdate = false; var reload = false; if (!crd.isPresent()) { System.out.println("No WebServingResource found, reconciling disabled"); currentCrd = null; skipUpdate = true; } else if (!crd.get().getSpec().equals( Optional.ofNullable(currentCrd) .map(WebServingResource::getSpec).orElse(null))) { currentCrd = crd.orElse(null); System.out.println("Crd changed, Reconciling ConfigMap"); reload = true; }
If there is no crd then there is nothing to be done. And if the crd changed then we have to reload everything.
var currentConfigMap = client.configMaps().inNamespace("default") .withName("web-serving-app-config").get(); if(!skipUpdate && (reload || desiredConfigMap(currentCrd).equals(currentConfigMap))) { System.out.println("New configmap, reconciling WebServingResource"); client.configMaps().inNamespace("default").withName("web-serving-app-config") .createOrReplace(desiredConfigMap(currentCrd)); reload = true; }
This is for the case that ConfigMap is changed in between the iterations. Since it is mounted in pod then we have to reload the deployment.
var currentServingDeploymentNullable = client.apps().deployments().inNamespace("default") .withName("web-serving-app-deployment").get(); var currentServingDeployment = Optional.ofNullable(currentServingDeploymentNullable); if(!skipUpdate && (reload || !desiredWebServingDeployment(currentCrd).getSpec().equals( currentServingDeployment.map(Deployment::getSpec).orElse(null)))) { System.out.println("Reconciling Deployment"); client.apps().deployments().inNamespace("default").withName("web-serving-app-deployment") .createOrReplace(desiredWebServingDeployment(currentCrd)); } var currentServingServiceNullable = client.services().inNamespace("default") .withName("web-serving-app-svc").get(); var currentServingService = Optional.ofNullable(currentServingServiceNullable); if(!skipUpdate && (reload || !desiredWebServingService(currentCrd).getSpec().equals( currentServingService.map(Service::getSpec).orElse(null)))) { System.out.println("Reconciling Service"); client.services().inNamespace("default").withName("web-serving-app-svc") .createOrReplace(desiredWebServingService(currentCrd)); }
If any of the service or deployment differs from the defaults we will replace them with the defaults.
synchronized (changes) { changes.wait(); }
Then the aforementioned lock.
So now the only thing is to define the desired configmap, service and deployment.
private static Deployment desiredWebServingDeployment(WebServingResource crd) { return new DeploymentBuilder() .withNewMetadata() .withName("web-serving-app-deployment") .withNamespace("default") .addToLabels("app", "web-serving-app") .withOwnerReferences(createOwnerReference(crd)) .endMetadata() .withNewSpec() .withReplicas(1) .withNewSelector() .addToMatchLabels("app", "web-serving-app") .endSelector() .withNewTemplate() .withNewMetadata() .addToLabels("app", "web-serving-app") .endMetadata() .withNewSpec() .addNewContainer() .withName("web-serving-app-container") .withImage("ghcr.io/dgawlik/webpage-serving:1.0.5") .withVolumeMounts(new VolumeMountBuilder() .withName("web-serving-app-config") .withMountPath("/static") .build()) .addNewPort() .withContainerPort(8080) .endPort() .endContainer() .withVolumes(new VolumeBuilder() .withName("web-serving-app-config") .withConfigMap(new ConfigMapVolumeSourceBuilder() .withName("web-serving-app-config") .build()) .build()) .withImagePullSecrets(new LocalObjectReferenceBuilder() .withName("regcred").build()) .endSpec() .endTemplate() .endSpec() .build(); } private static Service desiredWebServingService(WebServingResource crd) { return new ServiceBuilder() .editMetadata() .withName("web-serving-app-svc") .withOwnerReferences(createOwnerReference(crd)) .withNamespace(crd.getMetadata().getNamespace()) .endMetadata() .editSpec() .addNewPort() .withPort(8080) .withTargetPort(new IntOrString(8080)) .endPort() .addToSelector("app", "web-serving-app") .endSpec() .build(); } private static ConfigMap desiredConfigMap(WebServingResource crd) { return new ConfigMapBuilder() .withMetadata( new ObjectMetaBuilder() .withName("web-serving-app-config") .withNamespace(crd.getMetadata().getNamespace()) .withOwnerReferences(createOwnerReference(crd)) .build()) .withData(Map.of("page1", crd.getSpec().page1(), "page2", crd.getSpec().page2())) .build(); } private static OwnerReference createOwnerReference(WebServingResource crd) { return new OwnerReferenceBuilder() .withApiVersion(crd.getApiVersion()) .withKind(crd.getKind()) .withName(crd.getMetadata().getName()) .withUid(crd.getMetadata().getUid()) .withController(true) .build(); }
The magic of the OwnerReference is that you mark the resource which is it’s parent. Whenever you delete the parent k8s will delete automatically all the dependant resources.
But you can’t run it yet. You need a docker credentials in kubernetes:
kubectl delete secret regcred kubectl create secret docker-registry regcred \ --docker-server=ghcr.io \ --docker-username=dgawlik \ --docker-password=$GITHUB_TOKEN
Run this script once. Then we also need to set up the ingress:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-ingress spec: rules: - http: paths: - path: / pathType: Prefix backend: service: name: web-serving-app-svc port: number: 8080
The workflow
So first you build the operator project. Then you take target/classes/META-INF/fabric8/webservingresources.com.github.webserving-v1.yml and apply it. From now on the kubernetes is ready to accept your crd. Here it is:
apiVersion: com.github.webserving/v1alpha1 kind: WebServingResource metadata: name: example-ws namespace: default spec: page1: | <h1 id="Hola-amigos">Hola amigos!</h1> <p>Buenos dias!</p> page2: | <h1 id="Hello-my-friend">Hello my friend</h1> <p>Good evening</p>
You apply the crd kubectl apply -f src/main/resources/crd-instance.yaml. And then you run Main of the operator.
Then monitor the pod if it is up. Next just take the ip of the cluster:
minikube ip
And in your browser navigate to /page1 and /page2.
Then try to change the crd and apply it again. After a second you should see the changes.
The end.
Conclusion
A bright observer will notice that the code has some concurrency issues. A lot can happen in between the start and the end of the loop. But there are a lot of cases to consider and tried to keep it simple. You can do it as aftermath.
Like wise for the deployment. Instead of running it in IDE you can build the image the same way as for serving app and write deployment of it. That’s basically demystification of the operator — it is just a pod like every other.
I hope you found it useful.
Thanks for reading.
I almost forgot - here is the repo:
https://github.com/dgawlik/operator-hello-world
위 내용은 Java로 kooperator 작성하기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

이 기사는 카페인 및 구아바 캐시를 사용하여 자바에서 다단계 캐싱을 구현하여 응용 프로그램 성능을 향상시키는 것에 대해 설명합니다. 구성 및 퇴거 정책 관리 Best Pra와 함께 설정, 통합 및 성능 이점을 다룹니다.

이 기사는 Lambda 표현식, 스트림 API, 메소드 참조 및 선택 사항을 사용하여 기능 프로그래밍을 Java에 통합합니다. 간결함과 불변성을 통한 개선 된 코드 가독성 및 유지 관리 가능성과 같은 이점을 강조합니다.

Java의 클래스 로딩에는 부트 스트랩, 확장 및 응용 프로그램 클래스 로더가있는 계층 적 시스템을 사용하여 클래스로드, 링크 및 초기화 클래스가 포함됩니다. 학부모 위임 모델은 핵심 클래스가 먼저로드되어 사용자 정의 클래스 LOA에 영향을 미치도록합니다.

이 기사는 캐싱 및 게으른 하중과 같은 고급 기능을 사용하여 객체 관계 매핑에 JPA를 사용하는 것에 대해 설명합니다. 잠재적 인 함정을 강조하면서 성능을 최적화하기위한 설정, 엔티티 매핑 및 모범 사례를 다룹니다. [159 문자]

이 기사에서는 Java 프로젝트 관리, 구축 자동화 및 종속성 해상도에 Maven 및 Gradle을 사용하여 접근 방식과 최적화 전략을 비교합니다.

이 기사에서는 선택기와 채널을 사용하여 단일 스레드와 효율적으로 처리하기 위해 선택기 및 채널을 사용하여 Java의 NIO API를 설명합니다. 프로세스, 이점 (확장 성, 성능) 및 잠재적 인 함정 (복잡성,

이 기사에서는 Maven 및 Gradle과 같은 도구를 사용하여 적절한 버전 및 종속성 관리로 사용자 정의 Java 라이브러리 (JAR Files)를 작성하고 사용하는 것에 대해 설명합니다.

이 기사는 네트워크 통신을위한 Java의 소켓 API, 클라이언트 서버 설정, 데이터 처리 및 리소스 관리, 오류 처리 및 보안과 같은 중요한 고려 사항에 대해 자세히 설명합니다. 또한 성능 최적화 기술, i


핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

MinGW - Windows용 미니멀리스트 GNU
이 프로젝트는 osdn.net/projects/mingw로 마이그레이션되는 중입니다. 계속해서 그곳에서 우리를 팔로우할 수 있습니다. MinGW: GCC(GNU Compiler Collection)의 기본 Windows 포트로, 기본 Windows 애플리케이션을 구축하기 위한 무료 배포 가능 가져오기 라이브러리 및 헤더 파일로 C99 기능을 지원하는 MSVC 런타임에 대한 확장이 포함되어 있습니다. 모든 MinGW 소프트웨어는 64비트 Windows 플랫폼에서 실행될 수 있습니다.

Atom Editor Mac 버전 다운로드
가장 인기 있는 오픈 소스 편집기

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

mPDF
mPDF는 UTF-8로 인코딩된 HTML에서 PDF 파일을 생성할 수 있는 PHP 라이브러리입니다. 원저자인 Ian Back은 자신의 웹 사이트에서 "즉시" PDF 파일을 출력하고 다양한 언어를 처리하기 위해 mPDF를 작성했습니다. HTML2FPDF와 같은 원본 스크립트보다 유니코드 글꼴을 사용할 때 속도가 느리고 더 큰 파일을 생성하지만 CSS 스타일 등을 지원하고 많은 개선 사항이 있습니다. RTL(아랍어, 히브리어), CJK(중국어, 일본어, 한국어)를 포함한 거의 모든 언어를 지원합니다. 중첩된 블록 수준 요소(예: P, DIV)를 지원합니다.
